Create Data Binding Programmatically

From Documentation

Overview

Under MVC approach, getting component's attribute value by calling getter methods causes lots of routine code in a composer. But under MVVM approach, all attributes' values are saved to ViewModel's properties automatically without calling any methods because of data binding. Through using a binder to add data binding for components, we also can enjoy this benefit in a composer. This section introduces basic usages of a binder.

Binder API Usage

There are 3 basic steps to create data bindings:

  1. Initialize the binder
  2. Store data objects as attributes
    To make data be available for EL expressions
  3. Add data bindings


Assume we have a form to fill in personal information.

	<window apply="org.zkoss.reference.developer.mvvm.advance.DynamicBindingComposer" width="600px">
		<grid >
			<rows>
				<row>
					First Name:
					<textbox id="fn"/>
				</row>
				<row>
					Last Name:
					<textbox id="ln"/>
				</row>
				<row>
					Age:
					<intbox/>
				</row>
				<row spans="2">
					<div>
						<button label="Submit" />
						<button label="Reset" />
					</div>
				</row>
				<row spans="2">
					<div>
					Preview: I am <label id="fnLabel" /> <label id="lnLabel" />, <label id="ageLabel"/> years old.
					</div>
				</row>
			</rows>
		</grid>
		...
	</window>

We hope that user input can be automatically saved to a bean.

3 basic steps example

public class DynamicBindingComposer extends SelectorComposer {

	private Binder binder = new DefaultBinder();

	@Wire("grid")
	private Grid grid;
	
	@Wire("#fn") 
	private Textbox firstNameBox;
	...
	private Person person;

	@Override
	public void doAfterCompose(Component comp) throws Exception {
		super.doAfterCompose(comp);
		
		binder.init(comp,this, null);
		
		grid.setAttribute("person", person);
		
		binder.addPropertySaveBindings(firstNameBox, "value", "person.firstName"
				, null, null, null, null, null,null,null);
		binder.addPropertyLoadBindings(firstNameBox, "value", "person.firstName"
				, null, null, null, null, null);
		// add more data bindings...
		
		binder.loadComponent(grid, false); //load beans' data to initialize components
	}
  • Line 3: Create a DefaultBinder to use as its Javadoc suggests.
  • Line 17: We should initialize DefaultBinder before using it. The first parameter is root component. The second parameter is ViewModel object. In this example, the composer plays the role as a ViewModel.
  • Line 19: Set the data bean as an attribute of the Grid, this can make the bean be accessible for EL expression by its key: person.
  • Line 21,23: Add save- or load-binding between the person bean's property person.firstName and the firstNameBox's value attribute. There are many parameters that we don't use in this example, so we pass null. please refer to Javadoc: Binder for more details.
  • Line 27: Load data for all load-bindings inside the Grid which is specified at first parameter. The second parameter indicates loading init-binding or not, as we don't use init-binding, we set it to "false".


The codes to add data binding:

		binder.addPropertySaveBindings(firstNameBox, "value", "person.firstName"
				, null, null, null, null, null,null,null);
		binder.addPropertyLoadBindings(firstNameBox, "value", "person.firstName"
				, null, null, null, null, null);

They equals to the below data binding annotation in a ZUL:

<textbox value="@bind(vm.person.firstName)"/>


After data binding is created, each time when we need to get user input, we can use the data bean directly instead of getting from a specific components as follows:

public class DynamicBindingComposer extends SelectorComposer {

	...

	@Listen("onClick = button[label='Submit']")
	public void submit(){
		Messagebox.show("I am "+person.getFirstName()+" "
			+person.getLastName()+", "+person.getAge()+" years old.");
	}

	...
}


Re-load the Load-Binding

If we change the data bean, we could trigger the binder to reload those load-binding for us. Assume that there is a "Reset" button, click the button can clear all user input. We can implement this function as follows:

public class DynamicBindingComposer extends SelectorComposer {

	...
	@Listen("onClick = button[label='Reset']")
	public void reset(){
		person = new Person();
		grid.setAttribute("person", person);
		binder.loadComponent(grid, false);
//		person.setFirstName("");
//		person.setLastName("");
//		person.setAge(0);
//		binder.notifyChange(person, "*");
	}

	...
}
  • Line 6: We could replace person with new instance to reset it.
  • Line 7: In this case, we should also replace the attribute in Grid.
  • Line 8: Let the binder reload all load-bindings inside the grid which is specified at first parameter.
  • Line 9-12: This is another way to trigger reloading data bindings, change properties and notify which properties you changed by binder.notifyChange(myBean, "propertyName"). In our example, we pass "*" as a property name which means all properties.

Add Data Binding for Collections

If components are dynamically created, we can't just write data binding annotation on a ZUL. In this case, we have to add data binding with a binder.

Assume that we hope end users can edit an item directly in a Listbox. Hence we could put a Textbox in each Listcell and make each Textbox bind to each object of Listbox's model. This data binding cannot be made by writing annotation in a zul because those Textboxs are dynamically created.


Dev-ref-mvvm-adv-data-binding-programmatically.png
Edit in a Listcell


Use ItemRenderer

The renderer class renders the items of a component's data model. We create those components for the Listbox inside a renderer, so we also create data binding in it.


	class MyListboxRenderer implements ListitemRenderer{

		public void render(Listitem listitem, Object data, int index)
				throws Exception {

			//store as an attribute which can be accessed by EL expression
			listitem.setAttribute("bean", data);
			
			//first name
			Listcell fnCell = new Listcell();
			listitem.appendChild(fnCell);
			Textbox fnBox = new Textbox();
			fnBox.setInplace(true);
			fnCell.appendChild(fnBox);
			binder.addPropertyLoadBindings(fnBox, "value", "bean.firstName"
					, null, null, null, null, null);
			binder.addPropertySaveBindings(fnBox, "value", "bean.firstName"
					, null, null, null, null, null, null, null);

			//last name
			...

			//age
			...

			//delete button
			...
		}
		
	}
  • Line 7: Set data bean as an attribute of a Listitem to make it available for EL expression, bean.propertyname.
  • Line 15-16: Add data binding between a Textbox's value attribute and a property bean.firstName.