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

From Documentation
Line 237: Line 237:
 
</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 could get the main property that needs to be validated by ‘ValidationContext.getProperty().getValue()’, if the value is not valid,  set invalid by calling ‘ValidationContext.invalid()’. 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 binded to it in 'order.zul', hence, it is possible to 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 ‘ValidationContext.getBindContext().getBinder()’. Binder has a java API ‘Binder.notifyChange(base,property)’ which we can call upon to notify that the property was 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 could get the main property that needs to be validated by ‘ValidationContext.getProperty().getValue()’, if the value is not valid,  set invalid by calling ‘ValidationContext.invalid()’. 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 binded to it in 'order.zul', hence, it is possible to 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 ‘ValidationContext.getBindContext().getBinder()’. Binder has a java API ‘Binder.notifyChange(base,property)’ which we can call upon to notify that the property has been changed
  
 
==Showcase 1==
 
==Showcase 1==

Revision as of 02:38, 11 November 2011

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
Version
ZK 6

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

Design the View Model

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’

Domain Object : Order.java

public class Order {
	String id;
	String description;
	double price;
	int quantity;
	Date creationDate;
	Date shippingDate;
	//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" })
	public double getTotalPrice() {
		return price * quantity;
	}
}

In this domain object, I also added some ZK Bind annotation to notify binder a change of data and to reload the View by adding @NotifyChange on a setter method of a property. After a binder is set to a property, it is notified to reload components that are binded to this property. @DependsOn 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 notification of change of ‘price’ or ‘quantity’, it will also mark ‘totalPrice’ as changed.

