ZK8 Wizard Example - Part 3

From Documentation
Revision as of 04:22, 20 January 2022 by Hawk (talk | contribs) (correct highlight (via JWB))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
DocumentationSmall Talks2016FebruaryZK8 Wizard Example - Part 3
ZK8 Wizard Example - Part 3

Author
Robert Wenzel, Engineer, Potix Corporation
Date
February 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]

The video above shows the resulting wizard including the validation.

Add the Form Binding

In order.zul the wizard content is surrounded by a div element initializing the form binding (same syntax as in ZK 6.5 or 7). This will wrap the original Order object into a Form Proxy (new in ZK 8) 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 inner wizard and it's pages unaware of the added Form Binding.

/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

Also the 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 - by using the "defaultWizardContentWrapper".

/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 and 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 objects) 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 none of its original methods are proxied.

A good example is the class java.math.BigDecimal which is neither final nor immutable by definition but in many cases it is useful to treat it like 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 postpone saving the input values to the real object until the "next"-button is clicked.

The combination with Form Validation makes things a little more interesting. In this example I use the formBeanValidator, which is a predefined validator leveraging the JSR-303 Bean Validation API.

(It is still possible to implement your own form validator(s) [6] without additional 3rd party libraries.)

	<div form="@id('orderForm') @load(vm.order) @save(vm.order, before=vm.wizardModel.nextCommand)
			@validator('formBeanValidator', prefix='p_', groups=wizardModel.currentStep.validationGroups)">
  • prefix='p_' : defines the prefix for the resulting validation messages
  • groups=wizardModel.currentStep.validationGroups: defines the varying validation groups to be checked for each step

To provide an implementation of the JSR-303 Bean Validation I chose the "hibernate-validator" dependency, which can be used separately from the hibernate ORM mapping framework.

/wizardexample/pom.xml [7]
add the dependency
	<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 using various constraint annotations.

Below the CreditCard class which adds several constraints to its fields. Here I show the validation constraints for the number field.

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: @NotNull meaning it may not be empty - making it a mandatory field
  • Line 2: @Size defining the required length of 16 characters

The annotation properties "groups" and "message" are explained further in the following sections.

Output 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>
  • Line 2: uses the prefix 'p_' as specified above - that's how the validation message for the validated property "basket.items" can be retrieved using vmsgs['p_basket.items']

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 of the wizard model.

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

Finally, 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 (BasketGroup, ShippingGroup, PaymentGroup)
	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

As shown above conditional validation can be applied to the whole wizard while leaving major parts of the code untouched. The shadow elements and templates ensure a consistent look and feel throughout the different pages, when displaying validation messages.

The Form Proxy mechanism preserves the object type of the form object making it transparently usable in each Step while only the surrounding Wizard knows about the validation logic.

Now as things are working nicely and user input is validated in a flexible / pluggable manner. Lets concentrate on the nice things and add different (responsive) 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.