Saturday, December 15, 2012

ZK Listbox: Event Processing with Renderer and MVVM



Introduction

Usually we will use template and binding data directly in listbox while using MVVM, but there are some issues such like the zul file and ViemModel might be tightly coupled by the event processing in template, or you cannot use template since some framework limitation, or your data cannot rendered by template well.

However, finally you make a decision that to use ListitemRenderer and you still need to process some events, instead of go through the 'binding vm via java code', you can also try add EventListener in ListitemRenderer and pass some custom event with required data to listbox then bind that custom event of Listbox to ViewModel.

This article will describe how to pass event to listbox from inner item.

Note: You can just ignore the 'template' and 'binding vm via java code' mentioned above, they are actually not so related to this article.

Pre-request

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

ZK Build View in ZUL file or Java Code (the java code part)
http://ben-bai.blogspot.tw/2012/12/zk-build-view-in-zul-file-or-java-code.html

ZK Listening/Processing Event in ZUL Page or Java Code
http://ben-bai.blogspot.tw/2012/12/zk-listeningprocessing-event-in-zul.html

Pass Event to Other Component
http://ben-bai.blogspot.tw/2012/12/pass-event-to-other-component.html

ZK Listbox: Using ListitemRenderer to Render Complex Data in Listbox
http://ben-bai.blogspot.tw/2012/12/zk-listbox-using-listitemrenderer-to.html

The Program

index.zul

In the zul file, we get model from vm, assign renderer to listbox, and execute some command in vm wile the custom onOrderEvent event of listbox is triggered.

As you can see, this is a very simple View for MVVM.

<zk>
    <!-- Tested with ZK 6.0.2 -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.viewmodel.TestVM')">
        <hbox>
            <!-- simply get itemModel from vm, use ItemRenderer
                to render items and to preprocess inner event
                and trigger orderItem event while the onOrderEvent
                custom event -->
            <listbox width="400px" model="@load(vm.itemModel)"
                itemRenderer="test.renderer.ItemRenderer"
                onOrderEvent="@command('orderItem')">
                <listhead>
                    <listheader label="Item Name" />
                    <listheader label="Available Amount" />
                    <listheader label="Order Amount" />
                    <listheader label="Order Button" />
                </listhead>    
            </listbox>
            <!-- simply get orderModel from vm, use OrderRenderer
                to render items and to preprocess inner event
                and trigger cancelOrder event while the onOrderEvent
                custom event -->
            <listbox width="400px" model="@load(vm.orderModel)"
                itemRenderer="test.renderer.OrderRenderer"
                onOrderEvent="@command('cancelOrder')">
                <listhead>
                    <listheader label="Item Name" />
                    <listheader label="Ordered Amount" />
                    <listheader label="Cancel Button" />
                </listhead>
            </listbox>
        </hbox>
    </div>
</zk>


TestVM.java

In the ViewModel, there is a Model ShoppingCart in it, and have some getter to provide model, and some command that will be triggered by OrderEvent.

As you can see, this is a very simple ViewModel for MVVM

package test.viewmodel;

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 test.data.Item;
import test.data.Order;
import test.data.ShoppingCart;
import test.event.OrderEvent;

/**
 * Tested with ZK 6.0.2
 * 
 * The VM interact with the view
 * 
 * @author benbai123
 *
 */
public class TestVM {
    ShoppingCart _cart = new ShoppingCart();

    public ListModel<Item> getItemModel () {
        return _cart.getItemModel();
    }
    public ListModel<Order> getOrderModel () {
        return _cart.getOrderModel();
    }
    @Command
    @NotifyChange({"itemModel", "orderModel"})
    public void orderItem (
            @ContextParam(ContextType.TRIGGER_EVENT) OrderEvent event) {
        _cart.orderItem(event);
    }
    @Command
    @NotifyChange({"itemModel", "orderModel"})
    public void cancelOrder (
            @ContextParam(ContextType.TRIGGER_EVENT) OrderEvent event) {
        _cart.cancelOrder(event);
    }
}


Item.java

Contains information of an item

package test.data;

/**
 * Item class contains item name and amount
 * @author benbai123
 *
 */
public class Item {
    private String _itemName;
    private int _amount;

    public Item (String itemName, int amount) {
        _itemName = itemName;
        _amount = amount;
    }
    public String getItemName () {
        return _itemName;
    }
    public void setAmount (int amount) {
        _amount = amount;
    }
    public int getAmount () {
        return _amount;
    }
}


