Create Data Binding Programmatically

From Documentation
< User:Hawk
Revision as of 04:58, 1 April 2022 by Hawk (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Icon info.png Note: The content of this page has been deprecated/removed in the latest version.

Overview

Under the MVC approach, getting component's attribute value by getter method requires developers to write a lot of routine codes in a composer. However, 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 how to obtain and utilise this benefit with basic usages of a binder.

Binder API Usage

There are 3 basic steps involved to create data binding:

  1. Initialize the binder
  2. Store data objects as attributes
    To make data objects 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 instead of calling getter manually. We can bind each input component to a bean's property in a composer.

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
	}
  • Step 1:
    • Line 3: Create a DefaultBinder to use as its Javadoc suggests.
    • Line 17: We should initialize DefaultBinder before using it. The first parameter is a root component. The second parameter is ViewModel object. In this example, the composer plays the role as a ViewModel.
  • Step 2:
    • 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.
  • Step 3:
    • 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: When we use load-binding or init-binding, we need to call loadComponent() to load data for all bindings inside a component which is specified at the first parameter. In our example, all binding inside Grid and its children components will be loaded. The second parameter indicates loading init-binding or not, as we don't use init-binding, we set it to "false".


After these data bindings are 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 {
	private Person person;
	...

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

	...
}


Re-load Data After Change

If we change the data bean, we could trigger the binder to reload the data to components with load-binding created for us. Assume that there is a "Reset" button, by clicking the button you can clear all user input. We can implement this function like the following:

public class DynamicBindingComposer extends SelectorComposer {

	private Binder binder = new DefaultBinder();
	
	@Wire("grid")
	private Grid grid;
	...
	private Person person;

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

	public void resetAlternative(){
		person.setFirstName("");
		person.setLastName("");
		person.setAge(0);
		binder.notifyChange(person, "*");
	}

	...
}
  • Line 12: We could replace person with a new instance to reset it.
  • Line 13: In this case, we should also replace the attribute in Grid.
  • Line 14: Let the binder reload the load-bindings inside the grid which is specified at first parameter.
  • Line 17: This is another way to trigger reload of 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 want end users to be able to edit an item directly in a Listbox, 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 Textbox are dynamically created.


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


Data Binding in ItemRenderer

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

Create data bindings in a renderer

public class DynamicCollectionBindingComposer extends SelectorComposer {

	private Binder binder = new DefaultBinder();

	@Wire("listbox")
	private Listbox listbox;
	private ListModelList<Person> personList;
	...
	@Override
	public void doAfterCompose(Component comp) throws Exception {
		super.doAfterCompose(comp);
		
		//initialize binder
		...
		//set data bean as an attribute
		...
		//add data binding
		...
		listbox.setModel(personList);
		listbox.setItemRenderer(new MyListboxRenderer());
		
		//load beans' data to initialize components
	}
	...

	class MyListboxRenderer implements ListitemRenderer{

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

			//store the data bean 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 20: Use our custom renderer in the Listbox.
  • Line 33: Set data bean as an attribute of a Listitem to make it available for EL expression, bean.propertyname.
  • Line 40,42: Add a data binding between a Textbox's value attribute and a property bean.firstName.
  • Other data bindings for "last name" and "age" are created in a similar way to "first name".

After completing these data bindings, when users edit in a Textbox inside the Listbox, the data is automatically saved to the binding bean of the model.