The Name Of My Page"

From Documentation
m
Line 1: Line 1:
{{Template:Smalltalk_Author|
+
{{Template:Smalltalk_Author
|author=Matthieu Duchemin, Engineer, Potix Corporation
+
|author=Olivier Driesbach, Engineer, Cross
|date=January 2016
+
|date=January 08, 2016
|version=ZK 8.0
+
|version=ZK Studio 2.0.1
 
}}
 
}}
  
ZK Framework offer lots of features to simplify the creation of a Java based web application. As some of these features are not always known, we sometime come across suboptimal implementation. Usually, it only take a few simple steps to make them simpler and easier. Those aren't necessarily broken or damaging, but they can have positive effects without much changes or efforts.
+
__TOC__
You will find here a few common mistakes in ZK, and solutions to avoid them.
 
  
=ZK Do's and Don'ts Part 1=
+
= Introduction =
 +
 
 +
Since version 6 of ZK, developers can choose to create web applications using the MVVM design pattern as an alternative of the traditional MVC one. If you're not familiar with MVVM, I recommend reading through the following articles as a good starting point:
 +
 
 +
<ol>
 +
<li>[http://books.zkoss.org/wiki/Small_Talks/2011/December/MVVM_in_ZK6:in_Contrast_to_MVC MVVM in contrast to MVC]</li>
 +
<li>[http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_your_first_MVVM_page Design your first MVVM page]</li>
 +
<li>[http://books.zkoss.org/wiki/Small_Talks/2011/November/MVVM_in_ZK_6_-_Design_CRUD_page_by_MVVM_pattern Design  a CRUD page with MVVM]</li>
 +
</ol>
 +
 
 +
= Purpose of this article =
 +
 
 +
As explained in the ZK Advanced section ([http://books.zkoss.org/zk-mvvm-book/8.0/advanced/wire_components.html Chapter 4.4: Wire Components]) of the online documentation, it is not suggested to wire UI components into your ViewModels because you will loose the main advantage of the using the MVVM design pattern: Separate view's concerns from ViewModel operations.
 +
 
 +
Unfortunately, if you've experienced the development of a real world ZK application, you've probably found how it is that difficult to fully respect this condition. Especially when you need to open/close a new window at the end of an action.
 +
 
 +
This article will show you how it is possible with some java templates using ZK MVVM basic concepts.
 +
 
 +
= Context =
 +
 
 +
The full source code of this article is based on a real world application use case. It has been adapted to its minimalistic version in order to be reusable like a conception pattern. It is available as a download at the end of the article.
 +
 
 +
<ol>
 +
=== <li><b>Use case</b></li> ===
 +
 
 +
<ol>
 +
<li>The user goes to the list's page of persons.</li>
 +
<li>The user selects one person.</li>
 +
<li>The user clicks on the "Address" button to open a new window.</li>
 +
<li>The new window displays the list of adresses available in the system.</li>
 +
<li>The user selects one address in the list and click on the "Select" button.</li>
 +
<li>The selected adress is attached to the selected person of the first window and the address window is closed automatically.</li>
 +
</ol>
  
==Calling components directly when using a model==
 
ZK offers default ListModel<ref>http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVC/Model/List_Model</ref> implementations such as ListModelList. They provide an easy way to separate the view from the model. By providing a template or a renderer to control the appearance, and a model to handle the data.
 
While delegating control to the model, it feel sometime "easier" to act directly on a component. One would use grid.getRows().appendChild(..) to add a new row, or use listbox.setSelectedIndex() to set the selection state of a Listbox.
 
If the components content is directly controlled by the application, this may be correct. However, if the content is controlled by a model, the model itself should perform these actions.
 
 
<br/>
 
<br/>
;Do:
+
===<li><b>Known difficulties</b></li> ===
*Use ListModels instead of basic collections when assigning a model to a component.
+
 
**List – ListModelList <ref>https://www.zkoss.org/javadoc/latest/zk/org/zkoss/zul/ListModelList.html</ref>
+
<ol>
**Set – ListModelSet <ref>https://www.zkoss.org/javadoc/latest/zk/org/zkoss/zul/ListModelSet.html</ref>
+
<li>Create the address button on the person's page without coding the URI of the addresses' page into the button's action of the ViewModel.</li>
**Map - ListModelMap <ref>https://www.zkoss.org/javadoc/latest/zk/org/zkoss/zul/ListModelMap.html</ref>
+
<li>Send the selected address (POJO or identifier) from the address ViewModel to the person ViewModel without having references between each other (separation of concerns).</li>
**Object[] or Enum – ListModelArray <ref>https://www.zkoss.org/javadoc/latest/zk/org/zkoss/zul/ListModelArray.html</ref>
+
<li>Close the addresses' window without coding the UI stuff in the ViewModel.</li>
*Use ListModel methods to apply changes to the model.
+
</ol>
 +
 
 
<br/>
 
<br/>
;Don’t:
+
=== <li><b>Person and Address (Model, View and ViewModel)</b></li> ===
*Use component methods to alter the view when using a model.
+
 
*Mix model and manual entries in the same component.
+
'''The Person.java (Model)'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.person;
 +
 
 +
import demo.mvvm.address.Address;
 +
 
 +
public class Person {
 +
 +
private String firstName;
 +
private String lastName;
 +
private String email;
 +
private Address address;
 +
 +
public Person() {
 +
}
 +
 +
public Person(String firstName, String lastName, String email) {
 +
this.firstName = firstName;
 +
this.lastName = lastName;
 +
this.email = email;
 +
}
 +
 
 +
// Getters and setters ...
 +
}
 +
</source>
 +
 
 +
'''The Address.java (Model)'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.address;
 +
 
 +
public class Address {
 +
 +
private String number;
 +
private String line1;
 +
private String line2;
 +
private String zipCode;
 +
private String city;
 +
 +
public Address() {
 +
}
 +
 +
public Address(String number, String line1, String line2, String zipCode, String city) {
 +
this.number = number;
 +
this.line1 = line1;
 +
this.line2 = line2;
 +
this.zipCode = zipCode;
 +
this.city = city;
 +
}
 +
 
 +
// Getters and setters ...
 +
 +
public String getCompleteAddress() {
 +
return getNumber() +" "+ getLine1() +" "+ getZipCode() +" "+ getCity();
 +
}
 +
}
 +
</source>
 +
 
 +
'''The personList.zul (View)'''
  
==Understanding ZK scopes==
+
<source lang="xml">
Scopes in ZK define the range affected by and action. For example, when creating an event queue, we need to define the scope of listeners affected by its events.
+
<window id="mainPanel">
The basic scopes are DESKTOP, SESSION and APPLICATION. <ref>http://books.zkoss.org/wiki/ZK_Developer's_Reference/Event_Handling/Event_Queues#More_About_Scopes</ref>
+
 
APPLICATION is very straightforward: it will affect every listeners on the whole application.
+
<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
SESSION represent the browser session. Assuming that the client uses cookies, it will match each tab in the browser.  
+
DESKTOP roughly represents a unique browser tab. In specific cases, like Iframe, a tab can contain more than one desktop.
+
<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
When using events, choosing the right scope can lead to increased performances, reduced network activity and reduced memory consumption. It’s also work remembering that using an event queue with any scope except Desktop will automatically activate server push.
+
<listhead>
<br/>
+
    <listheader label="Last name" width="100px" />
;Do:
+
    <listheader label="First name" width="100px" />
*Always use the smallest possible scope.
+
    <listheader label="Email" />
*Be careful when using session scope, as it will affect every desktops in the session.
+
    <listheader label="Address" />
*Be extra careful when using application scope, as it will affect any desktops in the application.
+
</listhead>
<br/>
+
<template name="model">
;Don’t:
+
    <listitem>
*Use a scope broader than desktop unless you are sure that you need it.
+
        <listcell label="@load(each.lastName)" />
*Choose session or application if you do not want server push to be activated.
+
        <listcell label="@load(each.firstName)" />
 +
        <listcell label="@load(each.email)" />
 +
        <listcell label="@load(each.address.completeAddress)" />
 +
    </listitem>
 +
</template>
 +
</listbox>
 +
 +
<button label="Address" disabled="@load(empty personVM.selectedPerson)" />
 +
 +
</vlayout>
 +
 
 +
</window>
 +
</source>
 +
 
 +
'''The addressList.zul (View)'''
 +
 
 +
<source lang="xml">
 +
<window title="Addresses" mode="modal" width="800px" closable="true">
 +
 
 +
<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
 +
 +
<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
 +
<listhead>
 +
    <listheader label="Number" width="100px" />
 +
    <listheader label="Line 1" />
 +
    <listheader label="Line 2" />
 +
    <listheader label="Zip Code" width="100px" />
 +
<listheader label="City" width="100px" />
 +
</listhead>
 +
    <template name="model">
 +
        <listitem>
 +
            <listcell label="@load(each.number)" />
 +
            <listcell label="@load(each.line1)" />
 +
            <listcell label="@load(each.line2)" />
 +
            <listcell label="@load(each.zipCode)" />
 +
            <listcell label="@load(each.city)"  />
 +
        </listitem>
 +
    </template>
 +
</listbox>
 +
 +
<button label="Validate" disabled="@load(empty addressVM.selectedAddress)" />
 +
 +
</vlayout>
 +
 +
</window>
 +
</source>
 +
 
 +
'''The PersonViewModel.java'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.person;
 +
 
 +
import java.util.List;
 +
 
 +
import org.zkoss.bind.annotation.Init;
 +
import org.zkoss.bind.annotation.NotifyChange;
 +
import org.zkoss.zk.ui.event.Event;
 +
import org.zkoss.zk.ui.event.EventListener;
 +
 
 +
import demo.mvvm.address.Address;
 +
 
 +
public class PersonViewModel {
 +
 +
protected List<Person> persons;
 +
private Person selectedPerson;
 +
 +
@Init(superclass=true)
 +
public void initPersonViewModel() {
 +
initPersons();
 +
}
 +
 +
public List<Person> getPersons() {
 +
return persons;
 +
}
 +
 +
public Person getSelectedPerson() {
 +
return selectedPerson;
 +
}
 +
 +
public void setSelectedPerson(Person selectedPerson) {
 +
this.selectedPerson = selectedPerson;
 +
}
 +
 +
protected void initPersons() {
 +
persons = new PersonService().findAll();
 +
}
 +
}
 +
</source>
 +
 
 +
'''The AddressViewModel.java'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.address;
 +
 
 +
import java.util.List;
 +
 
 +
import org.zkoss.bind.annotation.Command;
 +
import org.zkoss.bind.annotation.Init;
 +
 
 +
public class AddressViewModel {
 +
 
 +
    private List<Address> addresses;
 +
    private Address selectedAddress;
 +
 
 +
    @Init(superclass=true)
 +
    public void initAddressViewModel() {
 +
    initAddresses();
 +
    }
 +
   
 +
    public List<Address> getAddresses() {
 +
    return addresses;
 +
    }
 +
 +
    public Address getSelectedAddress() {
 +
        return selectedAddress;
 +
    }
 +
 +
    public void setSelectedAddress(Address selectedAddress) {
 +
        this.selectedAddress = selectedAddress;
 +
    }
 +
   
 +
    private void initAddresses() {
 +
        addresses = new AddressService().findAll();
 +
    }
 +
}
 +
</source>
 +
 
 +
'''The PersonService.java'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.person;
 +
 
 +
import java.util.ArrayList;
 +
import java.util.List;
 +
 
 +
import demo.mvvm.Person;
 +
 
 +
public class PersonService {
 +
 
 +
public PersonService() {}
 +
 
 +
public List<Person> findAll() {
 +
 +
ArrayList<Person> result = new ArrayList<Person>();
 +
result.add(new Person("John", "Doe", "[email protected]"));
 +
result.add(new Person("Oswald", "Cobblepot", "[email protected]"));
 +
result.add(new Person("Marcus", "Brody", "[email protected]"));
 +
result.add(new Person("Thomas", "Anderson", "[email protected]"));
 +
return result;
 +
}
 +
}
 +
</source>
 +
 
 +
'''And the AddressService.java'''
 +
 
 +
<source lang="java">
 +
package demo.mvvm.address;
 +
 
 +
import java.util.ArrayList;
 +
import java.util.List;
 +
 
 +
import demo.mvvm.Address;
 +
 
 +
public class AddressService {
 +
 
 +
public AddressService() {
 +
}
 +
 +
public List<Address> findAll() {
 +
 +
ArrayList<Address> result = new ArrayList<Address>();
 +
 +
result.add(new Address("21", "Jump Street", "", "10001", "New York City"));
 +
result.add(new Address("23", "Sec. 1", "Changan E. Rd #7F-2", "10441", "Taipei City"));
 +
result.add(new Address("36", "Quai des Orfèvres", "", "75000", "Paris"));
 +
result.add(new Address("1600", "Pennsylvania Avenue NW", "", "DC 20500", "Washington"));
 +
 +
return result;
 +
}
 +
}
 +
</source>
 +
 
 +
</ol>
 +
 
 +
= Step by step implementation =
 +
 
 +
<ol>
 +
=== <li>Create dynamic interactions with the right conditions</li> ===
 +
 
 +
When you design the ZUML structure of your "single one page" application, you need to take care of the design of your ZUL pages if you don't want to have huge files with many components and ViewModel instances loaded in-memory when the end-user interface is not displaying them.  
 +
 
 +
Ideally, in our case, if the end user doesn't click on the "Address" button, we don't want to have the "addressList.zul" file integrated in the ZUML structure because the ViewModel will be created too.
 +
 
 +
A bad integration would look like
 +
 
 +
<source lang="xml">
 +
<window id="mainPanel">
 +
 
 +
<div visible="@load(not empty personVM.selectedPerson)">
 +
    <include src="otherPage.zul" />
 +
</div>
 +
 +
</window>
 +
</source>
 +
 
 +
Depending on the root component of the otherPage.zul (a modal window for example), this code will not work and you would have to put the visible attribute condition on the window element.
 +
 
 +
To prevent creating these kinds of visibility dependencies between components, you need to use the include tag.
 +
In the CE and EE version of ZK, you can use the apply alternative which is very similar.
 +
 
 +
<source lang="xml" high="9,11">
 +
<window id="mainPanel">
 +
 
 +
<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
 +
 +
<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
 +
...
 +
</listbox>
 +
 +
<button label="Address" disabled="@load(empty personVM.selectedPerson)" onClick="@command('selectAddress')" />
 +
 +
<include src="@load(personVM.selectAddress ? 'addressList.zul' : '')" />
 +
 +
</vlayout>
 +
 
 +
</window>
 +
</source>
 +
 
 +
Now, the "selectAddress" command, will enable/disable a boolean property which is binded in the src attribute of the include tag. The tip is using the empty value to attach/detach the page to include only when necessary. Therefore, the included ViewModel will be instanciated dynamically.
 +
 
 +
<source lang="java" high="17,35,36,37,38,39">
 +
package demo.mvvm.person;
 +
 
 +
import java.util.List;
 +
 
 +
import org.zkoss.bind.annotation.BindingParam;
 +
import org.zkoss.bind.annotation.Command;
 +
import org.zkoss.bind.annotation.GlobalCommand;
 +
import org.zkoss.bind.annotation.Init;
 +
import org.zkoss.bind.annotation.NotifyChange;
 +
 
 +
import demo.mvvm.address.Address;
 +
 
 +
public class PersonViewModel {
 +
 +
protected List<Person> persons;
 +
private Person selectedPerson;
 +
private boolean selectAddress;
 +
 +
@Init(superclass=true)
 +
public void initPersonViewModel() {
 +
initPersons();
 +
selectAddress = false;
 +
}
 +
 +
// Getters and setters ...
 +
 +
public boolean isSelectAddress() {
 +
return selectAddress;
 +
}
 +
 
 +
public void setSelectAddress(boolean selectAddress) {
 +
this.selectAddress = selectAddress;
 +
}
 +
 +
@Command
 +
@NotifyChange("selectAddress")
 +
public void selectAddress() {
 +
selectAddress = true; // Activate the address selection mode
 +
}
 +
 
 +
protected void initPersons() {
 +
persons = new PersonService().findAll();
 +
}
 +
}
 +
</source>
 +
 
 +
=== <li>Communication between ViewModels</li> ===
 +
 
 +
The recommended way to establish communication between ViewModels is to use the @GlobalCommand mecanism. Regarding our use case, we need to implement the closing of the address window when the user click on the validate button and the default close button of the window component.
 +
 
 +
'''Update the Address button'''
 +
 
 +
<source lang="xml" high="1,10">
 +
<window title="Addresses" mode="modal" width="800px" closable="true" onClose="@global-command('cancelAddress')">
 +
 
 +
<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
 +
 +
<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
 +
...
 +
</listbox>
 +
 +
<button label="Validate" disabled="@load(empty addressVM.selectedAddress)"
 +
onClick="@global-command('updateAddress', address=addressVM.selectedAddress)" />
 +
 +
</vlayout>
 +
</window>
 +
</source>
 +
 
 +
'''Below the PersonViewModel updated accordingly'''
 +
 
 +
<source lang="java" high="33,34,35,36,37,38,40,41,42,43,44">
 +
package demo.mvvm.person;
 +
 
 +
import java.util.List;
 +
 
 +
import org.zkoss.bind.annotation.BindingParam;
 +
import org.zkoss.bind.annotation.Command;
 +
import org.zkoss.bind.annotation.GlobalCommand;
 +
import org.zkoss.bind.annotation.Init;
 +
import org.zkoss.bind.annotation.NotifyChange;
 +
 
 +
import demo.mvvm.address.Address;
  
==Component belong to another desktop==
+
public class PersonViewModel {
Saving a reference to a component can be a powerful way to keep track of changes, but there is one pitfall to avoid. ZK components are bound to a desktop, and cannot be passed around to another desktop or session.
+
Choosing where to save or send the component reference is critical, as accessing it outside of its original desktop would cause exceptions in the best case, and corrupted views in the worst. To avoid this, always use desktop scope when passing around components, and only send values when serving a whole session or application.
+
protected List<Person> persons;
<br/>
+
private Person selectedPerson;
;Do:
+
private boolean selectAddress;
*Keep references of business objects, values between desktops if needed.
+
*Use event queues for live data. Use session or application parameters for static data.
+
@Init(superclass=true)
<br/>
+
public void initPersonViewModel() {
;Don’t:
+
initPersons();
*Keep a reference to a view object such as a component as a session or application parameter.
+
selectAddress = false;
*Send a reference to a view object in an event queue with a scope is broader than desktop.
+
}
 +
 +
// Getters and setters ...
 +
 +
@Command
 +
@NotifyChange("selectAddress")
 +
public void selectAddress() {
 +
selectAddress = true; // Activate the address selection mode
 +
}
 +
 +
@GlobalCommand
 +
@NotifyChange({"selectedPerson", "selectAddress"})
 +
public void updateAddress(@BindingParam("address") Address address) {
 +
getSelectedPerson().setAddress(address);
 +
selectAddress = false; // Disable the address selection mode
 +
}
 +
 +
@GlobalCommand
 +
@NotifyChange("selectAddress")
 +
public void cancelAddress() {
 +
selectAddress = false; // Disable the address selection mode
 +
}
  
==Choosing @init, @load, @save, or @bind==
+
protected void initPersons() {
Using the MVVM data-binding annotations to wire a view model and a view, you may use these four annotations.
+
persons = new PersonService().findAll();
*@init will load the value in the view once, but will not update it.<ref>http://books.zkoss.org/zk-mvvm-book/8.0/syntax/databindinginit.html</ref>
+
}
*@load will load the value in the view, then updated it when the view model notify a change.<ref>http://books.zkoss.org/zk-mvvm-book/8.0/syntax/load.html</ref>
+
}
*@save will not load anything in the view, but will save the values from the view into the view model when updated.<ref>http://books.zkoss.org/zk-mvvm-book/8.0/syntax/save.html</ref>
+
</source>
*@bind is basically @save plus @load. Values will be updated from and to the view model when necessary.<ref>http://books.zkoss.org/zk-mvvm-book/8.0/syntax/bind.html</ref>
 
By expensiveness, they are ranked as follow: @init < @load | @save < @bind.
 
<br/>
 
;Do:
 
*Choose the least expensive option for each binding.
 
<br/>
 
;Don’t:
 
*Bind a value using @save or @bind on components whose value cannot be set by the user, such as label, etc.
 
*Bind a value using @load or @bind on components whose value is never set on server side.
 
*Use @load when @init is sufficient.
 
  
==Notifying the whole ListModel on change==
+
</ol>
Speaking of models and data-binding, ListModels will automatically update the view after many actions, such as adding or deleting an element. This makes adding new entries in a complex control such as a grid very easy.
 
However, it’s not always best to refresh the whole model after making a small update on one of its items. ListModels can actually update individual items, which can save a lot of resources.
 
<br/>
 
;Do:
 
*Bind a ListModel to a component using @init, unless you need to replace the whole model by a new object.
 
*Use the ListModelList.notifyChange(…) method to update a single entry, or set the entry again to trigger a refresh instead of notifying the whole model.
 
<br/>
 
;Don’t:
 
*Use @NotifyChange(“*”),  or BindUtils.postNotifyChange(…, …, …, "*"); unless there is no better option.
 
*Use @NotifyChange(“myListModel”) or BindUtils.postNotifyChange(…, …, …, "myListModel"); to update a single row.
 
  
= References =  
+
= Source code =
<references/>
 
  
{{Template:CommentedSmalltalk_Footer_new|
+
Click here for the full [https://github.com/Cyrus964/ZKDemoMVVM/archive/master.zip source code]
|name=Potix Corporation
 
}}
 

Revision as of 02:21, 19 April 2016

The Name Of My Page

Author
Olivier Driesbach, Engineer, Cross
Date
January 08, 2016
Version
ZK Studio 2.0.1

Introduction

Since version 6 of ZK, developers can choose to create web applications using the MVVM design pattern as an alternative of the traditional MVC one. If you're not familiar with MVVM, I recommend reading through the following articles as a good starting point:

  1. MVVM in contrast to MVC
  2. Design your first MVVM page
  3. Design a CRUD page with MVVM

Purpose of this article

As explained in the ZK Advanced section (Chapter 4.4: Wire Components) of the online documentation, it is not suggested to wire UI components into your ViewModels because you will loose the main advantage of the using the MVVM design pattern: Separate view's concerns from ViewModel operations.

Unfortunately, if you've experienced the development of a real world ZK application, you've probably found how it is that difficult to fully respect this condition. Especially when you need to open/close a new window at the end of an action.

This article will show you how it is possible with some java templates using ZK MVVM basic concepts.

Context

The full source code of this article is based on a real world application use case. It has been adapted to its minimalistic version in order to be reusable like a conception pattern. It is available as a download at the end of the article.

  1. Use case
    1. The user goes to the list's page of persons.
    2. The user selects one person.
    3. The user clicks on the "Address" button to open a new window.
    4. The new window displays the list of adresses available in the system.
    5. The user selects one address in the list and click on the "Select" button.
    6. The selected adress is attached to the selected person of the first window and the address window is closed automatically.


  2. Known difficulties
    1. Create the address button on the person's page without coding the URI of the addresses' page into the button's action of the ViewModel.
    2. Send the selected address (POJO or identifier) from the address ViewModel to the person ViewModel without having references between each other (separation of concerns).
    3. Close the addresses' window without coding the UI stuff in the ViewModel.


  3. Person and Address (Model, View and ViewModel)
  4. The Person.java (Model)

    package demo.mvvm.person;
    
    import demo.mvvm.address.Address;
    
    public class Person {
    	
    	private String firstName;
    	private String lastName;
    	private String email;
    	private Address address;
    	
    	public Person() {
    	}
    	
    	public Person(String firstName, String lastName, String email) {
    		this.firstName = firstName;
    		this.lastName = lastName;
    		this.email = email;
    	}
    
    	// Getters and setters ...
    }
    

    The Address.java (Model)

    package demo.mvvm.address;
    
    public class Address {
    	
    	private String number;
    	private String line1;
    	private String line2;
    	private String zipCode;
    	private String city;
    	
    	public Address() {
    	}
    	
    	public Address(String number, String line1, String line2, String zipCode, String city) {
    		this.number = number;
    		this.line1 = line1;
    		this.line2 = line2;
    		this.zipCode = zipCode;
    		this.city = city;
    	}
    
    	// Getters and setters ...
    	
    	public String getCompleteAddress() {
    		return getNumber() +" "+ getLine1() +" "+ getZipCode() +" "+ getCity();
    	}
    }
    

    The personList.zul (View)

    <window id="mainPanel">
    
    	<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
    	
    		<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
    			<listhead>
    			    <listheader label="Last name" width="100px" />
    			    <listheader label="First name" width="100px" />
    			    <listheader label="Email" />
    			    <listheader label="Address" />
    			</listhead>
    			<template name="model">
    			    <listitem>
    			        <listcell label="@load(each.lastName)" />
    			        <listcell label="@load(each.firstName)" />
    			        <listcell label="@load(each.email)" />
    			        <listcell label="@load(each.address.completeAddress)" />
    			    </listitem>
    			</template>
    		</listbox>
    		
    		<button label="Address" disabled="@load(empty personVM.selectedPerson)" />
    		
    	</vlayout>
    
    </window>
    

    The addressList.zul (View)

    <window title="Addresses" mode="modal" width="800px" closable="true">
    
    	<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
    	
    		<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
    			<listhead>
    			    <listheader label="Number" width="100px" />
    			    <listheader label="Line 1" />
    			    <listheader label="Line 2" />
    			    <listheader label="Zip Code" width="100px" />
    				<listheader label="City" width="100px" />
    			</listhead>
    		    <template name="model">
    		        <listitem>
    		            <listcell label="@load(each.number)" />
    		            <listcell label="@load(each.line1)" />
    		            <listcell label="@load(each.line2)" />
    		            <listcell label="@load(each.zipCode)" />
    		            <listcell label="@load(each.city)"  />
    		        </listitem>
    		    </template>
    		</listbox>
    		
    		<button label="Validate" disabled="@load(empty addressVM.selectedAddress)" />
    	
    	</vlayout>
     
    </window>
    

    The PersonViewModel.java

    package demo.mvvm.person;
    
    import java.util.List;
    
    import org.zkoss.bind.annotation.Init;
    import org.zkoss.bind.annotation.NotifyChange;
    import org.zkoss.zk.ui.event.Event;
    import org.zkoss.zk.ui.event.EventListener;
    
    import demo.mvvm.address.Address;
    
    public class PersonViewModel {
    	
    	protected List<Person> persons;
    	private Person selectedPerson;
    	
    	@Init(superclass=true)
    	public void initPersonViewModel() {
    		initPersons();
    	}
    	
    	public List<Person> getPersons() {
    		return persons;
    	}
    	
    	public Person getSelectedPerson() {
    		return selectedPerson;
    	}
    	
    	public void setSelectedPerson(Person selectedPerson) {
    		this.selectedPerson = selectedPerson;
    	}
    	
    	protected void initPersons() {
    		persons = new PersonService().findAll();
    	}
    }
    

    The AddressViewModel.java

    package demo.mvvm.address;
    
    import java.util.List;
    
    import org.zkoss.bind.annotation.Command;
    import org.zkoss.bind.annotation.Init;
    
    public class AddressViewModel {
    
        private List<Address> addresses;
        private Address selectedAddress;
    
        @Init(superclass=true)
        public void initAddressViewModel() {
        	initAddresses();
        }
        
        public List<Address> getAddresses() {
        	return addresses;
        }
     
        public Address getSelectedAddress() {
            return selectedAddress;
        }
    	
        public void setSelectedAddress(Address selectedAddress) {
            this.selectedAddress = selectedAddress;
        }
        
        private void initAddresses() {
            addresses = new AddressService().findAll();
        }
    }
    

    The PersonService.java

    package demo.mvvm.person;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import demo.mvvm.Person;
    
    public class PersonService {
    
    	public PersonService() {}
    
    	public List<Person> findAll() {
    		
    		ArrayList<Person> result = new ArrayList<Person>();
    		result.add(new Person("John", "Doe", "[email protected]"));
    		result.add(new Person("Oswald", "Cobblepot", "[email protected]"));
    		result.add(new Person("Marcus", "Brody", "[email protected]"));
    		result.add(new Person("Thomas", "Anderson", "[email protected]"));
    		return result;
    	}
    }
    

    And the AddressService.java

    package demo.mvvm.address;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import demo.mvvm.Address;
    
    public class AddressService {
    
    	public AddressService() {
    	}
    		
    	public List<Address> findAll() {
    		
    		ArrayList<Address> result = new ArrayList<Address>();
    		
    		result.add(new Address("21", "Jump Street", "", "10001", "New York City"));
    		result.add(new Address("23", "Sec. 1", "Changan E. Rd #7F-2", "10441", "Taipei City"));
    		result.add(new Address("36", "Quai des Orfèvres", "", "75000", "Paris"));
    		result.add(new Address("1600", "Pennsylvania Avenue NW", "", "DC 20500", "Washington"));
    		
    		return result;
    	}
    }
    

Step by step implementation

  1. Create dynamic interactions with the right conditions
  2. When you design the ZUML structure of your "single one page" application, you need to take care of the design of your ZUL pages if you don't want to have huge files with many components and ViewModel instances loaded in-memory when the end-user interface is not displaying them.

    Ideally, in our case, if the end user doesn't click on the "Address" button, we don't want to have the "addressList.zul" file integrated in the ZUML structure because the ViewModel will be created too.

    A bad integration would look like

    <window id="mainPanel">
    
    	<div visible="@load(not empty personVM.selectedPerson)">
    	    <include src="otherPage.zul" />
    	</div>
    	
    </window>
    

    Depending on the root component of the otherPage.zul (a modal window for example), this code will not work and you would have to put the visible attribute condition on the window element.

    To prevent creating these kinds of visibility dependencies between components, you need to use the include tag. In the CE and EE version of ZK, you can use the apply alternative which is very similar.

    <window id="mainPanel">
    
    	<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
    	
    		<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
    			...
    		</listbox>
    		
    		<button label="Address" disabled="@load(empty personVM.selectedPerson)" onClick="@command('selectAddress')" />
    		
    		<include src="@load(personVM.selectAddress ? 'addressList.zul' : '')" />
    		
    	</vlayout>
    
    </window>
    

    Now, the "selectAddress" command, will enable/disable a boolean property which is binded in the src attribute of the include tag. The tip is using the empty value to attach/detach the page to include only when necessary. Therefore, the included ViewModel will be instanciated dynamically.

    package demo.mvvm.person;
    
    import java.util.List;
    
    import org.zkoss.bind.annotation.BindingParam;
    import org.zkoss.bind.annotation.Command;
    import org.zkoss.bind.annotation.GlobalCommand;
    import org.zkoss.bind.annotation.Init;
    import org.zkoss.bind.annotation.NotifyChange;
    
    import demo.mvvm.address.Address;
    
    public class PersonViewModel {
    	
    	protected List<Person> persons;
    	private Person selectedPerson;
    	private boolean selectAddress;
    	
    	@Init(superclass=true)
    	public void initPersonViewModel() {
    		initPersons();
    		selectAddress = false;
    	}
    	
    	// Getters and setters ...
    	
    	public boolean isSelectAddress() {
    		return selectAddress;
    	}
    
    	public void setSelectAddress(boolean selectAddress) {
    		this.selectAddress = selectAddress;
    	}
    	
    	@Command
    	@NotifyChange("selectAddress")
    	public void selectAddress() {
    		selectAddress = true; // Activate the address selection mode
    	}
    
    	protected void initPersons() {
    		persons = new PersonService().findAll();
    	}
    }
    

  3. Communication between ViewModels
  4. The recommended way to establish communication between ViewModels is to use the @GlobalCommand mecanism. Regarding our use case, we need to implement the closing of the address window when the user click on the validate button and the default close button of the window component.

    Update the Address button

    <window title="Addresses" mode="modal" width="800px" closable="true" onClose="@global-command('cancelAddress')">
    
    	<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
    	
    		<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
    			...
    		</listbox>
    		
    		<button label="Validate" disabled="@load(empty addressVM.selectedAddress)"
    			onClick="@global-command('updateAddress', address=addressVM.selectedAddress)" />
    	
    	</vlayout>
    </window>
    

    Below the PersonViewModel updated accordingly

    package demo.mvvm.person;
    
    import java.util.List;
    
    import org.zkoss.bind.annotation.BindingParam;
    import org.zkoss.bind.annotation.Command;
    import org.zkoss.bind.annotation.GlobalCommand;
    import org.zkoss.bind.annotation.Init;
    import org.zkoss.bind.annotation.NotifyChange;
    
    import demo.mvvm.address.Address;
    
    public class PersonViewModel {
    	
    	protected List<Person> persons;
    	private Person selectedPerson;
    	private boolean selectAddress;
    	
    	@Init(superclass=true)
    	public void initPersonViewModel() {
    		initPersons();
    		selectAddress = false;
    	}
    	
    	// Getters and setters ...
    	
    	@Command
    	@NotifyChange("selectAddress")
    	public void selectAddress() {
    		selectAddress = true; // Activate the address selection mode
    	}
    	
    	@GlobalCommand
    	@NotifyChange({"selectedPerson", "selectAddress"})
    	public void updateAddress(@BindingParam("address") Address address) {
    		getSelectedPerson().setAddress(address);
    		selectAddress = false; // Disable the address selection mode
    	}
    	
    	@GlobalCommand
    	@NotifyChange("selectAddress")
    	public void cancelAddress() {
    		selectAddress = false; // Disable the address selection mode
    	}
    
    	protected void initPersons() {
    		persons = new PersonService().findAll();
    	}
    }
    

Source code

Click here for the full source code