Order.java

Contains information of an order.

package test.data;

/**
 * Order class contains item (the Item class) and item index
 * (item index received while order that item) 
 * @author benbai123
 *
 */
public class Order {
    private Item _item;
    private int _itemIndex;

    public Order (Item item, int itemIndex) {
        _item = item;
        _itemIndex = itemIndex;
    }
    public Item getItem () {
        return _item;
    }
    public int getItemIndex () {
        return _itemIndex;
    }
}


ShoppingCart.java

Contains a list of item and a list of order, also handle the 'order item' and 'cancel order' in it.

package test.data;

import java.util.ArrayList;
import java.util.List;

import org.zkoss.zul.ListModel;
import org.zkoss.zul.ListModelList;

import test.event.OrderEvent;

/**
 * The shoppingcart contains the item model and order model
 * also handle the orderItem and cancelOrder here
 * 
 * @author benbai123
 *
 */
public class ShoppingCart {
    private ListModelList<Item> _itemModel;
    private ListModelList<Order> _orderModel;

    public ListModel<Item> getItemModel () {
        if (_itemModel == null) {
            List<Item> items = new ArrayList<Item>();
            items.add(new Item("Item One", 5));
            items.add(new Item("Item Two", 3));
            items.add(new Item("Item Three", 10));
            _itemModel = new ListModelList<Item>(items);
        }
        return _itemModel;
    }
    public ListModel<Order> getOrderModel () {
        if (_orderModel == null) {
            _orderModel = new ListModelList<Order>(new ArrayList<Order>());
        }
        return _orderModel;
    }

    public void orderItem (OrderEvent event) {
        addOrder (event.getItemName(), event.getAmount(), event.getItemIndex());
        decreaseItemAmount (event.getItemIndex(), event.getAmount());
    }

    public void cancelOrder (OrderEvent event) {
        removeOrder (event.getItemName());
        increaseItemAmount (event.getItemIndex(), event.getAmount());
    }
    private void decreaseItemAmount (int index, int amount) {
        Item item = _itemModel.getElementAt(index);
        item.setAmount(item.getAmount() - amount);
    }
    private void increaseItemAmount (int index, int amount) {
        Item item = _itemModel.getElementAt(index);
        item.setAmount(item.getAmount() + amount);
    }
    private void addOrder (String itemName, int amount, int itemIndex) {
        Order order = getOrderByName(itemName);
        if (order != null) {
            Item item = order.getItem();
            item.setAmount(item.getAmount() + amount);
        } else {
            order = new Order(new Item(itemName, amount), itemIndex);
            _orderModel.add(order);
        }
    }
    private void removeOrder (String itemName) {
        Order order = getOrderByName(itemName);;
        if (order != null) {
            _orderModel.remove(order);
        }
    }
    private Order getOrderByName (String itemName) {
        int size = _orderModel.getSize();
        Order order;
        for (int i = 0; i < size; i++) {
            order = _orderModel.getElementAt(i);
            if (itemName.equals(order.getItem().getItemName())) {
                return order;
            }
        }
        return null;
    }
}


ItemRenderer.java

Renderer to render a list of Item.

The order button will process the onClick event itself, and pass the OrderEvent with required data to listbox as needed. This let us only need to face the event we are interested (the OrderEvent of the Item List) instead of face all button click then handle all situation in ViewModel.

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.util.Clients;

import org.zkoss.zul.Button;
import org.zkoss.zul.Intbox;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.Listitem;

import test.data.Item;
import test.event.OrderEvent;

/**
 * Tested with ZK 6.0.2
 * 
 * ItemRenderer render Item class to listitem
 * @author benbai123
 *
 */
public class ItemRenderer implements ListitemRenderer<Item> {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void render (final Listitem listitem, final Item data, final int index) {
        final Listbox lb = listitem.getListbox();
        // item name and available amount
        new Listcell(data.getItemName()).setParent(listitem);
        new Listcell(data.getAmount() + "").setParent(listitem);

        // intbox for input the amount to order
        Listcell lc = new Listcell();
        final Intbox intbox = new Intbox();
        intbox.setParent(lc);
        lc.setParent(listitem);