View Model : 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","validationMessages"})
	public void setSelected(Order selected) {
		this.selected = selected;
		validationMessages.clear();//clear when another order selected
	}

	//action commands
	@NotifyChange({"selected","orders","validationMessages"})
	public void newOrder(){
		Order order = new Order();
		getOrders().add(order);
		selected = order;//select the new one
		validationMessages.clear();//clear message
	}
	
	@NotifyChange({"selected","validationMessages"})
	public void saveOrder(){
		getService().save(selected);
		validationMessages.clear();//clear message
	}
	
	@NotifyChange({"selected","orders","validationMessages"})
	public void deleteOrder(){
		getService().delete(selected);//delete selected
		getOrders().remove(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;
	}

	public OrderService getService() {...}

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

In the above example, 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 Map<String,String> to provide messages when manipulating View Model.

In the ‘newOrder()’, I created a new ‘order’ object and cleared the message. In the ‘saveOrder()’, I saved the selected ‘order’ and cleared the message. In the ‘deleteOrder()’, I deleted the selected ‘order’ and removed it from the order list. In all of the methods, I added corresponding @NotifyChange to notify properties when there is a change of data.

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.

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

The View

I have mentioned a few basic binding concepts in the previous article I will explain it again briefly in the next paragraph, 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. 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 to ‘selectedItem’ ‘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.

View : order.zul

<window title="Order Management" border="normal" width="600px"
	apply="org.zkoss.bind.BindComposer" viewModel="@bind(vm='org.zkoss.bind.examples.order.OrderVM')" >
	<listbox model="@bind(vm.orders)" selectedItem="@bind(vm.selected)" hflex="true" height="200px"><template name="model" var="item">
			<listitem >
				<listcell label="@bind(item.id)"/>				
				<listcell label="@bind(item.quantity)"/>
				<listcell label="@bind(item.price) @converter('formatedNumber', format='###,##0.00')"/>
				<listcell label="@bind(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
				<listcell label="@bind(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
			</listitem>
		</template>
	</listbox>
	<toolbar>
		<button label="New" onClick="@bind('newOrder')" />
		<button label="Save" onClick="@bind('saveOrder')" disabled="@bind(empty vm.selected)" />
		<button label="Delete" onClick="@bind('deleteOrder')" disabled="@bind(empty vm.selected)" />
	</toolbar>
	<groupbox visible="@bind(not empty vm.selected)" hflex="true" mold="3d">
		<grid hflex="true" ><rows>
				<row>Id <label value="@bind(vm.selected.id)"/></row>
				<row>Description <textbox value="@bind(vm.selected.description)"/></row>
				<row>Quantity
					<hlayout> 
						<intbox value="@bind(vm.selected.quantity) @validator(vm.quantityValidator)"/>
						<label value="@bind(vm.validationMessages['quantity'])" sclass="red" />
					</hlayout>	
				</row>					
				<row>Price 
					<hlayout>
						<doublebox value="@bind(vm.selected.price) @validator(vm.priceValidator)" format="###,##0.00" />
						<label value="@bind(vm.validationMessages['price'])" sclass="red" />
					</hlayout>
				</row>
				<row>Total Price <label value="@bind(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>

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 is loaded to the attribute of a component, the attribute will also be saved to the property after user has edited it. We also introduced the @validator(expresson) syntax which enables the validation before saving data to the property of bean.

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

Validator in View Model

Since I binded 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 @validator(expression). However, providing it in the View Model is the easiest way to demonstrate its implementation.

View Model : OrderVM.java

	public Validator getPriceValidator(){
		return new Validator(){
			public void validate(ValidationContext ctx) {
				Double price = (Double)ctx.getProperty().getValue();
				if(price==null || price<=0){
					ctx.setInvalid(); // mark invalid
					validationMessages.put("price", "must large than 0");
				}else{
					validationMessages.remove("price");
				}
				//notify messages was changed.
				ctx.getBindContext().getBinder().notifyChange(validationMessages, "price");
			}
		};
	}
	
	public Validator getQuantityValidator(){
		return new Validator(){
			public void validate(ValidationContext ctx) {
				Integer quantity = (Integer)ctx.getProperty().getValue();
				if(quantity==null || quantity<=0){
					ctx.setInvalid();// mark invalid
					validationMessages.put("quantity", "must large than 0");
				}else{
					validationMessages.remove("quantity");
				}
				//notify messages was changed.
				ctx.getBindContext().getBinder().notifyChange(validationMessages, "quantity");
			}
		};
	}

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 could get the main property that needs to be validated by ‘ValidationContext.getProperty().getValue()’, if the value is not valid, set invalid by calling ‘ValidationContext.invalid()’. 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 binded to it in 'order.zul', hence, it is possible to 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 ‘ValidationContext.getBindContext().getBinder()’. Binder has a java API ‘Binder.notifyChange(base,property)’ which we can call upon to notify that the property has been changed

Showcase 1

Issues

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

  • When edit a field, such as the price, the value is directly update to bean if is valid, however it is not saved before you click the save button. So if you edit a price and move to another order. You will see the value was changed in order list, but the value is not saved by service.
  • When edit a filed, such as the price, after edit the field, if the value is not valid, you will see the validation message, the bean is 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 click save directly, the value are all not been verified and saved.

In 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 Command Execution

A command execution is a mechanism of ZK Bind to perform a method call on the View Model. It binds to a component’s event, when a bound event comes, binder will follow the lifecycle to complete the execution, there 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 View Model at the same time before or after ‘EXECUTE’ phase (i.e., call the View Model’s command method) by using @bind(save=expression before|after ‘a-command’) syntax (use this syntax, the value will not save immediately after user changed it). You could also load values to component before or after ‘EXECUTE’ phase by using @bind(load=expression before|after ‘a-command’) syntax.

Validation in Command Execution

The validation is also included in the command execution. It is performed in VALIDATION phase before any other phases. If there are multiple save binding that depends on the same command. All validators of bindings will be called in the VALIDATION phase. If a validator said invalid; the execution will be break, and ignore the remained phases.

Phases of Command Execution

Following is the phases of Command Execution:

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

  • When a bound zk event coming, binder enters COMMAND phase which will invoke another other phase
  • In VALIDATION phase, binder first collects all the properties that need to be verified. Then it calls each validator of save-binding that related with this command. In each call to a validator, binder provides a new ValidationContext which contains the main property and other collected properties. Which means, you could do dependent validation by collected properties, for example, checking the shipping date have to large than creation date. If any validator reports invalid by calling ValidationContext.setInvalid (), binder ignores the other phase, then it loads any other properties that notifies changed dynamically, for example by calling Binder.notifyChange().
  • In SAVE-BEFORE phase, binder call all the save-binding that is relative to the command and marked ‘before’ to save the value to expression
  • In LOAD-BEFORE phase, binder call all the load-binding that is relative to the command and marked ‘before’ to load the value from expression to the component
  • In EXECUTE phase, binder call 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 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 SAVE-AFTER phase, binder call all the save-binding that is relative to the command and marked ‘after’ to save the value to expression
  • In LOAD-AFTER phase, binder call all the load-binding that is relative to the command and marked ‘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 fix it, I change the binding of the View as following.

View : order2.zul

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

I change all @bind syntax in the grid to @bind(expression, save=expression before ‘a-command’) syntax. For example, @bind(vm.selected.price, save=vm.selected.price before ‘saveOrder’) , so the intbox is loading when ‘vm.selected.price’ changed, and only save in ‘saveOrder’ command execution and before call to saveOrder() of View Model.

I also add @validator() to datebox to do dependent validation( shippingDate have to later than creationDate). The new validator is provided by View Model, so I create a new OrderVM2 extends OrderVM to provide the validator.

View Model : OrderVM2.java

public class OrderVM2 extends OrderVM{
	//validators for command
	public Validator getCreationDateValidator(){
		return new Validator(){
			public void validate(ValidationContext ctx) {
				Date creation = (Date)ctx.getProperty().getValue();
				if(creation==null){
					ctx.setInvalid();// mark invalid
					validationMessages.put("creationDate", "must not null");
				}else{
					validationMessages.remove("creationDate");
				}
				//notify messages was changed.
				ctx.getBindContext().getBinder().notifyChange(validationMessages, "creationDate");
			}
		};
	}
	public Validator getShippingDateValidator(){
		return new Validator(){
			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)){
					ctx.setInvalid();
					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");
			}
		};
	}
}

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 use ValidationContext.getProperties(property) to get the Property array (because you might bind to different bean with same property name in the same command, such as ‘vm.i1.price’ and ‘vm.i2.price’, in this case you will get 2 Property which’s name is ‘price’ but the base object is ‘i1’ and ‘i2’ respectively), since there is only 1 ‘creationDate’ property in this case, I get the value form the first Property directly. Then I simply compare the date to do the validation.

