MVVM in ZK 6 - Design CRUD page by MVVM pattern

From Documentation
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.