Introduction
This article describe how to implement the inplace editing with renderer in MVVM.
Pre-request
ZK Listbox: Event Processing with Renderer and MVVM
http://ben-bai.blogspot.tw/2012/12/zk-listbox-event-processing-with.html
The Program
index.zul
A simple View that get/set value from ViewModel and trigger some Command as needed.
<zk>
<!-- Tested with ZK 6.0.2 -->
<div apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm') @init('test.viewmodel.TestVM')">
<hlayout>
<vlayout>
<listbox width="400px" model="@load(vm.personModel)"
selectedIndex="@bind(vm.selectedIndex)"
itemRenderer="test.renderer.InplaceEditingPersonRenderer"
onModelDataChange="@command('updateModelData')">
</listbox>
<hlayout>
<button label="show current data" onClick="@command('displayCurrentData')" />
<button label="add new" onClick="@command('addNew')" />
<button label="delete selected" onClick="@command('delSel')" />
</hlayout>
</vlayout>
<textbox rows="10" width="350px" value="@load(vm.currentData)" />
</hlayout>
</div>
</zk>
TestVM.java
Provide data and handle data changed event as needed.
package test.viewmodel;
import java.util.ArrayList;
import java.util.List;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.ListModelList;
import test.data.Person;
import test.event.ModelDataChangeEvent;
/**
* tested with ZK 6.0.2
* @author ben
*
*/
public class TestVM {
private ListModelList<Person> _personModel;
private int _selectedIndex = -1;
/**
* The hard coded person model
* @return ListModelList contains several Person
*/
public ListModel<Person> getPersonModel () {
if (_personModel == null) {
List<Person> l = new ArrayList<Person>();
l.add(new Person("First Name One", "Last Name One", 21));
l.add(new Person("First Name Two", "Last Name Two", 23));
l.add(new Person("First Name Three", "Last Name Three", 25));
_personModel = new ListModelList<Person>(l);
}
return _personModel;
}
/**
* save the selected index while select a listitem
* @param selectedIndex
*/
public void setSelectedIndex (int selectedIndex) {
_selectedIndex = selectedIndex;
}
/**
* update selected index while delSel
* @return
*/
public int getSelectedIndex () {
return _selectedIndex;
}
/**
* return all Persons' data, can be considered as "save data"
* @return
*/
public String getCurrentData () {
StringBuilder sb = new StringBuilder("");
if (_personModel != null) {
if (_selectedIndex >= 0) {
Person p = _personModel.getElementAt(_selectedIndex);
if (isValidPerson(p)) {
sb.append("Selected Person: \r\n")
.append(p.getFirstName()).append(" ")
.append(p.getLastName()).append(", age = ")
.append(p.getAge()).append("\r\n\r\n");
}
}
for (Person p : _personModel.getInnerList()) {
if (isValidPerson(p)) {
sb.append(p.getFirstName()).append(" ")
.append(p.getLastName()).append(", age = ")
.append(p.getAge()).append("\r\n");
}
}
}
return sb.toString();
}
/**
* add a new Person to _personModel,
* added to tail if no selected index,
* added below current selected item if selected idnex >= 0
*/
@Command
public void addNew () {
if (_selectedIndex >= 0) {
_personModel.add(_selectedIndex+1, new Person("", "", null));
} else {
_personModel.add(new Person("", "", null));
}
}
/**
* remove selected Person from model
*/
@Command
@NotifyChange("_selectedIndex")
public void delSel () {
if (_selectedIndex >= 0) {
_personModel.remove(_selectedIndex);
_selectedIndex = -1;
}
}
/**
* update the value of a Person based on the index and Data
* in the ModelDataChangeEvent
* @param event ModelDataChangeEvent contains the information with respect the index and changed data
*/
@Command
public void updateModelData (
@ContextParam(ContextType.TRIGGER_EVENT) ModelDataChangeEvent event) {
Person oldPerson = _personModel.get(event.getIndex());;
Person newPerson = (Person)event.getData();
oldPerson.setFirstName(newPerson.getFirstName());
oldPerson.setLastName(newPerson.getLastName());
oldPerson.setAge(newPerson.getAge());
}
/**
* simply notify that the currentData should be updated
*/
@Command
@NotifyChange("currentData")
public void displayCurrentData () {
}
/**
* confirm all fields of a Person is valid
* @param p the Person to check
* @return whether all fields of the Person is valid
*/
private boolean isValidPerson (Person p) {
return !(p.getFirstName() == null
|| p.getFirstName().isEmpty()
|| p.getLastName() == null
|| p.getLastName().isEmpty()
|| p.getAge() == null
|| p.getAge() < 0);
}
}
InplaceEditingPersonRenderer.java
Render listheaders and listitems, initial sorting, the inner textbox/intbox will handle the onChange event and pass it to listbox as needed.
package test.renderer;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zul.Intbox;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listhead;
import org.zkoss.zul.Listheader;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.Textbox;
import test.data.Person;
import test.data.PersonComparator;
import test.event.ModelDataChangeEvent;
/**
* tested with ZK 6.0.2
* @author ben
*
*/
public class InplaceEditingPersonRenderer implements ListitemRenderer<Person> {
@SuppressWarnings("unchecked")
/**
* render Person to a listitem
*/
public void render (Listitem listitem, Person data, int index) {
Listbox listbox = listitem.getListbox();
if (index == 0 && listbox.getListhead() == null) {
createListhead().setParent(listbox);
}
listitem.setValue(data);
addTextboxCell(listitem, data.getFirstName())
.addEventListener(Events.ON_CHANGE, getFirstnameChangedListener(listbox, data, listitem));
addTextboxCell(listitem, data.getLastName())
.addEventListener(Events.ON_CHANGE, getLastnameChangedListener(listbox, data, listitem));
Intbox ibx = addIntboxCell(listitem, data.getAge());
ibx.addEventListener(Events.ON_CHANGE, getAgeChangedListener(listbox, ibx, data, listitem));
}
/**
* create listhead
* @param listbox, append the listhead to it
*/
private Listhead createListhead () {
Listhead lh = new Listhead();
createListheader("First Name", "firstName").setParent(lh);
createListheader("Last Name", "lastName").setParent(lh);
createListheader("Age", "age").setParent(lh);
return lh;
}
/**
* create a listheader
* @param label the label of the listheader
* @param fieldToCompare the value to set to PersonComparator's 'field' field
* @return
*/
private Listheader createListheader (String label, String fieldToCompare) {
Listheader lhr = new Listheader(label);
lhr.setSortAscending(new PersonComparator(true, fieldToCompare));
lhr.setSortDescending(new PersonComparator(false, fieldToCompare));
return lhr;
}
/**
* add a listcell contains a textbox and return the textbox
* @param listitem the listitem to append this listcell
* @param value the value to set to the textbox
* @return Textbox the created textbox
*/
private Textbox addTextboxCell (Listitem listitem, String value) {
Listcell lc = new Listcell();
Textbox tbx = new Textbox(value);
tbx.setParent(lc);
lc.setParent(listitem);
return tbx;
}
/**
* add a listcell contains an intbox and returh the intbox
* @param listitem the listitem to append this listcell
* @param value the value to set to the intbox
* @return Intbox the created intbox
*/
private Intbox addIntboxCell (Listitem listitem, Integer value) {
Listcell lc = new Listcell();
Intbox ibx = new Intbox();
if (value != null) {
ibx.setValue(value);
}
ibx.setParent(lc);
lc.setParent(listitem);
return ibx;
}
/**
* The EventListener for firstName changed
* @param listbox the listbox, post event to it
* @param oldData the original Person, need the lastName and age of it
* @param listitem the listitem contains the textbox of this event, need the index of it
* @return EventListener that listen to firstName changed
*/
@SuppressWarnings("rawtypes")
private EventListener getFirstnameChangedListener (final Listbox listbox, final Person oldData, final Listitem listitem) {
return new EventListener () {
public void onEvent (Event event) {
InputEvent ievent = (InputEvent)event;
Events.postEvent(ModelDataChangeEvent
.getModelDataChangeEvent(listbox,
new Person(ievent.getValue(), oldData.getLastName(), oldData.getAge()),
listitem.getIndex()));
}
};
}
/**
* The EventListener for lastName changed
* @param listbox the listbox, post event to it
* @param oldData the original Person, need the firstName and age of it
* @param listitem the listitem contains the textbox of this event, need the index of it
* @return EventListener that listen to lastName changed
*/
@SuppressWarnings("rawtypes")
private EventListener getLastnameChangedListener (final Listbox listbox, final Person oldData, final Listitem listitem) {
return new EventListener () {
public void onEvent (Event event) {
InputEvent ievent = (InputEvent)event;
Events.postEvent(ModelDataChangeEvent
.getModelDataChangeEvent(listbox,
new Person(oldData.getFirstName(), ievent.getValue(), oldData.getAge()),
listitem.getIndex()));
}
};
}
/**
* The EventListener for age changed
* @param listbox the listbox, post event to it
* @param oldData the original Person, need the firstName and lastName of it
* @param listitem the listitem contains the intbox of this event, need the index of it
* @return EventListener that listen to age changed
*/
@SuppressWarnings("rawtypes")
private EventListener getAgeChangedListener (final Listbox listbox, final Intbox intbox, final Person oldData, final Listitem listitem) {
return new EventListener () {
public void onEvent (Event event) {
Events.postEvent(ModelDataChangeEvent
.getModelDataChangeEvent(listbox,
new Person(oldData.getFirstName(), oldData.getLastName(), intbox.getValue()),
listitem.getIndex()));
}
};
}
}
ModelDataChangeEvent.java
Contains the changed data and the index to apply this change in it.
package test.event;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
/**
* tested with ZK 6.0.2
* @author ben
*
*/
public class ModelDataChangeEvent extends Event {
private static final long serialVersionUID = 3645653880934243558L;
// the changed data, can be any Object
private final Object _data;
// the index to apply this change
private int _index;
/**
* Get a ModelDataChangeEvent
* @param target the target to post this event
* @param data the changed data
* @param index the index to apply this change
* @return ModelDataChangeEvent
*/
public static ModelDataChangeEvent getModelDataChangeEvent (Component target, Object data, int index) {
return new ModelDataChangeEvent("onModelDataChange", target, data, index);
}
/**
*
* @param name event name, should starts with "on"
* @param target
* @param data
* @param index
*/
public ModelDataChangeEvent (String name, Component target, Object data, int index) {
super(name, target);
_data = data;
_index = index;
}
public Object getData () {
return _data;
}
public int getIndex () {
return _index;
}
}
Person.java
Contains some fields of a Person, simple POJO.
package test.data;
public class Person {
private String _firstName;
private String _lastName;
private Integer _age;
public Person (String firstName, String lastName, Integer age) {
_firstName = firstName;
_lastName = lastName;
_age = age;
}
public void setFirstName (String firstName) {
_firstName = firstName;
}
public void setLastName (String lastName) {
_lastName = lastName;
}
public void setAge (Integer age) {
_age = age;
}
public String getFirstName () {
return _firstName;
}
public String getLastName () {
return _lastName;
}
public Integer getAge () {
return _age;
}
}
PersonComparator.java
Use it to compare two person, on specific field, in ascending or descending way.
package test.data;
import java.util.Comparator;
/**
* the Comparator to compare a Person of fields firstName,
* lastName or age in ascending or descending order
* @author ben
*
*/
public class PersonComparator implements Comparator<Person> {
private boolean _asc;
private String _field;
/**
* Constructor
* @param asc true: ascending, false: descending
* @param field to compare, firstName, lastName or age
*/
public PersonComparator (boolean asc, String field) {
_asc = asc;
_field = field;
}
/**
* compare two person
*/
public int compare(Person p1, Person p2) {
int result = 0,
mult = _asc ? 1 : -1;
result = "firstName".equals(_field)?
p1.getFirstName().compareTo(p2.getFirstName()) * mult
: "lastName".equals(_field)?
p1.getLastName().compareTo(p2.getLastName()) * mult
: "age".equals(_field)?
compareAge(p1.getAge(), p2.getAge()) * mult
: 0;
return result;
}
/**
* function for compare age
*/
private int compareAge (Integer a1, Integer a2) {
int a = (a1 == null || a1 < 0)? 0 : a1.intValue();
int b = (a2 == null || a2 < 0)? 0 : a2.intValue();
return a > b? 1
: a < b? -1
: 0;
}
}
The Result
View demo on line
http://screencast.com/t/YhtUpCtDW
Download
Project at github
https://github.com/benbai123/ZK_Practice/tree/master/Pattern/MVVM/ListboxMVVMInplaceEditing
Demo flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/Renderer_MVVM_Inplace_Editing.swf