MVVM in Action

From Documentation
Revision as of 02:22, 19 April 2016 by Tendysu (talk | contribs) (Created page with "{{Template:Smalltalk_Author |author=Olivier Driesbach, Engineer, Cross |date=January 08, 2016 |version=ZK Studio 2.0.1 }} __TOC__ = Introduction = Since version 6 of ZK, devel...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
MVVM in Action

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", "john.doe@unknown.com"));
    		result.add(new Person("Oswald", "Cobblepot", "pinguin@linux.com"));
    		result.add(new Person("Marcus", "Brody", "mbrody@indiana.com"));
    		result.add(new Person("Thomas", "Anderson", "neo@matrix.com"));
    		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