Thursday, December 20, 2012

ZK Listbox: Inplace Editing with Renderer and MVVM


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

13 comments:

  1. How can we call this method

    public void onEvent (Event event) {
    Events.postEvent(ModelDataChangeEvent
    .getModelDataChangeEvent(listbox,
    new Person(oldData.getFirstName(), oldData.getLastName(), intbox.getValue()),
    listitem.getIndex()));
    }

    From ZUL page as i am generating listcell with textbox or intbox in zul page itself now i will want to get all the cells which have value changed something like dirtychecking so that i will save only that data into databse

    ReplyDelete
    Replies
    1. Since it is defined in an event listener, it cannot be called directly from zul page but will be triggered by client side event.

      Delete
  2. SO Another way we can get the only change values from listbox..like i have changed 8-10 cells values and when i will go for save i will get only changes value so that i can do less database hit.

    ReplyDelete
    Replies
    1. As you have known MVVM will trigger the setter of data bean in model to update value, I'll implement what you described as below:

      1. Add a boolean field named 'isChanged' into data bean.

      2. Put 'isChanged = true' in each setter.

      3. update to database by "for (Data d : dataList) {if (d.isChanged) {updateToDB(d)}}"

      Delete
  3. Ok but when page will be loaded first time then at that time also setter will call then isChanged will be true at that postion also

    ReplyDelete
    Replies
    1. A possible way is keep the original value, and change the isChanged boolean value if and only if value really changed.

      Delete
  4. Right now what i did, I just call a method onChange and then create a new list and added all these changed object into these list and in save method saved these values what your thinking on this

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Replies
    1. Regarding "only update changed instance to DB"? I think the above is what we can do now, mark changed or push to changed list while setter called and value is really changed, and update those marked/pushed instance as needed.

      Delete
    2. Or is there any specific issue?

      Delete
  7. Hi Ben i did not get your first comment? The scenario is this i have a listbox which contain a item with these components(textbox,checkbox ,datebox ,radio group) in a cell .If User clicked on Radio button same item's date cell will changed and user manually changed textbox value,datebox. Second one user can add new item and clicked on save button and that item default value will be saved in DB. These all some scenario in my listbox so will want to save only chnaged value or new added item.

    ReplyDelete
    Replies
    1. I mean prepare two field members for one attribute, e.g., 'oriName' and 'name' for the attribute 'userName', oriName is the original value and only assign it in constructor, when calling setter save the changed value to 'name', this way you can confirm whether the value is really changed.

      Delete