MVVM in ZK 6 - Design CRUD page by MVVM pattern"
m |
m (correct highlight (via JWB)) |
||
(15 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
{{Template:Smalltalk_Author| | {{Template:Smalltalk_Author| | ||
|author=Dennis Chen, Senior Engineer, Potix Corporation | |author=Dennis Chen, Senior Engineer, Potix Corporation | ||
− | |date=November | + | |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 | + | 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 | + | =Design the ViewModel= |
− | Before designing the | + | 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"> | + | <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 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
@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 | + | 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. |
− | === | + | ===ViewModel : OrderVM.java=== |
− | <source lang="java"> | + | <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( | + | @NotifyChange("selected") |
public void setSelected(Order selected) { | public void setSelected(Order selected) { | ||
this.selected = selected; | this.selected = selected; | ||
− | |||
} | } | ||
//action commands | //action commands | ||
− | @NotifyChange({"selected","orders | + | @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 | ||
− | |||
} | } | ||
− | @NotifyChange( | + | @Command @NotifyChange("selected") |
public void saveOrder(){ | public void saveOrder(){ | ||
getService().save(selected); | getService().save(selected); | ||
− | |||
} | } | ||
− | @NotifyChange({"selected","orders | + | @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 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
Line 124: | Line 86: | ||
</source> | </source> | ||
− | In the example above, I used a | + | 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 | ||
=Design the View & Run= | =Design the View & Run= | ||
− | Following is the preview of a View binding with a | + | 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 | + | 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"> | + | <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="@ | + | apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM')" |
− | <listbox model="@ | + | 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="@ | + | <listcell label="@load(item.id)"/> |
− | <listcell label="@ | + | <listcell label="@load(item.quantity)"/> |
− | <listcell label="@ | + | <listcell label="@load(item.price) @converter('formatedNumber', format='###,##0.00')"/> |
− | <listcell label="@ | + | <listcell label="@load(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/> |
− | <listcell label="@ | + | <listcell label="@load(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/> |
</listitem> | </listitem> | ||
</template> | </template> | ||
</listbox> | </listbox> | ||
<toolbar> | <toolbar> | ||
− | <button label="New" onClick="@ | + | <button label="New" onClick="@command('newOrder')" /> |
− | <button label="Save" onClick="@ | + | <button label="Save" onClick="@command('saveOrder')" disabled="@load(empty vm.selected)" /> |
− | <button label="Delete" onClick="@ | + | <button label="Delete" onClick="@command('deleteOrder')" disabled="@load(empty vm.selected)" /> |
</toolbar> | </toolbar> | ||
− | <groupbox visible="@ | + | <groupbox visible="@load(not empty vm.selected)" hflex="true" mold="3d"> |
<grid hflex="true" > | <grid hflex="true" > | ||
… <rows> | … <rows> | ||
− | <row>Id <label value="@ | + | <row>Id <label value="@load(vm.selected.id)"/></row> |
− | <row>Description <textbox value="@ | + | <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="@ | + | <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="@ | + | <label value="@load(vmsgs[pbox])" sclass="red" /> |
</hlayout> | </hlayout> | ||
</row> | </row> | ||
− | <row>Total Price <label value="@ | + | <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 | + | 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 binded the value of a doublebox to | + | 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 | + | ==Validator in ViewModel== |
− | Since I | + | 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. |
− | === | + | ===ViewModel : OrderVM.java=== |
− | <source lang="java"> | + | <source lang="java" highlight="1,2,6"> |
public Validator getPriceValidator(){ | public Validator getPriceValidator(){ | ||
− | return new | + | 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 | + | addInvalidMessage(ctx, "must be larger than 0"); |
− | |||
− | |||
− | |||
} | } | ||
− | |||
− | |||
} | } | ||
}; | }; | ||
Line 221: | Line 178: | ||
public Validator getQuantityValidator(){ | public Validator getQuantityValidator(){ | ||
− | return new | + | 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 | + | addInvalidMessage(ctx, "must be larger than 0"); |
− | |||
− | |||
− | |||
} | } | ||
− | |||
− | |||
} | } | ||
}; | }; | ||
Line 237: | Line 189: | ||
</source> | </source> | ||
− | Two validators are provided: | + | 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 | + | *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, | + | *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 | + | 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 | + | 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 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. | + | 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 COMMAND phase will be invoked and all phases within the COMMAND 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 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 | + | *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 SAVE-BEFORE phase, binder calls all the save-binding that is relative to the command and mark | + | *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 LOAD-BEFORE phase, binder calls all the load-binding that is relative to the command and mark | + | *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 EXECUTE phase, binder calls the command method of the | + | *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 SAVE-AFTER phase, binder calls all the save-binding that is relative to the command and mark | + | *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 LOAD-AFTER phase, binder calls all the load-binding that is relative to the command and mark | + | *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"> | + | <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="@ | + | apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM2')" |
+ | validationMessages="@id('vmsgs')"> | ||
... | ... | ||
<rows> | <rows> | ||
− | <row>Id <label value="@ | + | <row>Id <label value="@load(vm.selected.id)"/></row> |
− | <row>Description <textbox value="@ | + | <row>Description <textbox value="@load(vm.selected.description) @save(vm.selected.description, before='saveOrder')"/></row> |
<row>Quantity | <row>Quantity | ||
<hlayout> | <hlayout> | ||
− | <intbox value="@ | + | <intbox id="qbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder') |
@validator(vm.quantityValidator)"/> | @validator(vm.quantityValidator)"/> | ||
− | <label value="@ | + | <label value="@load(vmsgs[qbox])" sclass="red" /> |
</hlayout> | </hlayout> | ||
</row> | </row> | ||
<row>Price | <row>Price | ||
<hlayout> | <hlayout> | ||
− | <doublebox value="@ | + | <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="@ | + | <label value="@load(vmsgs[pbox])" sclass="red" /> |
</hlayout> | </hlayout> | ||
</row> | </row> | ||
− | <row>Total Price <label value="@ | + | <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="@ | + | <datebox id="cdbox" value="@load(vm.selected.creationDate) @save(vm.selected.creationDate,before='saveOrder') |
@validator(vm.creationDateValidator)"/> | @validator(vm.creationDateValidator)"/> | ||
− | <label value="@ | + | <label value="@load(vmsgs[cdbox])" sclass="red" /> |
</hlayout> | </hlayout> | ||
</row> | </row> | ||
<row>Shipping Date | <row>Shipping Date | ||
<hlayout> | <hlayout> | ||
− | <datebox value="@ | + | <datebox id="sdbox" value="@load(vm.selected.shippingDate) @save(vm.selected.shippingDate,before='saveOrder') |
@validator(vm.shippingDateValidator)"/> | @validator(vm.shippingDateValidator)"/> | ||
− | <label value="@ | + | <label value="@load(vmsgs[sdbox])" sclass="red" /> |
</hlayout> | </hlayout> | ||
</row> | </row> | ||
Line 326: | Line 279: | ||
</source> | </source> | ||
− | All @bind syntax in the grid has been changed to the @ | + | 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 @validator() to datebox to do dependent validation( shippingDate has to be larger than creationDate). The new validator is provided by | + | 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. |
− | === | + | ===ViewModel : OrderVM2.java=== |
− | <source lang="java"> | + | <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 | + | 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 | + | addInvalidMessage(ctx,"must be not null"); |
− | |||
− | |||
− | |||
} | } | ||
− | |||
− | |||
} | } | ||
}; | }; | ||
} | } | ||
public Validator getShippingDateValidator(){ | public Validator getShippingDateValidator(){ | ||
− | return new | + | 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 | + | addInvalidMessage(ctx,"must be larger than creation date at least 3 days"); |
− | |||
− | |||
− | |||
} | } | ||
− | |||
− | |||
} | } | ||
}; | }; | ||
Line 369: | Line 312: | ||
</source> | </source> | ||
− | 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 | + | 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 | + | 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=== |
− | <source lang="java"> | + | <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 | + | @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 | + | 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. |
− | + | 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=" | + | <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="@ | + | 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="@ | + | <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="@ | + | <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="@ | + | <label value="@load(vm.deleteMessage)"/> |
</hlayout> | </hlayout> | ||
<hbox pack="center" hflex="true"> | <hbox pack="center" hflex="true"> | ||
− | <button label="Delete" onClick="@ | + | <button label="Delete" onClick="@command('deleteOrder')"/> |
− | <button label="Cancel" onClick="@ | + | <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 | + | 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 | + | 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 | + | *The 'expression' - if the evaluated result is a Validator, use it directly |
− | *The | + | *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 | + | *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 | + | *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="@ | + | | '''comp-attribute="@save(expression, before<nowiki>|</nowiki>after='a-command')"''' |
− | | Provide a save | + | | 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-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 | + | *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 | + | 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 | + | [[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
Dennis Chen, Senior Engineer, Potix Corporation
November 14, 2011
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.
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:
- When a bound ZK event enters the binder, the
COMMAND
phase will be invoked and all phases within theCOMMAND
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 newValidationContext
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 callingValidationContext.setInvalid ()
orAbstractValidator.addInvalidMessage()
, binder ignores all other phases and loads any other properties that has been notified for a change, for example by callingBinder.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 namedsaveOrder()
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
|
@validator(expression, arg = arg-expression) | Provides a validator for a binding
|
comp-attribute="@save(expression, before|after='a-command')" | Provide a binding to save component's attribute to a property of the expression
|
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.
|
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. |