MVVM in ZK 6 - Design CRUD page by MVVM pattern"

From Documentation
m (correct highlight (via JWB))
 
(10 intermediate revisions by 4 users not shown)
Line 2: Line 2:
 
|author=Dennis Chen, Senior Engineer, Potix Corporation
 
|author=Dennis Chen, Senior Engineer, Potix Corporation
 
|date= November 14, 2011
 
|date= November 14, 2011
|version=ZK 6
+
|version=ZK 6 FL-2012-01-11 and after
 
}}
 
}}
 +
__TOC__
  
 
In the previous article [http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_your_first_MVVM_page Design your first MVVM page] I have demonstrated how ZK Bind can be used to accomplish a search page by the MVVM pattern. In this article, I will demonstrate the whole process of how a common CRUD page can be designed using MVVM pattern, including the creation, validation when editing and confirmation when deleting.
 
In the previous article [http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_your_first_MVVM_page Design your first MVVM page] I have demonstrated how ZK Bind can be used to accomplish a search page by the MVVM pattern. In this article, I will demonstrate the whole process of how a common CRUD page can be designed using MVVM pattern, including the creation, validation when editing and confirmation when deleting.
Line 9: Line 10:
 
=Case scenario=
 
=Case scenario=
  
In this article, I will use an Order Management scenario to show you the concepts. In an Order Management page, you can list the Orders, select one to see the details and modify it and you can also create a new Order or delete an Order. I will also use 3 cases of View Model and View to demonstrate some issues that may arise when designing a MVVM page and how you can come about solving it.
+
In this article, I will use an Order Management scenario to show you the concepts. In an Order Management page, you can list the Orders, select one to see the details and modify it and you can also create a new Order or delete an Order. I will also use 3 cases of ViewModel and View to demonstrate some issues that may arise when designing a MVVM page and how you can come about solving it.
  
=Design the View Model=
+
=Design the ViewModel=
  
Before designing the View Model, we have to firstly design a domain object. In this example, the domain object is an ‘Order’ class which contains the following fields: ‘id’, ‘description’, ‘price’, ‘quantity’, ‘creationDate’ and ‘shippingDate’. Below is the partial code of the ‘Order’
+
Before designing the ViewModel, we have to firstly design a domain object. In this example, the domain object is an "Order" class which contains the following fields: "id", "description", "price", "quantity", "creationDate" and "shippingDate". Below is the partial code of the "Order"
  
 
===Domain Object : Order.java===
 
===Domain Object : Order.java===
  
<source lang="java" high="41,43">
+
<source lang="java" highlight="9,11">
 
public class Order {
 
public class Order {
 
String id;
 
String id;
Line 26: Line 27:
 
Date shippingDate;
 
Date shippingDate;
 
//getter
 
//getter
...
 
//setter
 
@NotifyChange
 
public void setId(String id) {
 
this.id = id;
 
}
 
 
@NotifyChange
 
public void setCreationDate(Date creationDate) {
 
this.creationDate = creationDate;
 
}
 
 
@NotifyChange
 
public void setShippingDate(Date shippingDate) {
 
this.shippingDate = shippingDate;
 
}
 
 
@NotifyChange
 
public void setDescription(String description) {
 
this.description = description;
 
}
 
 
@NotifyChange
 
public void setPrice(double price) {
 
this.price = price;
 
}
 
 
@NotifyChange
 
public void setQuantity(int quantity) {
 
this.quantity = quantity;
 
}
 
 
 
@DependsOn( { "price", "quantity" })
 
@DependsOn( { "price", "quantity" })
 
public double getTotalPrice() {
 
public double getTotalPrice() {
 
return price * quantity;
 
return price * quantity;
 
}
 
}
 +
...
 +
//setter
 +
...
 
}
 
}
 
</source>
 
</source>
  
In this domain object, I also added some ZK Bind annotations to notify binder when there is a change of data and to reload the View by adding <tt>@NotifyChange</tt> on a setter method of a property. After a binder set value to a property, the binder is then notified to reload attributes of components that are bound to this property. <tt>@DependsOn</tt> is another special annotation to establish dependency for a dynamic value. For example, ‘totalPrice’ is calculated by multiplying ‘price’ and ‘quantity’. It does not have a field to store its value, hence ‘totalPrice‘ depends on ‘price’ and ‘qantity’. When binder gets any change notifications of property ‘price’ or ‘quantity’, it will also mark property ‘totalPrice’ as changed.
+
In this domain object, I added a ZK Bind annotation <code>@DependsOn</code>, It is a special annotation to establish dependency for a dynamic value. For example, "totalPrice" is calculated by multiplying "price" and "quantity". It does not have a field to store its value, hence "totalPrice" depends on "price" and "qantity". When binder gets any change notifications from property "price" or "quantity", it will also mark property "totalPrice" as changed.
  
===View Model : OrderVM.java===
+
===ViewModel : OrderVM.java===
  
<source lang="java" high="20,28,34,41,47">
+
<source lang="java" highlight="19,26,31,37">
 
public class OrderVM {
 
public class OrderVM {
 
ListModelList<Order> orders;//the order list
 
ListModelList<Order> orders;//the order list
Line 81: Line 53:
 
}
 
}
  
@NotifyChange({"selected","validationMessages"})
+
@NotifyChange("selected")
 
public void setSelected(Order selected) {
 
public void setSelected(Order selected) {
 
this.selected = selected;
 
this.selected = selected;
validationMessages.clear();//clear when another order selected
 
 
}
 
}
  
 
//action commands
 
//action commands
@NotifyChange({"selected","orders","validationMessages"})
+
@Command @NotifyChange({"selected","orders"})
 
public void newOrder(){
 
public void newOrder(){
 
Order order = new Order();
 
Order order = new Order();
 
getOrders().add(order);
 
getOrders().add(order);
 
selected = order;//select the new one
 
selected = order;//select the new one
validationMessages.clear();//clear message
 
 
}
 
}
 
 
@NotifyChange({"selected","validationMessages"})
+
@Command @NotifyChange("selected")
 
public void saveOrder(){
 
public void saveOrder(){
 
getService().save(selected);
 
getService().save(selected);
validationMessages.clear();//clear message
 
 
}
 
}
 
 
@NotifyChange({"selected","orders","validationMessages"})
+
@Command @NotifyChange({"selected","orders"})
 
public void deleteOrder(){
 
public void deleteOrder(){
 
getService().delete(selected);//delete selected
 
getService().delete(selected);//delete selected
 
getOrders().remove(selected);
 
getOrders().remove(selected);
 
selected = null; //clean the selected
 
selected = null; //clean the selected
validationMessages.clear();//clear message
 
}
 
 
Map<String, String> validationMessages = new HashMap<String,String>();//validation messages
 
 
public Map<String,String> getValidationMessages(){
 
return validationMessages;
 
 
}
 
}
  
Line 124: Line 86:
 
</source>
 
</source>
  
In the example above, I used a ‘OrderService’ to ‘list(), ‘save()and delete’()orders to establish an isolation between View Model and business logic. I also declared a ‘validationMessages’ which is a <tt>Map<String,String></tt> to provide messages when manipulating View Model.
+
In the example above, I used a "OrderService" to "list()", "save()" and "delete()" orders to establish an isolation between ViewModel and business logic. In command method "newOrder()", I created a new "order" object. In command method "saveOrder()", I saved the selected "order". In command method "deleteOrder()", I deleted the selected "order" and removed it from the order list. In all command methods, I added <code>@Command</code> annotations to to them and I also added corresponding <code>@NotifyChange</code> annotations to hint changed properties caused by their corresponding command methods.
 
 
In command method ‘newOrder(), I created a new ‘order’ object and cleared the message. In command method ‘saveOrder(), I saved the selected ‘order’ and cleared the message. In command method ‘deleteOrder(), I deleted the selected ‘order’ and removed it from the order list. In all command methods, I added corresponding @NotifyChange annotations to hint changed properties caused by their corresponding command methods.
 
  
 
