ZK8 Wizard Example - Part 3 final

From Documentation
Documentationobertwenzel
obertwenzel

Author
Robert Wenzel, Engineer, Potix Corporation
Date
January 2016
Version
ZK 8.0

Introduction

This chapter will show how the ZK features form binding and form validation can be applied to the previously existing wizard (Part 2) with little impact to the overall code. [1]

Add form the binding

In order.zul the wizard content is surrounded by a div element initializing the form binding. This will wrap the original Order object into a form proxy acting as a cache to allow validation before updates are propagated to the original object. The form proxy object is then passed as the order parameter into the wizard, leaving the wizard unaware of the changes.

/wizardexample/src/main/webapp/order.zul [2]
	<wizard wizardModel="@init(vm.wizardModel)" wrapperTemplate="formWrapper">
		<template name="formWrapper">
			<div form="@id('orderForm') @load(vm.order) @save(vm.order, before=vm.wizardModel.nextCommand)
					@validator('formBeanValidator', prefix='p_', groups=wizardModel.currentStep.validationGroups)">
				<sh:apply template="wizardContent" order="@init(orderForm)" savedOrder="@init(vm.order)"/>
			</div>
		</template>
	</wizard>
  • Line 3: initialize the form proxy and define the save command (save after completing a step before switching to the next step)
  • Line 5: passing both the form proxy order, and the real savedOrder (updated after succesful validation) as parameters

Wrap the wizardContainer

To wizard template was slightly changed to allow adding the form binding using an injected wrapperTemplate. This preserves the option to use the wizard without form binding.

/wizardexample/src/main/webapp/WEB-INF/zul/template/wizard/wizard.zul [3]
 	<window border="normal" title="@load(wizardVM.currentStep.title)" ... >
		<sh:apply template="@init(empty wrapperTemplate ? 'defaultWizardContentWrapper' : wrapperTemplate)"/>
  	</window>
  
	<template name="defaultWizardContentWrapper">
		<sh:apply template="wizardContent"/>
	</template>

  	<template name="wizardContent">
	   ...
 	</template>

Enable form binding in the model classes

Since the form binding in ZK 8 is based on a Proxy Object mechanism the Order related classes need some annotations to give the Proxy mechanism the additional information how to create the Proxy and which methods to ignore.

For example calculated (or read only) fields which are derived from other fields should be annotated @Transient. Those getters will not be intercepted by the form proxy keep performing their original calculations whenever executed.

zk.example.order.api.Basket [4]
here totalPrice and totalItems are based on getItems()/getItemPrice()/getQuantity() but not cached by the form proxy
	@Transient
	public BigDecimal getTotalPrice() {
		return this.getItems().stream()
				.map(BasketItem::getItemPrice)
				.reduce(BigDecimal.ZERO, BigDecimal::add);
	}

	@Transient
	public int getTotalItems() {
		return this.getItems().stream()
				.mapToInt(BasketItem::getQuantity)
				.sum();
	}

One limitation of the proxy mechanism is that a class (to be proxied) requires a zero-argument constructor. For this case or if you intend to use a class in an immutable way (and not create a nested form proxy object) you can annotate a field getter with @Immutable. Which means it can still have a setter to replace the whole object, but the object itself is no longer wrapped by a form proxy and changes (if possible) are written directly to the real object.

A good example is the class java.math.BigDecimal which is neither final nor immutable by definition but in many cases treated as immutable.

zk.example.order.api.BasketItem [5]
another example for @Transient and one for @Immutable (means only setting a new unitPrice is handled by the form proxy, while calling unitPrice.setScale(...) is not intercepted at all)
	@Transient
	public BigDecimal getItemPrice() {
		return getUnitPrice().multiply(BigDecimal.valueOf(getQuantity()));
	}
	...
	@Immutable
	public BigDecimal getUnitPrice() {
		return unitPrice;
	}
	public void setUnitPrice(BigDecimal unitPrice) {
		this.unitPrice = unitPrice;
	}

Enable Validation

