Create Data Binding Programmatically"

From Documentation
 
(39 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
 +
{{Deprecated Content}}
 +
 
= Overview =
 
= 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.
+
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=
 
= Binder API Usage=
  
There are 3 basic steps to create data bindings:
+
There are 3 basic steps involved to create data binding:
  
 
# Initialize the binder
 
# Initialize the binder
 
# Store data objects as attributes
 
# Store data objects as attributes
#: To make data be available for EL expressions
+
#: To make data objects available for EL expressions
 
# Add data bindings
 
# Add data bindings
  
  
 +
Assume we have a form to fill in personal information.
  
''' Basic steps example'''
+
<source lang="xml" high='6,10, 14'>
<source lang="java" high='3,10,12,14,15, 18'>
+
<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>
 +
</source>
 +
 
 +
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'''
 +
<source lang="java" high='3,17,19,21,23, 27'>
 
public class DynamicBindingComposer extends SelectorComposer {
 
public class DynamicBindingComposer extends SelectorComposer {
  
 
private Binder binder = new DefaultBinder();
 
private Binder binder = new DefaultBinder();
 +
 +
@Wire("grid")
 +
private Grid grid;
 +
 +
@Wire("#fn")
 +
private Textbox firstNameBox;
 
...
 
...
 +
private Person person;
  
 
@Override
 
@Override
Line 24: Line 70:
 
super.doAfterCompose(comp);
 
super.doAfterCompose(comp);
 
 
binder.init(grid,this, null);
+
binder.init(comp,this, null);
 
 
comp.setAttribute("person", person);
+
grid.setAttribute("person", person);
 
 
binder.addPropertySaveBindings(firstNameBox, "value", "person.firstName", null, null, null, null, null,null,null);
+
binder.addPropertySaveBindings(firstNameBox, "value", "person.firstName"
binder.addPropertyLoadBindings(firstNameBox, "value", "person.firstName", null, null, null, null, null);
+
, null, null, null, null, null,null,null);
 +
binder.addPropertyLoadBindings(firstNameBox, "value", "person.firstName"
 +
, null, null, null, null, null);
 
// add more data bindings...
 
// add more data bindings...
 
 
binder.loadComponent(grid, true); //optionally, call to load beans' data for the first time
+
binder.loadComponent(grid, false); //load beans' data to initialize components
 
}
 
}
 
</source>
 
</source>
* Line 3:  
+
* Step 1:
* Line 10:
+
** Line 3: Create a <javadoc>org.zkoss.bind.DefaultBinder</javadoc> to use as its Javadoc suggests.
* Line 12:
+
** Line 17: We should initialize <tt>DefaultBinder</tt> 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.
* Line 14:
+
* Step 2:
* Line 15:
+
** 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: <tt>person</tt>.
* Line 18:
+
* Step 3:
 +
** Line 21,23: Add save- or load-binding between the person bean's property <tt>person.firstName</tt> and the <tt>firstNameBox</tt>'s <tt>value</tt> attribute. There are many parameters that we don't use in this example, so we pass <tt>null</tt>.  please refer to Javadoc: <javadoc>org.zkoss.bind.Binder</javadoc> for more details.
 +
* Line 27: When we use load-binding or init-binding, we need to call <tt>loadComponent()</tt> 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".
  
  
Here we give some common examples, for more details please refer to Javadoc: <javadoc>org.zkoss.bind.Binder</javadoc>.
+
<!--
 
+
The codes to add data binding:
 +
<source lang="java">
 +
binder.addPropertySaveBindings(firstNameBox, "value", "person.firstName"
 +
, null, null, null, null, null,null,null);
 +
binder.addPropertyLoadBindings(firstNameBox, "value", "person.firstName"
 +
, null, null, null, null, null);
 +
</source>
  
'''Property binding in a ZUL'''
+
They equal to the below data binding annotation in a ZUL:
 
<source lang="xml">
 
<source lang="xml">
 
<textbox value="@bind(vm.person.firstName)"/>
 
<textbox value="@bind(vm.person.firstName)"/>
 
</source>
 
</source>
 +
-->
 +
 +
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:
 +
 +
<source lang="java" high='7'>
 +
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.");
 +
}
  
'''Programmatic property binding'''
+
...
<source lang="java">
+
}
binder.addPropertyLoadBindings(firstNameBox, "value", "vm.person.firstName", null, null, null, null, null);
 
binder.addPropertySaveBindings(firstNameBox,"value","vm.person.firstName", null, null, null, null, null,null,null);
 
 
</source>
 
</source>
  
  
 +
== Re-load Data After Change ==
  
<source lang="java">
+
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:
String[] command = {"submit"};
 
binder.addPropertyLoadBindings(firstNameLabel, "value", "vm.person.firstName", null, command, null, null, null);
 
</source>
 
  
 +
<source lang="java" high="12,13,14, 17">
 +
public class DynamicBindingComposer extends SelectorComposer {
  
== Form Binding ==
+
private Binder binder = new DefaultBinder();
 +
 +
@Wire("grid")
 +
private Grid grid;
 +
...
 +
private Person person;
  
<source lang="java">
+
@Listen("onClick = button[label='Reset']")
binder.addFormLoadBindings(grid, "fx", "vm.person", null, null, null);
+
public void reset(){
binder.addFormSaveBindings(grid, "fx", "vm.person", command, null, null, null, null);
+
person = new Person();
</source>
+
grid.setAttribute("person", person);
 +
binder.loadComponent(grid, false);
 +
}
  
 +
public void resetAlternative(){
 +
person.setFirstName("");
 +
person.setLastName("");
 +
person.setAge(0);
 +
binder.notifyChange(person, "*");
 +
}
  
<source lang="java">
+
...
binder.addPropertyLoadBindings(firstNameBox, "value", "fx.firstName", null, null, null, null, null);
+
}
binder.addPropertySaveBindings(firstNameBox, "value", "fx.firstName", null, null, null, null, null
 
,"vm.emptyValidator",null);
 
</source>
 
  
<source lang="xml" high="3,8">
 
<div apply="org.zkoss.bind.BindComposer"
 
viewModel="@id('vm') @init('org.zkoss.reference.developer.mvvm.advance.DynamicFormBindingVM')"
 
validationMessages="@id('vmsgs')">
 
...
 
<row>
 
First Name:
 
<textbox id="fn"/>
 
<label id="fnError" style="color:red" />
 
</row>
 
 
</source>
 
</source>
  
 
+
* Line 12: We could replace <tt>person</tt> with a new instance to reset it.
<source lang="java">
+
* Line 13: In this case, we should also replace the attribute in ''Grid''.
binder.addPropertyLoadBindings(fNameErrorLabel, "value", "vmsgs[fn]", null, null, null, null, null);
+
* Line 14: Let the binder reload the load-bindings inside the <tt>grid</tt> which is specified at first parameter.
</source>
+
* Line 17: This is another way to trigger reload of data bindings, change properties and notify which properties you changed by <tt>binder.notifyChange(myBean, "propertyName")</tt>. In our example, we pass <tt>"*"</tt> as a property name which means '''all properties'''.
  
 
= Add Data Binding for Collections=
 
= Add Data Binding for Collections=
  
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 the ''Textbox'' bind to a object of ''Listbox'''s model. This data binding cannot be made by writing annotation in a zul because those ''Textbox''s are dynamically created.
+
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.
  
<source lang="xml" high="6">
+
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.
  
<window apply="org.zkoss.reference.developer.mvvm.advance.DynamicCollectionBindingComposer"
 
width="600px">
 
...
 
<div apply="org.zkoss.bind.BindComposer"
 
viewModel="@id('vm') @init('org.zkoss.reference.developer.mvvm.advance.DynamicCollectionBindingVM')">
 
<listbox model="@load(vm.personList)" selectedItem="@bind(vm.selectedPerson)" rows="10">
 
  
</listbox>
+
[[File:dev-ref-mvvm-adv-data-binding-programmatically.png | center | 500px]]
...
+
<div style="text-align:center">'''Edit in a Listcell'''</div>
</div>
 
...
 
</window>
 
</source>
 
  
  
== Use ItemRenderer ==
+
== Data Binding in ItemRenderer ==
  
 +
The [[ZK_Developer%27s_Reference/MVC/View/Renderer| 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.
  
[[File:dev-ref-mvvm-adv-data-binding-programmatically.png]]
+
'''Create data bindings in a renderer'''
 +
<source lang="java" high='20, 32,40,42'>
 +
public class DynamicCollectionBindingComposer extends SelectorComposer {
  
 +
private Binder binder = new DefaultBinder();
  
<source lang="java">
+
@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{
 
class MyListboxRenderer implements ListitemRenderer{
Line 131: Line 211:
 
throws Exception {
 
throws Exception {
  
//TODO explain why
+
//store the data bean as an attribute which can be accessed by EL expression
listitem.setAttribute("each", data);
+
listitem.setAttribute("bean", data);
 
 
 
//first name
 
//first name
Line 140: Line 220:
 
fnBox.setInplace(true);
 
fnBox.setInplace(true);
 
fnCell.appendChild(fnBox);
 
fnCell.appendChild(fnBox);
binder.addPropertyLoadBindings(fnBox, "value", "each.firstName", null, null, null, null, null);
+
binder.addPropertyLoadBindings(fnBox, "value", "bean.firstName"
binder.addPropertySaveBindings(fnBox, "value", "each.firstName", null, null, null, null, null, null, null);
+
, null, null, null, null, null);
 +
binder.addPropertySaveBindings(fnBox, "value", "bean.firstName"
 +
, null, null, null, null, null, null, null);
  
 
//last name
 
//last name
Line 154: Line 236:
 
 
 
}
 
}
 +
}
 
</source>
 
</source>
 +
* 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, <tt>bean.propertyname</tt>.
 +
* Line 40,42: Add a data binding between a ''Textbox'''s <tt>value</tt> attribute and a property <tt>bean.firstName</tt>.
 +
* Other data bindings for "last name" and "age" are created in a similar way to "first name".
  
== Use Template==
+
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.

Latest revision as of 04:58, 1 April 2022

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.