=Design the View & Run=
 
=Design the View & Run=
  
Following is the preview of a View binding with a View Model. I used a listbox to display the orders, 3 buttons to perform the New, Save and Delete actions, and a grid with textbox, databox to edit the details.
+
Following is the preview of a View binding with a ViewModel. I used a listbox to display the orders, 3 buttons to perform the New, Save and Delete actions, and a grid with textbox, datebox to edit the details.
  
 
[[File:smalltalks-mvvm-in-zk6-design-crud-page-view.png]]
 
[[File:smalltalks-mvvm-in-zk6-design-crud-page-view.png]]
Line 136: Line 96:
 
==The View==
 
==The View==
  
A few basic binding concepts was mentioned in the previous [http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_your_first_MVVM_page article] I will explain it again briefly here. For detailed information, please visit the referred article. Basically, I have set the <tt>apply</tt> attribute to <tt>org.zkoss.bind.BindComposer</tt> as the composer and bind the <tt>viewModel</tt> to ‘OrderVM’ that I have just created (with the variable name ''vm''). I also bound the 'model' of listbox to ‘vm.orders’ and 'selectedItem' to ‘vm.selected’. In order to show each order of the model, I bound specified properties in the template labelled with predefined converter. I also bound 3 buttons to 3 commands to perform actions on the buttons. By binding ‘selectedItem’ to ‘vm.seleted’, when an order is selected, the Save and Delete buttons are only clickable while the groupbox of the editor part is only visible.
+
A few basic binding concepts were mentioned in the previous [http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_your_first_MVVM_page article] I will explain it again briefly here. For detailed information, please visit the referred article. Basically, I have set the <code>apply</code> attribute to <code>org.zkoss.bind.BindComposer</code> as the composer and bind the <code>viewModel</code> to "OrderVM' that I have just created (with the variable name ''vm''). I also binded the "model" of listbox to "vm.orders" and "selectedItem" to "vm.selected". In order to show each order of the model, I binded specified properties in the template labelled with predefined converter. I also binded 3 buttons to 3 commands to perform actions on the buttons. By binding "selectedItem" to "vm.seleted", when an order is selected, the Save and Delete buttons are only clickable while the groupbox of the editor part is visible.
  
 
===View : order.zul===
 
===View : order.zul===
  
<source lang="xml" high="2,3,32,33">
+
<source lang="xml" highlight="3,4,33,34">
 
<window title="Order Management" border="normal" width="600px"
 
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@bind(vm='org.zkoss.bind.examples.order.OrderVM')" >
+
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM')"
<listbox model="@bind(vm.orders)" selectedItem="@bind(vm.selected)" hflex="true" height="200px">
+
validationMessages="@id('vmsgs')">
 +
<listbox model="@load(vm.orders)" selectedItem="@bind(vm.selected)" hflex="true" height="200px">
 
… <template name="model" var="item">
 
… <template name="model" var="item">
 
<listitem >
 
<listitem >
<listcell label="@bind(item.id)"/>
+
<listcell label="@load(item.id)"/>
<listcell label="@bind(item.quantity)"/>
+
<listcell label="@load(item.quantity)"/>
<listcell label="@bind(item.price) @converter('formatedNumber', format='###,##0.00')"/>
+
<listcell label="@load(item.price) @converter('formatedNumber', format='###,##0.00')"/>
<listcell label="@bind(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
+
<listcell label="@load(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
<listcell label="@bind(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
+
<listcell label="@load(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
 
</listitem>
 
</listitem>
 
</template>
 
</template>
 
</listbox>
 
</listbox>
 
<toolbar>
 
<toolbar>
<button label="New" onClick="@bind('newOrder')" />
+
<button label="New" onClick="@command('newOrder')" />
<button label="Save" onClick="@bind('saveOrder')" disabled="@bind(empty vm.selected)" />
+
<button label="Save" onClick="@command('saveOrder')" disabled="@load(empty vm.selected)" />
<button label="Delete" onClick="@bind('deleteOrder')" disabled="@bind(empty vm.selected)" />
+
<button label="Delete" onClick="@command('deleteOrder')" disabled="@load(empty vm.selected)" />
 
</toolbar>
 
</toolbar>
<groupbox visible="@bind(not empty vm.selected)" hflex="true" mold="3d">
+
<groupbox visible="@load(not empty vm.selected)" hflex="true" mold="3d">
 
<grid hflex="true" >
 
<grid hflex="true" >
 
… <rows>
 
… <rows>
<row>Id <label value="@bind(vm.selected.id)"/></row>
+
<row>Id <label value="@load(vm.selected.id)"/></row>
<row>Description <textbox value="@bind(vm.selected.description)"/></row>
+
<row>Description <textbox value="@load(vm.selected.description)"/></row>
 
<row>Quantity
 
<row>Quantity
 
<hlayout>  
 
<hlayout>  
<intbox value="@bind(vm.selected.quantity) @validator(vm.quantityValidator)"/>
+
<intbox id="qbox" value="@bind(vm.selected.quantity) @validator(vm.quantityValidator)"/>
<label value="@bind(vm.validationMessages['quantity'])" sclass="red" />
+
<label value="@load(vmsgs[qbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
 
<row>Price  
 
<row>Price  
 
<hlayout>
 
<hlayout>
<doublebox value="@bind(vm.selected.price) @validator(vm.priceValidator)" format="###,##0.00" />
+
<doublebox id="pbox" value="@bind(vm.selected.price) @validator(vm.priceValidator)" format="###,##0.00" />
<label value="@bind(vm.validationMessages['price'])" sclass="red" />
+
<label value="@load(vmsgs[pbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
<row>Total Price <label value="@bind(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
+
<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
 
<row>Creation Date  
 
<row>Creation Date  
 
<hlayout>  
 
<hlayout>  
Line 192: Line 153:
 
</window>
 
</window>
 
</source>
 
</source>
 +
To display validation messages, I binded <code>validationMessages</code>, which provides a built-in validation messages display mechanism, to 'vmsgs'.
  
Now, I am going to introduce the editor part. By binding a bean property to a user editable attribute of a component (ex, the ‘value’ of intbox), it creates two way binding (load-binding and save-binding), which means, not only the property can be loaded to the attribute of the component but also the attribute can be saved to the property if user has edited it. We also introduced the <tt>@validator(expresson)</tt> syntax which enables the validation before saving data to the bean property.  
+
Now, I am going to introduce the editor part. By binding a bean property to a user editable attribute of a component (ex, the "value" of intbox), it creates two way binding (load-binding and save-binding), which means, not only the property can be loaded to the attribute of the component but the attribute can also be saved to the property if user has edited it. We also introduced the <code>@validator(expresson)</code> syntax which enables the validation before saving data to the bean property.  
  
In the example above, I have bound the value of a doublebox to ‘vm.selected.price’ and also used the @validator(vm.priceValidator) annotation to provide a Validator for validation when saving data to ‘vm.selected.price’. The value of 'label' is also bound to ‘vm.validationMessage[key]in order to show the message in cases where validation fails.
+
In the example above, I have binded the value of a ''doublebox'' to "<b>vm.selected.price</b>" and also used the <code>@validator(vm.priceValidator)</code> annotation to provide a ''Validator'' to execute validation when saving data to "<b>vm.selected.price</b>". The value of "label" is also binded to "vmsgs[pbox]" in order to show the message in cases where validation fails in component 'pbox'.
  
==Validator in View Model==
+
==Validator in ViewModel==
  
Since I bound View with View Model with some validators, I have to provide the validator in the ViewModel. The validator can also come from other beans that depends on the expression <tt>@validator(expression)</tt>. However, providing it in the View Model is the easiest way to demonstrate its implementation.  
+
Since I bound View with ViewModel with some validators, I have to provide the validator in the ViewModel. Note that validators can also come from other beans. It all depends on the expression in  <code>@validator(expression)</code>. However, providing it in the ViewModel is the easiest way to demonstrate its implementation.  
  