        // button to perform the order action
        lc = new Listcell();
        Button orderBtn = new Button("Order Item");
        orderBtn.addEventListener(Events.ON_CLICK, new EventListener() {
            public void onEvent (Event event) {
                // if the value is not grater zero or over the available amount
                // show alert message
                int amount = intbox.getValue() == null? 0 : intbox.getValue();
                if (amount <= 0) {
                    Clients.alert(" positive value only");
                } else if (amount > data.getAmount()) {
                    Clients.alert(" over available amount ");
                } else {
                    // post an OrderEvent to listbox
                    // to inform order action with the
                    // item name and the amount to order
                    Item item = new Item(data.getItemName(), amount);
                    Events.postEvent(OrderEvent.getOrderEvent(lb, item, index));
                }
            }
        });
        orderBtn.setParent(lc);
        lc.setParent(listitem);
    }
}


OrderRenderer.java

Renderer to render a list of Order.

Similar to the order button, the cancel button will process the onClick event itself, and pass the OrderEvent with required data to listbox as needed.

package test.renderer;

import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;

import org.zkoss.zul.Button;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;

import test.data.Item;
import test.data.Order;
import test.event.OrderEvent;

/**
 * Tested with ZK 6.0.2
 * 
 * OrderRenderer render Order class to listitem
 * @author benbai123
 *
 */
public class OrderRenderer implements ListitemRenderer<Order> {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void render (final Listitem listitem, final Order data, final int index) {
        final Listbox lb = listitem.getListbox();

        // get item from order
        Item item = data.getItem();
        // item name and ordered amount
        new Listcell(item.getItemName()).setParent(listitem);
        new Listcell(item.getAmount() + "").setParent(listitem);

        // button to perform the cancel action
        Listcell lc = new Listcell();
        Button cancelBtn = new Button("Cancel Order");
        cancelBtn.addEventListener(Events.ON_CLICK, new EventListener() {
            public void onEvent (Event event) {
                // post an OrderEvent to listbox
                // to inform cancel action with the
                // item name and the ordered amount to restore
                Events.postEvent(
                    OrderEvent.getOrderEvent(lb, data.getItem(), data.getItemIndex())
                );
            }
        });
        cancelBtn.setParent(lc);
        lc.setParent(listitem);
    }
}


OrderEvent.java

Contains the data of an order for both to order item or to cancel order.

package test.event;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

import test.data.Item;

/**
 * Tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class OrderEvent extends Event {
    private static final long serialVersionUID = 3645653880934243558L;

    private final String _itemName;
    // itemIndex denotes the index to restore the amount
    private final int _itemIndex;
    private final int _amount;

    public static OrderEvent getOrderEvent (Component target, Item data, int index) {
        return new OrderEvent("onOrderEvent", target, data.getItemName(), index, data.getAmount());
    }
    public OrderEvent (String name, Component target, String itemName, int itemIndex, int amount) {
        super(name, target);
        _itemName = itemName;
        _itemIndex = itemIndex;
        _amount = amount;
    }
    public String getItemName () {
        return _itemName;
    }
    public int getItemIndex () {
        return _itemIndex;
    }
    public int getAmount () {
        return _amount;
    }
}


The Result

View demo online
http://screencast.com/t/XZNxVOzbs

Extended Reading

http://stackoverflow.com/questions/13818305/pure-java-binding-in-zk-listbox-with-itemrenderer

http://www.zkoss.org/forum/listComment/20938-Listbox-using-template-how-can-I-get-the-previous-row-so-can-use-in-logic-for-current-row

Download

Demo flash at github
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/handle_event_in_item_renderer_with_mvvm.swf

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Pattern/MVVM/RendererWithMVVM

5 comments:

  1. Hi ,

    Please tell me one thing if i am doing ..
    editListbox.addRadioGroupCell(listitem, model, null).addEventListener(
    Events.ON_CHECK,
    getRadioButtonChangedListener(listbox, data, listitem));

    WIll this event will fire when page is rendered? I think it have to fire when someone click on radio .I am wrong ?

    ReplyDelete
    Replies
    1. No, it should not fire when page is rendered, or it should be a bug and you can report it at ZK tracker: http://tracker.zkoss.org/secure/IssueNavigator.jspa?mode=hide&requestId=10001

      Delete
  2. Hmm It is initializing only when page rendering not firing event sorry wrong assumption by me

    ReplyDelete
  3. Hi

    Any help on http://forum.zkoss.org/question/43546/bandbox-autocomplete/

    ReplyDelete