Showcase 2

Show Dialog

It is very important to ask user when he tries to do something is critical, such as deletes an order. I am going to talk about how to show a question dialog in this section.

Redesign for Showing a Question Dialog

Back to think about the View Model, there will be a 2 new method to show and hide the dialog, and also need a message to display to user. I create an OrderVM3 extends OrderVM2 to provide the method and message.

View Model : OrderVM3.java

public class OrderVM3 extends OrderVM2{
	//message for confirming the deletion.
	String deleteMessage;
	
	@Override
	@NotifyChange({"selected","orders","validationMessages","deleteMessage"})
	public void deleteOrder(){
		super.deleteOrder();
		deleteMessage = null;
	}
	
	@NotifyChange("deleteMessage")
	public void confirmDelete(){
		//set the message to show to user
		deleteMessage = "Do you want to delete "+selected.getId()+" ?";
	}
	
	@NotifyChange("deleteMessage")
	public void cancelDelete(){
		//clear the message
		deleteMessage = null;
	}
	//getter
}

In OrderVM3, I provide a deleteMessage filed and add 2 new methods to set or clean it. I also override deleteOrder because 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 don’t want to show the dialog to ask user to delete it when the selected order is not save yet.

View : order3.zul

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

To show a dialog, I use a modal window and bind its ‘visible’ to ‘not empty vm.deleteMessage’. It is really easy to show or hide a dialog by this way in ZK Bind. To perform command, in the dialog, it has two buttons bind to command ‘deleteOrder’ and ‘cancelDelete’ respectively. ‘deleteOrder’ is the old command to delete the order and ‘cancelDelete’ is the new command to clean the message. In 2nd requirement, I only want to show the dialog when the order is persisted. Depends on the implementation of this example, an order is persisted when it has an id. I bind the button with @bind(empty vm.selected.id?'deleteOrder':'confirmDelete'), when clicking on the button, depends on the selected order has id or not, it will execute ‘deleteOrder’ or ‘confirmDelete’ command.

Showcase 3

Syntax review

ZUL annotation syntax

Syntax Explanation
@validator(expression, arg = arg-expression) Provide a validator for a binding
  • The ‘expression’, if the evaluated result is a Validator, uses 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 and cannot find validtor from view model, then get validator from ZK Bind built-in validators.
  • You could pass many arguments to Validator when doing validation. The ‘arg-expression’ will be evaluated also before calling to the validator method.
comp-attribute="@bind(save=expression before|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.
  • the expression has to be a savable expression(ex, ‘vm.filter’) since the value will save to it.
  • The attribute is save to expression in SAVE-BEFORE phase if it is before the command
  • The attribute is save to expression in SAVE-AFTER phase if it is after the command
  • Usually, a save-binding is doing before a command.

Java syntax

Syntax Explanation
@DependsOn on getter Mark the property depends on other properties, the return value of the getter is usually a dynamic calculated value.
  • When any notify change of the dependent property, will also notify binder to reload this property.

Summary

In this article, I demonstrate how to create a real CRUD page by MVVM pattern with 3 cases. Form ‘The save-binding with a validator’ to ‘Batch saving and validation by a command’ and ‘Showing a dialog for confirming a deletion’. There are still many things that I will talk about in this series of MVVM in ZK 6 article, give us any feedback is always welcome.

Downloads

[zbindexamples ] : You could download the deployable war file here, it also contains example source code of this article


Comments



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