===View Model : OrderVM.java===
+
===ViewModel : OrderVM.java===
  
<source lang="java" high="1,4,7,9,12">
+
<source lang="java" highlight="1,2,6">
 
public Validator getPriceValidator(){
 
public Validator getPriceValidator(){
return new Validator(){
+
return new AbstractValidator(){
 
public void validate(ValidationContext ctx) {
 
public void validate(ValidationContext ctx) {
 
Double price = (Double)ctx.getProperty().getValue();
 
Double price = (Double)ctx.getProperty().getValue();
 
if(price==null || price<=0){
 
if(price==null || price<=0){
ctx.setInvalid(); // mark invalid
+
addInvalidMessage(ctx, "must be larger than 0");
validationMessages.put("price", "must large than 0");
 
}else{
 
validationMessages.remove("price");
 
 
}
 
}
//notify messages was changed.
 
ctx.getBindContext().getBinder().notifyChange(validationMessages, "price");
 
 
}
 
}
 
};
 
};
Line 221: Line 178:
 
 
 
public Validator getQuantityValidator(){
 
public Validator getQuantityValidator(){
return new Validator(){
+
return new AbstractValidator(){
 
public void validate(ValidationContext ctx) {
 
public void validate(ValidationContext ctx) {
 
Integer quantity = (Integer)ctx.getProperty().getValue();
 
Integer quantity = (Integer)ctx.getProperty().getValue();
 
if(quantity==null || quantity<=0){
 
if(quantity==null || quantity<=0){
ctx.setInvalid();// mark invalid
+
addInvalidMessage(ctx, "must be larger than 0");
validationMessages.put("quantity", "must large than 0");
 
}else{
 
validationMessages.remove("quantity");
 
 
}
 
}
//notify messages was changed.
 
ctx.getBindContext().getBinder().notifyChange(validationMessages, "quantity");
 
 
}
 
}
 
};
 
};
Line 237: Line 189:
 
</source>
 
</source>
  
Two validators are provided: 'priceValidator' and 'quantityValidator', they are used to validate the values of price and quantity respectively in which the values has to be greater than 0. When implementing the validator, you can get the main property that needs to be validated by <tt>ValidationContext.getProperty().getValue()</tt>, if the value is not valid,  set invalid by calling <tt>ValidationContext.invalid()</tt>. In most cases, some kind of message needs to be shown if the value is not valid. In the OrderVM, there is a ‘validationMessages’ map in which it has some label bound to it in 'order.zul', hence, you can put the validation message to the map. The final step, we need to notify the binder that message has been changed. To do this, we need to get binder by <tt>ValidationContext.getBindContext().getBinder()</tt>. Binder has a java API <tt>Binder.notifyChange(base,property)</tt> which we can call upon to notify property that the message has been changed
+
Two validators are provided: "priceValidator" and 'quantityValidator', they are used to validate the values of price and quantity respectively in which the values has to be greater than 0. When implementing the validator, you can get the main property that needs to be validated by <code>ValidationContext.getProperty().getValue()</code>, if the value is not valid,  set invalid by calling <code>AbstractValidator.addInvalidMessage(ctx,message)</code>. In most cases, some kind of message needs to be shown if the value is not valid.
  
 
==Showcase 1==
 
==Showcase 1==
Line 247: Line 199:
 
The above implementation looks straight forward but it has some issues when editing an order.
 
The above implementation looks straight forward but it has some issues when editing an order.
 
*When editing a field, such as the price, the value is directly updated to the bean if it is a valid value i.e. larger than 0, however it is not automatically saved before you click the save button. So if you edit a price and move to another order, you will see that the value was changed in order list, but it is not saved by service.
 
*When editing a field, such as the price, the value is directly updated to the bean if it is a valid value i.e. larger than 0, however it is not automatically saved before you click the save button. So if you edit a price and move to another order, you will see that the value was changed in order list, but it is not saved by service.
*When editing a filed, such as the price, after editing the field, if the value is not valid, you will see the validation message while the bean still contains the old value. And you can still click the save button to save it by service with the old value.
+
*When editing a field, such as the price, after editing the field, if the value is not valid, you will see the validation message while the bean still contains the old value. And you can still click the save button to save it by service with the old value.
*When creating a new order, if clicking save directly, the values are all not been verified and saved.
+
*When creating a new order, by clicking save directly, the values are all not been verified and saved.
 
In the next section, I will introduce the batch saving and validation concept of a command to solve these issues.
 
In the next section, I will introduce the batch saving and validation concept of a command to solve these issues.
  
Line 254: Line 206:
  
 
==What is a Command Execution==
 
==What is a Command Execution==
A command execution is a mechanism of ZK Bind where it performs a method call on the View Model. It binds to a component’s event and when a bound event comes, binder will follow the lifecycle to complete the execution. There are 6 phases in the <tt>COMMAND</tt> execution: <tt>VALIDATION</tt>, <tt>SAVE-BEFORE</tt>, <tt>LOAD-BEFORE</tt>, <tt>EXECUTE</tt>, <tt>SAVE-AFTER</tt>, <tt>LOAD-AFTER</tt>.
+
A command execution is a mechanism of ZK Bind where it performs a method call on the ViewModel. It binds to a component's event and when a binding event comes, binder will follow the lifecycle to complete the execution. There are 6 phases in the <code>COMMAND</code> execution: <code>VALIDATION</code>, <code>SAVE-BEFORE</code>, <code>LOAD-BEFORE</code>, <code>EXECUTE</code>, <code>SAVE-AFTER</code>, <code>LOAD-AFTER</code>.
  
 
===Saving and Loading in Command Execution===
 
===Saving and Loading in Command Execution===
  
You could save multiple values to View Model at the same time before or after the <tt>EXECUTE</tt> phase (i.e., call the View Model’s command method) by using the <tt>@bind(save=expression before|after ‘a-command’)</tt> syntax (use this syntax, the value will not bed saved immediately after edited by user). You could also load values to a component before or after the <tt>EXECUTE</tt> phase by using the <tt>@bind(load=expression before|after ‘a-command’)</tt> syntax.
+
You could save multiple values to ViewModel at the same time before or after the <code>EXECUTE</code> phase (i.e., call the ViewModel's command method) by using the <code>@save(expression, before|after='a-command')</code> syntax (use this syntax, the value will not be saved immediately after being edited by user). You could also load values to a component before or after the <code>EXECUTE</code> phase by using the <code>@load(expression, before|after='a-command')</code> syntax.
  
 
===Validation in Command Execution===
 
===Validation in Command Execution===
  
Validation is also included in the command execution. It is performed in the <tt>VALIDATION</tt> phase before any other phases. If there are multiple save binding that depends on the same command, all validators of binding will be called in the <tt>VALIDATION</tt> phase. If a validator said invalid; the execution will be broken, and ignored in the remaining phases.
+
Validation is also included in the command execution. It is performed in the <code>VALIDATION</code> phase before any other phases. If there are multiple save binding that depends on the same command, all validators of binding will be called in the <code>VALIDATION</code> phase. If a validator said invalid; the execution will be broken, and ignored in the remaining phases.
  
 
===Phases of Command Execution===
 
===Phases of Command Execution===
Line 270: Line 222:
 
[[File:smalltalks-mvvm-in-zk6-design-crud-page-phases.png]]
 
[[File:smalltalks-mvvm-in-zk6-design-crud-page-phases.png]]
  
*When a bound ZK event enters the binder, the <tt>COMMAND</tt> phase will be invoked and all phases within the <tt>COMMAND</tt> phase will start to execute one by one
+
*When a bound ZK event enters the binder, the <code>COMMAND</code> phase will be invoked and all phases within the <code>COMMAND</code> phase will start to execute one by one
*In the <tt>VALIDATION</tt> phase, binder first collects all the properties that needs to be verified. Then, it calls each validator of save-binding that is related to this command. In each call to a validator, binder provides a new ValidationContext which contains the main property and other collected properties. This means, you can do dependent validation with collected properties, for example, checking whether the shipping date is large than the creation date. If any validator reports invalid by calling <tt>ValidationContext.setInvalid ()</tt>, binder ignores all other phase and loads any other properties that has been notified for a change, for example by calling <tt>Binder.notifyChange()</tt>.
+
*In the <code>VALIDATION</code> phase, binder first collects all the properties that needs to be verified. Then, it calls each validator of save-binding that is related to this command. In each call to a validator, binder provides a new <code>ValidationContext</code> which contains the main property and other collected properties. This means, you can do dependent validation with collected properties, for example, checking whether the shipping date is larger than the creation date. If any validator reports invalid by calling <code>ValidationContext.setInvalid ()</code> or <code>AbstractValidator.addInvalidMessage()</code>, binder ignores all other phases and loads any other properties that has been notified for a change, for example by calling <code>Binder.notifyChange()</code>.
*In the <tt>SAVE-BEFORE</tt> phase, binder calls all the save-binding that is relative to the command and mark ‘before’ to save the value to the expression
+
*In the <code>SAVE-BEFORE</code> phase, binder calls all the save-binding that is relative to the command and mark "before" to save the value to the expression
*In the <tt>LOAD-BEFORE</tt> phase, binder calls all the load-binding that is relative to the command and mark ‘before’ to load the value from expression to the component
+
*In the <code>LOAD-BEFORE</code> phase, binder calls all the load-binding that is relative to the command and mark "before" to load the value from expression to the component
*In the <tt>EXECUTE</tt> phase, binder calls the command method of the View Model. For example, if the command is ‘saveOrder', it will try to call method ‘saveOrder(Map args)’ of the View Model. If there is no such method, it will try to call method ‘saveOrder()of the View Model. If there is no method to execute, it complains with an exception.
+
*In the <code>EXECUTE</code> phase, binder calls the command method of the ViewModel. For example, if the command is "saveOrder", it will try to find a method that has annotation <code>@Command('saveOrder')</code> or a method which is named <code>saveOrder()</code> with <code>@Command()</code> of the viewModel, and then call it. If there is no method to execute, it complains with an exception.
*In the <tt>SAVE-AFTER</tt> phase, binder calls all the save-binding that is relative to the command and mark ‘after’ to save the value to the expression
+
*In the <code>SAVE-AFTER</code> phase, binder calls all the save-binding that is relative to the command and mark "after" to save the value to the expression
*In the <tt>LOAD-AFTER</tt> phase, binder calls all the load-binding that is relative to the command and mark ‘after’ to load the value from expression to component
+
*In the <code>LOAD-AFTER</code> phase, binder calls all the load-binding that is relative to the command and mark "after" to load the value from expression to component
  
 
==Redesign for Batch Saving & Validation==
 
==Redesign for Batch Saving & Validation==
Line 284: Line 236:
 
===View : order2.zul===
 
===View : order2.zul===
  
<source lang="xml" high="2,16,17,31,32,33">
+
<source lang="xml" highlight="17,18,32,33">
 
<window title="Order Management" border="normal" width="600px"
 
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@bind(vm='org.zkoss.bind.examples.order.OrderVM2')" >
+
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM2')"
 +
validationMessages="@id('vmsgs')">
 
...
 
...
 
<rows>
 
<rows>
<row>Id <label value="@bind(vm.selected.id)"/></row>
+
<row>Id <label value="@load(vm.selected.id)"/></row>
<row>Description <textbox value="@bind(vm.selected.description, save=vm.selected.description before 'saveOrder')"/></row>
+
<row>Description <textbox value="@load(vm.selected.description) @save(vm.selected.description, before='saveOrder')"/></row>
 
<row>Quantity
 
<row>Quantity
 
<hlayout>  
 
<hlayout>  
<intbox value="@bind(vm.selected.quantity, save=vm.selected.quantity before 'saveOrder')  
+
<intbox id="qbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder')  
 
@validator(vm.quantityValidator)"/>
 
@validator(vm.quantityValidator)"/>
<label value="@bind(vm.validationMessages['quantity'])" sclass="red" />
+
<label value="@load(vmsgs[qbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
 
<row>Price  
 
<row>Price  
 
<hlayout>
 
<hlayout>
<doublebox value="@bind(vm.selected.price, save=vm.selected.price before 'saveOrder')  
+
<doublebox id="pbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder')
 
@validator(vm.priceValidator)" format="###,##0.00" />
 
@validator(vm.priceValidator)" format="###,##0.00" />
<label value="@bind(vm.validationMessages['price'])" sclass="red" />
+
<label value="@load(vmsgs[pbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
<row>Total Price <label value="@bind(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
+
<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
 
<row>Creation Date  
 
<row>Creation Date  
 
<hlayout>  
 
<hlayout>  
<datebox value="@bind(vm.selected.creationDate, save=vm.selected.creationDate before 'saveOrder')
+
<datebox id="cdbox" value="@load(vm.selected.creationDate) @save(vm.selected.creationDate,before='saveOrder')
 
@validator(vm.creationDateValidator)"/>
 
@validator(vm.creationDateValidator)"/>
<label value="@bind(vm.validationMessages['creationDate'])" sclass="red" />
+
<label value="@load(vmsgs[cdbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
 
<row>Shipping Date  
 
<row>Shipping Date  
 
<hlayout>
 
<hlayout>
<datebox value="@bind(vm.selected.shippingDate, save=vm.selected.shippingDate before 'saveOrder')
+
<datebox id="sdbox" value="@load(vm.selected.shippingDate) @save(vm.selected.shippingDate,before='saveOrder')
 
@validator(vm.shippingDateValidator)"/>
 
@validator(vm.shippingDateValidator)"/>
<label value="@bind(vm.validationMessages['shippingDate'])" sclass="red" />
+
<label value="@load(vmsgs[sdbox])" sclass="red" />
 
</hlayout>
 
</hlayout>
 
</row>
 
</row>
Line 326: Line 279:
 
</source>
 
</source>
  
All <tt>@bind(expression)</tt> syntax in the grid has been changed to the <tt>@bind(expression, save=expression before ‘a-command’)</tt> syntax. For example, @bind(vm.selected.price, save=vm.selected.price before ‘saveOrder’) , so the intbox loads when ‘vm.selected.price’ is changed, and only save in ‘saveOrder’ command execution and before call to saveOrder() of View Model.
+
All <code>@bind(expression)</code> syntax in the grid has been changed to the <code>@load(expression) @save(expression, before='a-command')</code> syntax. For example, <code>@load(vm.selected.price) @save(vm.selected.price,before='saveOrder')</code> , so the intbox loads when "vm.selected.price" is changed, and only save before executing the "saveOrder" command (before calling saveOrder() of ViewModel)
  
I also added <tt>@validator(expression)</tt> to datebox to do dependent validation( shippingDate has to be larger than creationDate). The new validator is provided by View Model, so I created a new OrderVM2 extended from OrderVM to provide the new validator.  
+
I also added <code>@validator(expression)</code> to datebox to do dependent validation( shippingDate has to be larger than creationDate). The new validator is provided by the ViewModel, so I created a new OrderVM2 extended from OrderVM to provide the new validator.
  
===View Model : OrderVM2.java===
+
===ViewModel : OrderVM2.java===
<source lang="xml" high="18,21,22">
+
<source lang="java" highlight="13,16,17">
 
public class OrderVM2 extends OrderVM{
 
public class OrderVM2 extends OrderVM{
 
//validators for command
 
//validators for command
 
public Validator getCreationDateValidator(){
 
public Validator getCreationDateValidator(){
return new Validator(){
+
return new AbstractValidator(){
 
public void validate(ValidationContext ctx) {
 
public void validate(ValidationContext ctx) {
 
Date creation = (Date)ctx.getProperty().getValue();
 
Date creation = (Date)ctx.getProperty().getValue();
 
if(creation==null){
 
if(creation==null){
ctx.setInvalid();// mark invalid
+
addInvalidMessage(ctx,"must be not null");
validationMessages.put("creationDate", "must not null");
 
}else{
 
validationMessages.remove("creationDate");
 
 
}
 
}
//notify messages was changed.
 
ctx.getBindContext().getBinder().notifyChange(validationMessages, "creationDate");
 
 
}
 
}
 
};
 
};
 
}
 
}
 
public Validator getShippingDateValidator(){
 
public Validator getShippingDateValidator(){
return new Validator(){
+
return new AbstractValidator(){
 
public void validate(ValidationContext ctx) {
 
public void validate(ValidationContext ctx) {
 
Date shipping = (Date)ctx.getProperty().getValue();//the main property
 
Date shipping = (Date)ctx.getProperty().getValue();//the main property
Line 356: Line 304:
 
//do mixed validation, shipping date have to large than creation more than 3 days.
 
//do mixed validation, shipping date have to large than creation more than 3 days.
 
if(!CaldnearUtil.isDayAfter(creation,shipping,3)){
 
if(!CaldnearUtil.isDayAfter(creation,shipping,3)){
ctx.setInvalid();
+
addInvalidMessage(ctx,"must be larger than creation date at least 3 days");
validationMessages.put("shippingDate", "must large than creation date at least 3 days");
 
}else{
 
validationMessages.remove("shippingDate");
 
 
}
 
}
//notify the 'price' message in messages was changed.
 
ctx.getBindContext().getBinder().notifyChange(validationMessages, "shippingDate");
 
 
}
 
}
 
};
 
};
Line 369: Line 312:
 
</source>
 
</source>
  
In the implementation of shippingDateValidator, I get the shipping date directly by <tt>ValidationContext.getProperty().getValue()</tt> since it is the main property of this binding. To get any other dependent properties, I used <tt>ValidationContext.getProperties(property)</tt> to get the <tt>Property</tt> array (because you might bind to different beans with the same property name in the same command, such as ‘vm.i1.price’ and ‘vm.i2.price’, in this case you will get 2 properties with the name ‘price’ but with base object being ‘i1’ and ‘i2’ respectively), and since there is only one ‘creationDate’ property in this case, I can get the value from the first Property directly. Then, I simply compare the dates to execute the validation.
+
In the implementation of <code>shippingDateValidator</code>, I get the shipping date directly by <code>ValidationContext.getProperty().getValue()</code> since it is the main property of this binding. To get any other dependent properties, I used <code>ValidationContext.getProperties(property)</code> to get the <code>Property</code> array (because you might bind to different beans with the same property name in the same command, such as "vm.i1.price" and "vm.i2.price", in this case you will get 2 properties with the name "price" but with base object being "i1" and "i2" respectively), and since there is only one "creationDate" property in this case, I can get the value from the first Property directly. Then, I simply compare the dates to execute the validation.
  
 
==Showcase 2==
 
==Showcase 2==
Line 381: Line 324:
 
==Redesign for Showing a Question Dialog==
 
==Redesign for Showing a Question Dialog==
  
Back to thinking about the View Model, there will be a 2 new methods -  to show and hide the dialog, and we will also need a message to display to users. I createed an OrderVM3 extended from OrderVM2 to provide the method and message.
+
Back to thinking about the ViewModel, there will be a 2 new methods -  to show and hide the dialog, and we will also need a message to display to users. I have created an OrderVM3 which is extended from OrderVM2 to provide the method and message.
  
===View Model : OrderVM3.java===
+
===ViewModel : OrderVM3.java===
  
<source lang="java" high="3,7,9,13,15,19,21">
+
<source lang="java" highlight="3,7,9,13,15,19,21">
 
public class OrderVM3 extends OrderVM2{
 
public class OrderVM3 extends OrderVM2{
 
//message for confirming the deletion.
 
//message for confirming the deletion.
Line 391: Line 334:
 
 
 
@Override
 
@Override
@NotifyChange({"selected","orders","validationMessages","deleteMessage"})
+
@Command @NotifyChange({"selected","orders","deleteMessage"})
 
public void deleteOrder(){
 
public void deleteOrder(){
 
super.deleteOrder();
 
super.deleteOrder();
Line 397: Line 340:
 
}
 
}
 
 
@NotifyChange("deleteMessage")
+
@Command @NotifyChange("deleteMessage")
 
public void confirmDelete(){
 
public void confirmDelete(){
 
//set the message to show to user
 
//set the message to show to user
Line 403: Line 346:
 
}
 
}
 
 
@NotifyChange("deleteMessage")
+
@Command @NotifyChange("deleteMessage")
 
public void cancelDelete(){
 
public void cancelDelete(){
 
//clear the message
 
//clear the message
Line 412: Line 355:
 
</source>
 
</source>
  
In OrderVM3, I provided a deleteMessage file and added 2 new methods either to set or to clean it. I also overwritten deleteOrder as I need to clear the message after the order is deleted.
+
In OrderVM3, I provided a <code>deleteMessage</code> file and added 2 new methods either to set or to clean it. I also overwrote <code>deleteOrder</code> as I need to clear the message after the order is deleted.
  
Form the View concept, the dialog shows when the message has value and hides when the message is empty. Besides, I do not want to present the dialog to ask users to delete it when the selected order is not saved yet.
+
From the View concept, the dialog shows when the message has value and hides when the message is empty. Moreover, I do not want to present the dialog to ask users to delete when the selected order is not saved yet.
  
 
===View : order3.zul===
 
===View : order3.zul===
  
<source lang="xml" high="2,7,14,17,18">
+
<source lang="xml" highlight="2,7,14,17,18">
 
<window title="Order Management" border="normal" width="600px"
 
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@bind(vm='org.zkoss.bind.examples.order.OrderVM3')" >
+
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM3')" >
 
...
 
...
 
<toolbar>
 
<toolbar>
 
...
 
...
 
<!-- show confirm dialog when selected is persisted -->
 
<!-- show confirm dialog when selected is persisted -->
<button label="Delete" onClick="@bind(empty vm.selected.id?'deleteOrder':'confirmDelete')" disabled="@bind(empty vm.selected)" />
+
<button label="Delete" onClick="@command(empty vm.selected.id?'deleteOrder':'confirmDelete')" disabled="@load(empty vm.selected)" />
 
</toolbar>
 
</toolbar>
 
...
 
...
<window title="Confirm" mode="modal" border="normal" width="300px" visible="@bind(not empty vm.deleteMessage)">
+
<window title="Confirm" mode="modal" border="normal" width="300px" visible="@load(not empty vm.deleteMessage)">
 
<vbox hflex="true">
 
<vbox hflex="true">
 
<hlayout height="50px">
 
<hlayout height="50px">
 
<image src="~./zul/img/msgbox/question-btn.png"/>
 
<image src="~./zul/img/msgbox/question-btn.png"/>
<label value="@bind(vm.deleteMessage)"/>
+
<label value="@load(vm.deleteMessage)"/>
 
</hlayout>
 
</hlayout>
 
<hbox pack="center" hflex="true">
 
<hbox pack="center" hflex="true">
<button label="Delete" onClick="@bind('deleteOrder')"/>
+
<button label="Delete" onClick="@command('deleteOrder')"/>
<button label="Cancel" onClick="@bind('cancelDelete')"/>
+
<button label="Cancel" onClick="@command('cancelDelete')"/>
 
</hbox>
 
</hbox>
 
</vbox>
 
</vbox>
Line 443: Line 386:
 
</source>
 
</source>
  
To show a dialog, I used a modal window and bind its 'visible' to ‘not empty vm.deleteMessage’. It is really easy to show or hide a dialog this way by using ZK Bind. To perform a command, in the dialog, it has two buttons bound to commands ‘deleteOrder’ and ‘cancelDelete’ respectively. ‘deleteOrder’ is the old command to delete the order and ‘cancelDelete’ is the new command to clean the message.
+
To show a dialog, I used a modal window and bind its <code>visible</code> to <code>not empty vm.deleteMessage</code>. It is really easy to show or hide a dialog this way by using ZK Bind. To perform a command, in the dialog, it has two buttons bound to commands <code>deleteOrder</code> and <code>cancelDelete</code> respectively. <code>deleteOrder</code> is the old command to delete the order and <code>cancelDelete</code> is the new command to clean the message.
  
As a second requirement, I only want to show the dialog when the order is persisted. Depending on the implementation of this example, an order is persisted when it has an id. I bound the button with @bind(empty vm.selected.id?'deleteOrder':'confirmDelete'), when clicking on the button, depending on whether the selected order has an id or not, it will execute either the ‘deleteOrder’ or the ‘confirmDelete’ command.
+
As a second requirement, I only want to show the dialog when the order is persisted. Depending on the implementation of this example, an order is persisted when it has an id. I bound the button with <code>@command(empty vm.selected.id?'deleteOrder':'confirmDelete')</code>, and upon clicking on the button, depending on whether the selected order has an id or not, it will execute either the <code>deleteOrder</code> or the <code>confirmDelete</code> command.
  
 
==Showcase 3==
 
==Showcase 3==
Line 459: Line 402:
 
! '''Syntax'''
 
! '''Syntax'''
 
! '''Explanation'''
 
! '''Explanation'''
 +
|-
 +
| '''validationMessages="@id(name)"'''
 +
| Sets the ValidationMessages
 +
*Has to be used with a component that has <code>apply="org.zkoss.bind.BindComposer"</code>
 +
*The 'name' is the name of the ValidationMessages
 
|-
 
|-
 
| '''@validator(expression, arg = arg-expression)'''
 
| '''@validator(expression, arg = arg-expression)'''
 
| Provides a validator for a binding
 
| Provides a validator for a binding
*The ‘expression’ -  if the evaluated result is a Validator, use it directly
+
*The 'expression' -  if the evaluated result is a Validator, use it directly
*The ‘expression’- if the evaluated result is a string, then get a validator from View Model if it has a ‘getValidator(name:Stirng):Validator’ method.
+
*The 'expression'- if the evaluated result is a string, then get a validator from ViewModel if it has a 'getValidator(name:Stirng):Validator' method.
*The ‘expression’, if the evaluated result is a string and cannot find a validator from the View Model, then get a validator from ZK Bind built-in validators.
+
*The 'expression', if the evaluated result is a string and cannot find a validator from the ViewModel, then get a validator from ZK Bind built-in validators.
*You can pass many arguments to a Validator when executing validation. The ‘arg-expression’ will also be evaluated before calling to the validator method.
+
*You can pass many arguments to a Validator when executing validation. The 'arg-expression' will also be evaluated before calling to the validator method.
  
 
|-
 
|-
| '''comp-attribute="@bind(save=expression before<nowiki>|</nowiki>after 'a-command')"'''
+
| '''comp-attribute="@save(expression, before<nowiki>|</nowiki>after='a-command')"'''
| Provide a save-binding between component’s attribute and property of the expression, it have to before or after a static literal command.
+
| Provide a binding to save component's attribute to a property of the expression
*the expression has to be a savable expression(ex, ‘vm.filter’) since the value will be saved to it.
+
*The expression has to be a savable expression(ex, 'vm.filter') since the value will be saved to it.
 
*The attribute is saved to the expression in SAVE-BEFORE phase if it is before the command
 
*The attribute is saved to the expression in SAVE-BEFORE phase if it is before the command
 
*The attribute is saved to the expression in SAVE-AFTER phase if it is after the command
 
*The attribute is saved to the expression in SAVE-AFTER phase if it is after the command
Line 485: Line 433:
 
| '''@DependsOn on getter'''
 
| '''@DependsOn on getter'''
 
|Mark the properties that depends on other properties, the return value of the getter is usually a dynamic calculated value.
 
|Mark the properties that depends on other properties, the return value of the getter is usually a dynamic calculated value.
*When a change is notified to a dependent property, the binder will also be notified to reload that property.
+
*When a change is notified to a depends-on property, the binder will also be notified to reload this annoated property.
 
|}
 
|}
  
 
=Summary=
 
=Summary=
  
In this article, I have demonstrated how you can create a real CRUD page by the MVVM pattern with 3 cases. From ‘The save-binding with a validator’ to ‘Batch saving and validation by a command’ and ‘Showing a dialog for confirming a deletion’.
+
In this article, I have demonstrated how you can create a real CRUD page by the MVVM pattern with 3 cases. From 'The save-binding with a validator' to 'Batch saving and validation by a command' and finally to 'Showing a dialog for confirming a deletion'.
 
There will be more upcoming articles for this series of articles on MVVM and ZK 6, before then, any feedback is welcomed.
 
There will be more upcoming articles for this series of articles on MVVM and ZK 6, before then, any feedback is welcomed.
  
 
=Downloads=
 
=Downloads=
[[http://code.google.com/p/zkbind/downloads/list zbindexamples ]] : You can download the deployable war file here, it also contains example source code of this article  
+
[[http://code.google.com/p/zkbind/downloads/list zbindexamples ]] : You can download deployable war files here, it also contains source code of the examples in this article  
  
 
{{Template:CommentedSmalltalk_Footer|
 
{{Template:CommentedSmalltalk_Footer|
 
|name=Potix Corporation
 
|name=Potix Corporation
 
}}
 
}}

Latest revision as of 04:19, 20 January 2022

DocumentationSmall Talks2011NovemberMVVM in ZK 6 - Design CRUD page by MVVM pattern
MVVM in ZK 6 - Design CRUD page by MVVM pattern

Author
Dennis Chen, Senior Engineer, Potix Corporation
Date
November 14, 2011
Version
ZK 6 FL-2012-01-11 and after

In the previous article Design your first MVVM page I have demonstrated how ZK Bind can be used to accomplish a search page by the MVVM pattern. In this article, I will demonstrate the whole process of how a common CRUD page can be designed using MVVM pattern, including the creation, validation when editing and confirmation when deleting.

Case scenario

In this article, I will use an Order Management scenario to show you the concepts. In an Order Management page, you can list the Orders, select one to see the details and modify it and you can also create a new Order or delete an Order. I will also use 3 cases of ViewModel and View to demonstrate some issues that may arise when designing a MVVM page and how you can come about solving it.

Design the ViewModel

Before designing the ViewModel, we have to firstly design a domain object. In this example, the domain object is an "Order" class which contains the following fields: "id", "description", "price", "quantity", "creationDate" and "shippingDate". Below is the partial code of the "Order"

Domain Object : Order.java

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
	...
}

In this domain object, I added a ZK Bind annotation @DependsOn, It is a special annotation to establish dependency for a dynamic value. For example, "totalPrice" is calculated by multiplying "price" and "quantity". It does not have a field to store its value, hence "totalPrice" depends on "price" and "qantity". When binder gets any change notifications from property "price" or "quantity", it will also mark property "totalPrice" as changed.

ViewModel : OrderVM.java

public class OrderVM {
	ListModelList<Order> orders;//the order list
	Order selected;//the selected order

	public ListModelList<Order> getOrders() {
		if (orders == null) {
			orders = new ListModelList<Order>(getService().list());//init the list
		}
		return orders;
	}

	@NotifyChange("selected")
	public void setSelected(Order selected) {
		this.selected = selected;
	}

	//action commands
	@Command @NotifyChange({"selected","orders"})
	public void newOrder(){
		Order order = new Order();
		getOrders().add(order);
		selected = order;//select the new one
	}
	
	@Command @NotifyChange("selected")
	public void saveOrder(){
		getService().save(selected);
	}
	
	@Command @NotifyChange({"selected","orders"})
	public void deleteOrder(){
		getService().delete(selected);//delete selected
		getOrders().remove(selected);
		selected = null; //clean the selected
	}

	public OrderService getService() {...}

	//other getter …
	//validators
	...
}

In the example above, I used a "OrderService" to "list()", "save()" and "delete()" orders to establish an isolation between ViewModel and business logic. In command method "newOrder()", I created a new "order" object. In command method "saveOrder()", I saved the selected "order". In command method "deleteOrder()", I deleted the selected "order" and removed it from the order list. In all command methods, I added @Command annotations to to them and I also added corresponding @NotifyChange annotations to hint changed properties caused by their corresponding command methods.

Design the View & Run

Following is the preview of a View binding with a ViewModel. I used a listbox to display the orders, 3 buttons to perform the New, Save and Delete actions, and a grid with textbox, datebox to edit the details.

Smalltalks-mvvm-in-zk6-design-crud-page-view.png

The View

A few basic binding concepts were mentioned in the previous article I will explain it again briefly here. For detailed information, please visit the referred article. Basically, I have set the apply attribute to org.zkoss.bind.BindComposer as the composer and bind the viewModel to "OrderVM' that I have just created (with the variable name vm). I also binded the "model" of listbox to "vm.orders" and "selectedItem" to "vm.selected". In order to show each order of the model, I binded specified properties in the template labelled with predefined converter. I also binded 3 buttons to 3 commands to perform actions on the buttons. By binding "selectedItem" to "vm.seleted", when an order is selected, the Save and Delete buttons are only clickable while the groupbox of the editor part is visible.

View : order.zul

<window title="Order Management" border="normal" width="600px"
	apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM')"
	validationMessages="@id('vmsgs')">
	<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>
	<toolbar>
		<button label="New" onClick="@command('newOrder')" />
		<button label="Save" onClick="@command('saveOrder')" disabled="@load(empty vm.selected)" />
		<button label="Delete" onClick="@command('deleteOrder')" disabled="@load(empty vm.selected)" />
	</toolbar>
	<groupbox visible="@load(not empty vm.selected)" hflex="true" mold="3d">
		<grid hflex="true" ><rows>
				<row>Id <label value="@load(vm.selected.id)"/></row>
				<row>Description <textbox value="@load(vm.selected.description)"/></row>
				<row>Quantity
					<hlayout> 
						<intbox id="qbox" value="@bind(vm.selected.quantity) @validator(vm.quantityValidator)"/>
						<label value="@load(vmsgs[qbox])" sclass="red" />
					</hlayout>	
				</row>					
				<row>Price 
					<hlayout>
						<doublebox id="pbox" value="@bind(vm.selected.price) @validator(vm.priceValidator)" format="###,##0.00" />
						<label value="@load(vmsgs[pbox])" sclass="red" />
					</hlayout>
				</row>
				<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
				<row>Creation Date 
					<hlayout> 
						<datebox value="@bind(vm.selected.creationDate)"/>
					</hlayout>	
				</row>
				<row>Shipping Date 
					<hlayout> 
						<datebox value="@bind(vm.selected.shippingDate)" />
					</hlayout>
				</row>	
			</rows>
		</grid>
	</groupbox>
</window>

To display validation messages, I binded validationMessages, which provides a built-in validation messages display mechanism, to 'vmsgs'.

Now, I am going to introduce the editor part. By binding a bean property to a user editable attribute of a component (ex, the "value" of intbox), it creates two way binding (load-binding and save-binding), which means, not only the property can be loaded to the attribute of the component but the attribute can also be saved to the property if user has edited it. We also introduced the @validator(expresson) syntax which enables the validation before saving data to the bean property.

In the example above, I have binded the value of a doublebox to "vm.selected.price" and also used the @validator(vm.priceValidator) annotation to provide a Validator to execute validation when saving data to "vm.selected.price". The value of "label" is also binded to "vmsgs[pbox]" in order to show the message in cases where validation fails in component 'pbox'.

Validator in ViewModel

Since I bound View with ViewModel with some validators, I have to provide the validator in the ViewModel. Note that validators can also come from other beans. It all depends on the expression in @validator(expression). However, providing it in the ViewModel is the easiest way to demonstrate its implementation.

ViewModel : OrderVM.java

	public Validator getPriceValidator(){
		return new AbstractValidator(){
			public void validate(ValidationContext ctx) {
				Double price = (Double)ctx.getProperty().getValue();
				if(price==null || price<=0){
					addInvalidMessage(ctx, "must be larger than 0");
				}
			}
		};
	}
	
	public Validator getQuantityValidator(){
		return new AbstractValidator(){
			public void validate(ValidationContext ctx) {
				Integer quantity = (Integer)ctx.getProperty().getValue();
				if(quantity==null || quantity<=0){
					addInvalidMessage(ctx, "must be larger than 0");
				}
			}
		};
	}

Two validators are provided: "priceValidator" and 'quantityValidator', they are used to validate the values of price and quantity respectively in which the values has to be greater than 0. When implementing the validator, you can get the main property that needs to be validated by ValidationContext.getProperty().getValue(), if the value is not valid, set invalid by calling AbstractValidator.addInvalidMessage(ctx,message). In most cases, some kind of message needs to be shown if the value is not valid.

Showcase 1

Issues

The above implementation looks straight forward but it has some issues when editing an order.

  • When editing a field, such as the price, the value is directly updated to the bean if it is a valid value i.e. larger than 0, however it is not automatically saved before you click the save button. So if you edit a price and move to another order, you will see that the value was changed in order list, but it is not saved by service.
  • When editing a field, such as the price, after editing the field, if the value is not valid, you will see the validation message while the bean still contains the old value. And you can still click the save button to save it by service with the old value.
  • When creating a new order, by clicking save directly, the values are all not been verified and saved.

In the next section, I will introduce the batch saving and validation concept of a command to solve these issues.

Batch saving of a command

What is a Command Execution

A command execution is a mechanism of ZK Bind where it performs a method call on the ViewModel. It binds to a component's event and when a binding event comes, binder will follow the lifecycle to complete the execution. There are 6 phases in the COMMAND execution: VALIDATION, SAVE-BEFORE, LOAD-BEFORE, EXECUTE, SAVE-AFTER, LOAD-AFTER.

Saving and Loading in Command Execution

You could save multiple values to ViewModel at the same time before or after the EXECUTE phase (i.e., call the ViewModel's command method) by using the @save(expression, before|after='a-command') syntax (use this syntax, the value will not be saved immediately after being edited by user). You could also load values to a component before or after the EXECUTE phase by using the @load(expression, before|after='a-command') syntax.

Validation in Command Execution

Validation is also included in the command execution. It is performed in the VALIDATION phase before any other phases. If there are multiple save binding that depends on the same command, all validators of binding will be called in the VALIDATION phase. If a validator said invalid; the execution will be broken, and ignored in the remaining phases.

Phases of Command Execution

Following is the phases of a Command Execution:

Smalltalks-mvvm-in-zk6-design-crud-page-phases.png

  • When a bound ZK event enters the binder, the COMMAND phase will be invoked and all phases within the COMMAND phase will start to execute one by one
  • In the VALIDATION phase, binder first collects all the properties that needs to be verified. Then, it calls each validator of save-binding that is related to this command. In each call to a validator, binder provides a new ValidationContext which contains the main property and other collected properties. This means, you can do dependent validation with collected properties, for example, checking whether the shipping date is larger than the creation date. If any validator reports invalid by calling ValidationContext.setInvalid () or AbstractValidator.addInvalidMessage(), binder ignores all other phases and loads any other properties that has been notified for a change, for example by calling Binder.notifyChange().
  • In the SAVE-BEFORE phase, binder calls all the save-binding that is relative to the command and mark "before" to save the value to the expression
  • In the LOAD-BEFORE phase, binder calls all the load-binding that is relative to the command and mark "before" to load the value from expression to the component
  • In the EXECUTE phase, binder calls the command method of the ViewModel. For example, if the command is "saveOrder", it will try to find a method that has annotation @Command('saveOrder') or a method which is named saveOrder() with @Command() of the viewModel, and then call it. If there is no method to execute, it complains with an exception.
  • In the SAVE-AFTER phase, binder calls all the save-binding that is relative to the command and mark "after" to save the value to the expression
  • In the LOAD-AFTER phase, binder calls all the load-binding that is relative to the command and mark "after" to load the value from expression to component

Redesign for Batch Saving & Validation

I will use the command execution concept to solve issues that I mentioned in the first example. To resolve the issues, I changed the binding of View to the following.

View : order2.zul

<window title="Order Management" border="normal" width="600px"
	apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM2')"
	validationMessages="@id('vmsgs')">
...
		<rows>
			<row>Id <label value="@load(vm.selected.id)"/></row>
			<row>Description <textbox value="@load(vm.selected.description) @save(vm.selected.description, before='saveOrder')"/></row>
			<row>Quantity
				<hlayout> 
					<intbox id="qbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder') 
					@validator(vm.quantityValidator)"/>
					<label value="@load(vmsgs[qbox])" sclass="red" />
				</hlayout>	
			</row>					
			<row>Price 
				<hlayout>
					<doublebox id="pbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder')
					@validator(vm.priceValidator)" format="###,##0.00" />
					<label value="@load(vmsgs[pbox])" sclass="red" />
				</hlayout>
			</row>
			<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
			<row>Creation Date 
				<hlayout> 
					<datebox id="cdbox" value="@load(vm.selected.creationDate) @save(vm.selected.creationDate,before='saveOrder')
					@validator(vm.creationDateValidator)"/>
					<label value="@load(vmsgs[cdbox])" sclass="red" />
				</hlayout>	
			</row>
			<row>Shipping Date 
				<hlayout>
					<datebox id="sdbox" value="@load(vm.selected.shippingDate) @save(vm.selected.shippingDate,before='saveOrder')
					@validator(vm.shippingDateValidator)"/>
					<label value="@load(vmsgs[sdbox])" sclass="red" />
				</hlayout>
			</row>	
		</rows>
...
</window>
</zk>

All @bind(expression) syntax in the grid has been changed to the @load(expression) @save(expression, before='a-command') syntax. For example, @load(vm.selected.price) @save(vm.selected.price,before='saveOrder') , so the intbox loads when "vm.selected.price" is changed, and only save before executing the "saveOrder" command (before calling saveOrder() of ViewModel)

I also added @validator(expression) to datebox to do dependent validation( shippingDate has to be larger than creationDate). The new validator is provided by the ViewModel, so I created a new OrderVM2 extended from OrderVM to provide the new validator.

ViewModel : OrderVM2.java

public class OrderVM2 extends OrderVM{
	//validators for command
	public Validator getCreationDateValidator(){
		return new AbstractValidator(){
			public void validate(ValidationContext ctx) {
				Date creation = (Date)ctx.getProperty().getValue();
				if(creation==null){
					addInvalidMessage(ctx,"must be not null");
				}
			}
		};
	}
	public Validator getShippingDateValidator(){
		return new AbstractValidator(){
			public void validate(ValidationContext ctx) {
				Date shipping = (Date)ctx.getProperty().getValue();//the main property
				Date creation = (Date)ctx.getProperties("creationDate")[0].getValue();//the dependent
				//do mixed validation, shipping date have to large than creation more than 3 days.
				if(!CaldnearUtil.isDayAfter(creation,shipping,3)){
					addInvalidMessage(ctx,"must be larger than creation date at least 3 days");
				}
			}
		};
	}
}

In the implementation of shippingDateValidator, I get the shipping date directly by ValidationContext.getProperty().getValue() since it is the main property of this binding. To get any other dependent properties, I used ValidationContext.getProperties(property) to get the Property array (because you might bind to different beans with the same property name in the same command, such as "vm.i1.price" and "vm.i2.price", in this case you will get 2 properties with the name "price" but with base object being "i1" and "i2" respectively), and since there is only one "creationDate" property in this case, I can get the value from the first Property directly. Then, I simply compare the dates to execute the validation.

Showcase 2

Show Dialog

It is very important to ask users when he/she tries to do something critical, such as deleting an order. In this section, I am going to talk about how you can show a question dialog

Redesign for Showing a Question Dialog

Back to thinking about the ViewModel, there will be a 2 new methods - to show and hide the dialog, and we will also need a message to display to users. I have created an OrderVM3 which is extended from OrderVM2 to provide the method and message.

ViewModel : OrderVM3.java

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
}

In OrderVM3, I provided a deleteMessage file and added 2 new methods either to set or to clean it. I also overwrote deleteOrder as I need to clear the message after the order is deleted.

From the View concept, the dialog shows when the message has value and hides when the message is empty. Moreover, I do not want to present the dialog to ask users to delete when the selected order is not saved yet.

View : order3.zul

<window title="Order Management" border="normal" width="600px"
	apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM3')" >
...
		<toolbar>
...
			<!-- show confirm dialog when selected is persisted -->
			<button label="Delete" onClick="@command(empty vm.selected.id?'deleteOrder':'confirmDelete')" disabled="@load(empty vm.selected)" />
		</toolbar>
...
	<window title="Confirm" mode="modal" border="normal" width="300px" visible="@load(not empty vm.deleteMessage)">
		<vbox hflex="true">
			<hlayout height="50px">
				<image src="~./zul/img/msgbox/question-btn.png"/>
				<label value="@load(vm.deleteMessage)"/>
			</hlayout>
			<hbox pack="center" hflex="true">
				<button label="Delete" onClick="@command('deleteOrder')"/>
				<button label="Cancel" onClick="@command('cancelDelete')"/>
			</hbox>
		</vbox>
	</window>
</window>

To show a dialog, I used a modal window and bind its visible to not empty vm.deleteMessage. It is really easy to show or hide a dialog this way by using ZK Bind. To perform a command, in the dialog, it has two buttons bound to commands deleteOrder and cancelDelete respectively. deleteOrder is the old command to delete the order and cancelDelete is the new command to clean the message.

As a second requirement, I only want to show the dialog when the order is persisted. Depending on the implementation of this example, an order is persisted when it has an id. I bound the button with @command(empty vm.selected.id?'deleteOrder':'confirmDelete'), and upon clicking on the button, depending on whether the selected order has an id or not, it will execute either the deleteOrder or the confirmDelete command.

Showcase 3

Syntax review

ZUL annotation syntax

Syntax Explanation
validationMessages="@id(name)" Sets the ValidationMessages
  • Has to be used with a component that has apply="org.zkoss.bind.BindComposer"
  • The 'name' is the name of the ValidationMessages
@validator(expression, arg = arg-expression) Provides a validator for a binding
  • The 'expression' - if the evaluated result is a Validator, use it directly
  • The 'expression'- if the evaluated result is a string, then get a validator from ViewModel if it has a 'getValidator(name:Stirng):Validator' method.
  • The 'expression', if the evaluated result is a string and cannot find a validator from the ViewModel, then get a validator from ZK Bind built-in validators.
  • You can pass many arguments to a Validator when executing validation. The 'arg-expression' will also be evaluated before calling to the validator method.
comp-attribute="@save(expression, before|after='a-command')" Provide a binding to save component's attribute to a property of the expression
  • The expression has to be a savable expression(ex, 'vm.filter') since the value will be saved to it.
  • The attribute is saved to the expression in SAVE-BEFORE phase if it is before the command
  • The attribute is saved to the expression in SAVE-AFTER phase if it is after the command
  • Usually, a save-binding is done before a command.

Java syntax

Syntax Explanation
@DependsOn on getter Mark the properties that depends on other properties, the return value of the getter is usually a dynamic calculated value.
  • When a change is notified to a depends-on property, the binder will also be notified to reload this annoated property.

Summary

In this article, I have demonstrated how you can create a real CRUD page by the MVVM pattern with 3 cases. From 'The save-binding with a validator' to 'Batch saving and validation by a command' and finally to 'Showing a dialog for confirming a deletion'. There will be more upcoming articles for this series of articles on MVVM and ZK 6, before then, any feedback is welcomed.

Downloads

[zbindexamples ] : You can download deployable war files here, it also contains source code of the examples in this article


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.