By itself the form binding is not very useful in this example. It would simply delay saving the input values to the real object until the next button is clicked. In combination with form validation things get more interesting.

	<div form="@id('orderForm') @load(vm.order) @save(vm.order, before=vm.wizardModel.nextCommand)
			@validator('formBeanValidator', prefix='p_', groups=wizardModel.currentStep.validationGroups)">

In this example I use the formBeanValidator, which is a predifined validator leveraging the JSR-303 Bean Validation API.

(It is still possible to implement your own form validator [6].)

/wizardexample/pom.xml [7]
the hibernate-validator dependency provides an implementation of JSR-303, which can be used separately from the hibernate ORM mapping framework
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-validator</artifactId>
		<version>5.2.1.Final</version>
	</dependency>

This API is well documented and can be applied simply using various constraint annotations.

zk.example.order.api.CreditCard [8]
	@NotNull(groups={PaymentGroup.class, Default.class}, message="{field.empty}")
	@Size(min=16, groups={PaymentGroup.class, Default.class}, message="{creditCard.number.size}")
	public String getNumber() {
		return number;
	}
  • Line 1: mandatory field
  • Line 2: validate the credit card length to be exactly 16

Outputting validation messages

Validation messages can be rendered directly in a zul file using the validation messages holder (here called vmsgs).

/wizardexample/src/main/webapp/WEB-INF/zul/order/steps/basket.zul [9]
	<sh:if test="@load(!empty vmsgs['p_basket.items'])">
		<label style="color: red" value="@load(vmsgs['p_basket.items'])"/>
	</sh:if>

For ease of use I also enhanced the formRow template to provide a consistent way of displaying error messages on form fields. /wizardexample/src/main/webapp/WEB-INF/zul/template/wizard/formRow.zul (here the changes to formRow.zul in a diff view)

The the error message is passed into the formRow template by reference as the error parameter.

/wizardexample/src/main/webapp/WEB-INF/zul/order/steps/shippingAddress.zul [10]
			<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.street'))" 
				value="@ref(order.shippingAddress.street)"
				error="@ref(vmsgs['p_shippingAddress.street'])"/> 
			<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.city'))" 
				value="@ref(order.shippingAddress.city)" 
				error="@ref(vmsgs['p_shippingAddress.city'])"/> 
			<formRow type="textbox" label="@init(i18n:nls('order.shippingAddress.zipCode'))" 
				value="@ref(order.shippingAddress.zipCode)"
				error="@ref(vmsgs['p_shippingAddress.zipCode'])"/>

Using Validation Groups

The Bean validation API also supports validation groups, which allow partial validation of the Order object, for a single step. The validation of the final step can be done using the Default group to re-validate the whole object before submitting the order.

Here for the wizard the groups are dynamically applied based on the current step.

	@validator('formBeanValidator', prefix='p_', groups=wizardModel.currentStep.validationGroups)">

Creating a ValidatingWizardStep (extending WizardStep) conveniently holds the additional validation group for each step.

zk.example.order.OrderViewModel [11]
The validation groups are defined when creating a new step.
	private void initWizardModel() {
		List<WizardStep> availableSteps = Arrays.asList(
				wizardStep(BASKET, BasketGroup.class),
				wizardStep(SHIPPING_ADDRESS, ShippingGroup.class),
				wizardStep(PAYMENT, PaymentGroup.class),
				wizardStep(CONFIRMATION, Default.class)
					.withBeforeNextHandler(this::sendOrder)
					.withNextLabel(NlsFunctions.nls("order.confirmation.button.sendNow")),
				...
  • Line 6: the Confirmation step uses the Default group to validate the whole form object again before the final submit

I18N

I18N of validation messages are held in /wizardexample/src/main/resources/ValidationMessages.properties [12]

Summary

Lets add a different Layout in Part 4

Download

Running the Example

Checkout part-3

   git checkout part-3

The example war file can be built with maven:

   mvn clean package

Execute using jetty:

   mvn jetty:run

Then access the overview page http://localhost:8080/wizardexample/order.zul


Comments



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