From Documentation
Contents |
Overview
In ZK there are two approaches that you can utilize to create your application UI - either use XML like ZK User Interface Markup Language, ZUML or use pure Java like you would if you were developing desktop GUI application with Java Swing. This second approach to create UI with ZK can be achieved by implementing a Richlet.
Previously, we have already covered how to implement MVVM pattern if you are using ZUML files for your application views. In this article I will demonstrate how to use MVVM pattern within a Richlet where you are creating your application UI using Java. Briefly speaking, unlike normal way of creating your views as ZUML files and specifying ZK MVVM annotations on the component tags, in Richlet one normally creates UI with various components by instantiating new instances of their respective classes, assembling the component tree that would represent the UI on the client and adding various bindings on components using Binder APIs.
For this smalltalk I will use the same Order Management example used in previous smalltalk and rewrite it as a Richlet. I wouldn't go into details of MVVM pattern itself as they are covered in detail in previous smalltalks. You can also refer to MVVM section of Developer Reference for more details.
Case Scenario
The Order Management application allows you to list orders and view details of specific order. Users are also able to create a new order and modify or delete an existing one.
Design the ViewModel
In MVVM, ViewModel contains view's state but it is loosely coupled with View. The data binding mechanism synchronizes data between View and ViewModel. Keeping this in mind and since we are only re-writing Order Management example but with Java here it is easy to imagine there is no change in the ViewModel from the previous smalltalk. The Order domain object and OrderVM3 ViewModel can be reused just as it is.
Order domain object
In brief, The Order domain object contains the following fields: "id", "description", "price", "quantity", "creationDate", "shippingDate", and their respective getters and setters.
public class Order {
String id;
String description;
double price;
int quantity;
Date creationDate;
Date shippingDate;
//getter
@DependsOn( { "price", "quantity" })
public double getTotalPrice() {
return price * quantity;
}
...
//setter
...
}
OrderVM3 ViewModel
The OrderVM3 ViewModel contains the state of view like list of all orders and currently selected order in addition to MVVM Notification annotations to specify when to reload which properties to UI components. OrderVM3 ViewModel also uses OrderService to encapsulate and decouple business logic for CRUD operations on an Order from ViewModel.
public class OrderVM3 extends OrderVM2{
//message for confirming the deletion.
String deleteMessage;
@Override
@Command @NotifyChange({"selected","orders","deleteMessage"})
public void deleteOrder(){
super.deleteOrder();
deleteMessage = null;
}
@Command @NotifyChange("deleteMessage")
public void confirmDelete(){
//set the message to show to user
deleteMessage = "Do you want to delete "+selected.getId()+" ?";
}
@Command @NotifyChange("deleteMessage")
public void cancelDelete(){
//clear the message
deleteMessage = null;
}
//getter
}
Initialize the binder
Before creating our UI we can create a Binder instance and initialize it with our ViewModel. In our Richlet we can make use of DefaultBinder which is a Binder implementation similar to AnnotateBinder. Initializing DefaultBinder is a four step process
- Create root component which is to be associated with binder
- Instantiate a binder instance
- Initialize it with View model and root component
- Set binder as an attribute on the root component
public class OrderRichlet extends GenericRichlet {
private Binder binder;
public void service(Page page) {
//1.Create root component which is to be associated with binder
Window window = new Window("Order Management", "normal", false);
window.setWidth("800px");
window.setPage(page);
//2.Instantiate a binder instance. Use DefaultBinder
binder = new DefaultBinder();
//3. Initialize it with View model and root component
binder.init(window, new OrderVM3(), null);
//4. Set binder as an attribute on the root component
window.setAttribute("vm", binder.getViewModel());
....
binder.loadComponent(window,true);
...
Create UI and add bindings
Following is the preview of a View binding with a ViewModel. Here a listbox is used to display a list of all orders, three buttons to perform Create New, modify & Save and Delete an order operations. Right after the list of all orders we will have a grid with labels, textboxes, dateboxes to edit the details of a selected order.
Orders listbox
Create UI
In Richlet environment we will create this UI by instantiating instances of UI components and assembling component tree to represent above shown UI with the help of setParent/appendChild component APIs. Below is a code snippet that constructs the order listbox.
Starting with orders listbox, let's first create Listbox and its columns, associate it with ViewModel state and also instruct binder to use a template to render it correctly.
private Component buildOrderListbox(Binder binder){
Listbox listbox = new Listbox();
listbox.setHeight("200px");
listbox.setHflex("true");
Listhead head = new Listhead();
listbox.appendChild(head);
head.appendChild(new Listheader("Id"));
head.appendChild(new Listheader("Quantity"));
head.appendChild(new Listheader("Price"));
head.appendChild(new Listheader("Creation Date"));
head.appendChild(new Listheader("Shipping Date"));
// add load and save bindings using Binder APIs
...
// set listbox template to instruct binder how to render it
return listbox;
}
Add bindings
Once the Listbox component and its corresponding Listheaders are created and assembled, next we need to instruct binder how to bind it with the ViewModel state.
Property bindings
As stated above, OrderVM3 ViewModel contains the View's state in the form of the list of all orders in the system. We can use Binder API Binder.addPropertyLoadBindings(Component, String, String, String[], String[], Map, String, Map) to add load binding for orders property of ViewModel with model attribute of listbox as shown below
binder.addPropertyLoadBindings(listbox, "model", "vm.orders", null, null, null, null, null);
Similarly, we can also add load binding for selected property of viewmodel with selectedItem of listbox as shown below
binder.addPropertyLoadBindings(listbox, "selectedItem", "vm.selected", null, null, null, null, null);
However, whenever user changes the selected order in the listbox we wish the ViewModel state to be updated to indicate currently selected order. To achieve this we can use Binder.addPropertySaveBindings(Component, String, String, String[], String[], Map, String, Map, String, Map) to add save binding for selected whenever selectedItem changes on a listbox component.
binder.addPropertySaveBindings(listbox, "selectedItem", "vm.selected", null, null, null, null, null,null,null);
Template bindings
The last step is to create and associate a template with listbox which later can be utilized by the binder to render the listbox. Here we will implement listbox template for our orders listbox by implementing Template interface and override Template.create(Component, Component, VariableResolver, Composer) API to define how child components are created and add bindings to them.
class ListboxTemplate implements Template {
@SuppressWarnings("rawtypes")
public Component[] create(Component parent, Component insertBefore,
VariableResolver resolver, Composer composer){
//create template components & add binding expressions
Listitem listitem = new Listitem();
Listcell idCell = new Listcell();
listitem.appendChild(idCell);
binder.addPropertyLoadBindings(idCell, "label", "item.id", null, null, null, null, null);
Listcell quantityCell = new Listcell();
listitem.appendChild(quantityCell);
binder.addPropertyLoadBindings(quantityCell, "label", "item.quantity", null, null, null, null, null);
Listcell priceCell = new Listcell();
listitem.appendChild(priceCell);
binder.addPropertyLoadBindings(priceCell, "label", "item.price", null, null, null, SYS_NUMBER_CONVERTER, formatedNumberArg);
Listcell creationDateCell = new Listcell();
listitem.appendChild(creationDateCell);
binder.addPropertyLoadBindings(creationDateCell, "label", "item.creationDate", null, null, null, SYS_DATE_CONVERTER, formatedDateArg);
Listcell shippingDateCell = new Listcell();
listitem.appendChild(shippingDateCell);
binder.addPropertyLoadBindings(shippingDateCell, "label", "item.shippingDate", null, null, null, SYS_DATE_CONVERTER, formatedDateArg);
//append to the parent
if (insertBefore ==null){
parent.appendChild(listitem);
}else{
parent.insertBefore(listitem, insertBefore);
}
Component[] components = new Component[1];
components [0] = listitem;
return components;
}
public Map<String, Object> getParameters(){
Map<String,Object> parameters = new HashMap<String, Object>();
//set binding variable
parameters.put("var","item");
return parameters;
}
}
As shown above, template consists of one Listitem and several Listcell components to display particular order's properties. We create instance of Listitem and append Listcell instance to it.We also add load bindings to individual Listcell instance by calling binder's addPropertyLoadBindings API to bind item variable's properties with label attribute of Listcell components. This will cause binder to load item variable property values into individual Listcell label attribute. Finally we set this template to our orders listbox as shown below
listbox.setTemplate("model", new ListboxTemplate());
These above two code snippets are equivalent to following ZUML if we were to define UI and specify annotations in a ZUML file.
<listbox model="@load(vm.orders)" selectedItem="@bind(vm.selected)" hflex="true" height="200px">
...
<template name="model" var="item">
<listitem >
<listcell label="@load(item.id)"/>
<listcell label="@load(item.quantity)"/>
<listcell label="@load(item.price) @converter('formatedNumber', format='###,##0.00')"/>
<listcell label="@load(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
<listcell label="@load(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
</listitem>
</template>
</listbox>
CRUD Toolbar
CRUD toolbar consists of three buttons for creating New order, update or delete selected one.
Create UI components
We create a Toolbar instance and add three Button instances for New, Save and Delete operations as shown below.
private Toolbar buildToolbar(Binder binder){
Button newButton = new Button("New");
Button saveButton = new Button("Save");
Button deleteButton = new Button("Delete");
Toolbar toolbar = new Toolbar();
toolbar.appendChild(newButton);
toolbar.appendChild(saveButton);
toolbar.appendChild(deleteButton);
...
Add bindings
Next we add command bindings on these buttons by using addCommandBindings API of Binder as shown below.
binder.addCommandBinding(newButton, Events.ON_CLICK, "'newOrder'", null);
binder.addCommandBinding(saveButton, Events.ON_CLICK, "'saveOrder'", null);
binder.addPropertyLoadBindings(saveButton, "disabled", "empty vm.selected", null, null, null, null, null);
binder.addCommandBinding(deleteButton, Events.ON_CLICK, "empty vm.selected.id?'deleteOrder':'confirmDelete'", null);
binder.addPropertyLoadBindings(deleteButton, "disabled", "empty vm.selected", null, null, null, null, null);
Here we are binding ViewModel's newOrder command with newButton component's onClick event. Whenever end user clicks on New button, binder will invoke newOrder() in ViewModel.
Order details form
Finally we will have an Order details form to input new order details or update details of a selected order. The basic idea is to bind selected ViewModel property with form and its children components. The details form should be only shown when ViewModel's selected is not empty. This can be done as shown below.
Groupbox form = new Groupbox();
// bind viewmodel selected property with visible attribute of our form
binder.addPropertyLoadBindings(form, "visible", "not empty vm.selected", null, null, null, null, null);
This is how it works - Whenever user clicks New button, ViewModel will trigger newOrder() method in ViewModel which will create a new Order domain object instance, add to orders listbox and set selected state to indicate this new order. - Similarly, as explained above we have a save binding for selectedItem of listbox so whenever user selects an order in listbox ViewModel's selected is updated to reflect the current selection.
and since selected is bound to visible attribute of our form, it will be only displayed if end user clicks New button and makes a selection in orders listbox.
Creat UI and add bindings
Details form is grid of two columns. First column showing the order field name using a Label component and second column showing an input component with current value of the order property. Below is a code snippet that builds this form grid and the field bindings with selected properties.
// create grid with two columns
Grid grid = new Grid();
grid.setHflex("true");
grid.setParent(form);
Columns columns = new Columns();
Column labelCol = new Column();
labelCol.setWidth("120px");
columns.appendChild(labelCol);
columns.appendChild(new Column());
grid.appendChild(columns);
Rows rows = new Rows();
grid.appendChild(rows);
Row idRow = new Row();
idRow.appendChild(new Label("Id"));
Label idLabel = new Label();
idRow.appendChild(idLabel);
rows.appendChild(idRow);
binder.addPropertyLoadBindings(idLabel, "value", "vm.selected.id", null, null, null, null, null);
Row descriptionRow = new Row();
descriptionRow.appendChild(new Label("Description"));
Textbox descriptionBox = new Textbox();
descriptionRow.appendChild(descriptionBox);
rows.appendChild(descriptionRow);
//add load and save bindings for description
String[] beforeCommand = {"saveOrder"};
binder.addPropertyLoadBindings(descriptionBox, "value", "vm.selected.description", null, null, null, null, null);
binder.addPropertySaveBindings(descriptionBox, "value", "vm.selected.description", beforeCommand, null, null, null, null, null, null);
...
Here are few notes from above code
- Line 20 adds only load binding for viewmodel's selected order's id property. Id of an order will be autogenerated and hence doesn't need to changed or updated once created hence no save binding.
- Line 29,30 adds load as well as save binding for viewmodel's selected order's description property. Note that the addPropertySaveBindings API fourth parameter indicates the description to be saved before binder handle saveOrder command.
Load component
Once all property load/save and command bindings are added we need to instruct binder to load component. This will cause binder to actually process the load bindings and synchronize the ViewModel state with View.
binder.loadComponent(window,true);
Summary
In this article we demostated how to use MVVM pattern in a Richlet when you are creating UI with pure Java. The important difference with usual way of using MVVM with ZUML files as views is that you need to create binder instance, initialize it with ViewModel and add bindings on components through binder APIs. Much of MVVM benefit of decoupling View from ViewModel is still achieved with this.
Downloads
- Please download this order richlet example code from here
- You could also download the deployable war file from zbindexamples, it also contains example source code of this article.
Comments
| Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |
