Notification
Overview
The binder is responsible for synchronizing data between the View and ViewModel. Typically the ViewModel is a POJO and has no reference to any UI component. Hence it cannot push data to a UI component. Developers have to specify the dependency of ViewModel's properties with ZK provided Java annotation, then binder knows when to reload which property and to update the UI view.
Notify Change
ZK bind provides a set of Java annotations ( @NotifyChange, @DependsOn, @NotifyChangeDisabled ) to specify when to reload which property to UI components. Developers have to specify it at design time, and the binder will synchronize data at run-time. The binder keeps track of binding relationships between component's attribute and ViewModel's property. After each time it invokes a ViewModel's method (setter, getter, or command method), it looks for corresponding component attribute to reload according to specified properties in the annotation.
Notify on Command
During execution of a command, one or more properties are changed because Command method usually contains business or presentation logic. Developers have to specify which property (or properties) is changed in Java annotation's element, then the data binding mechanism can reload them from ViewModel to View at run-time after executing the Command.
The syntax to notify property change:
One property:
@NotifyChange("oneProperty")
Multiple properties:
@NotifyChange({"property01","property02"})
All properties in a ViewModel:
@NotifyChange("*")
Following is an example of @NotifyChange:
public class OrderVM {
//the order list
List<Order> orders;
//the selected order
Order selected;
//getter and setter
@NotifyChange("selected")
@Command
public void saveOrder(){
getService().save(selected);
}
@NotifyChange({"orders","selected"})
@Command
public void newOrder(){
Order order = new Order();
getOrders().add(order);
selected = order;//select the new one
}
@NotifyChange("*")
@Command
public void deleteOrder(){
getService().delete(selected);//delete selected
getOrders().remove(selected);
selected = null; //clean the selected
}
}
- In newOrder() , we change property "orders" and "selected", therefore we apply @NotifyChange to notify binder to reload 2 changed properties after command execution. (line 16)
- The deleteOrder() also change the same 2 properties. Assume that this ViewModel class has only 2 properties, we can use "*" (asterisk sign) to notify all properties in current class. (line 24)
Notify on Setter
For similar reason, property might be changed in setter method. After value is saved to ViewModel's property, multiple components or other properties might depend on the changed property. We also have to specify this data dependency by @NotifyChange .
Enabled by Default
A setter method usually changes only one property, e.g. setMessage() will set message to new value. To save developer's effort, the target property changed by setter method is notified automatically. That is, @NotifyChange annotation on a setter is not required unless there are some other properties changed by the setter and need to be explicitly notified.
public class MessageViewModel{
private String message;
public String getMessage() {
return this.message;
}
public void setMessage(String msg) {
message = msg;
}
}
- There is no @NotifyChange on setter method in MessageViewModel .
The ZUL that uses MessageViewModel .
<textbox id="msgBox" value="@bind(vm.message)"/>
<label id="msg1" value="@load(vm.message)"/>
<label id="msg2" value="@load(vm.message)"/>
- When a user input value in msgBox and the value is saved back to ViewModel, the data binding mechanism will synchronize the value to msg1 and msg2 automatically.
Developer can disable this default behavior by adding @NotifyChangeDisabled on setter method.
Specify Dependency
For setter method, @NotifyChange is used when multiple properties have dependency relationship, e.g. one property's value is calculated from the other properties. Notice that if you apply @NotifyChange on a setter method, default notification behavior will be overridden.
@NotifyChange on Setter
public class FullnameViewModel{
//getter and setter
@NotifyChange("fullname")
public void setFirstname(String firstname) {
this.firstname = firstname;
}
@NotifyChange({"lastname","fullname"})
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getFullname() {
return (firstname == null ? "" : firstname) + " "
+ (lastname == null ? "" : lastname);
}
}
- By default setLastname() will notify "lastname" property's change. Becuase we apply NotifyChange on it, this default notification is overridden. We have to notify it explicitly. (line 10)
In above code snippet, fullname is concatenated by firstname and lastname. When firstname and lastname is changed by setter, the fullname should be reload to reflect its change.
If a property depends on many properties, @NotifyChange will distribute in multiple places in a ViewModel class. Developers can use @DependsOn for convenience. This annotation defines the dependency relations among properties and can provide same function as explicit @NotifyChange, but it's used on getter method.
Example of @DependsOn
public class FullnameViewModel{
//getter and setter
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
@DependsOn({"firstname", "lastname"})
public String getFullname() {
return (firstname == null ? "" : firstname) + " "
+ (lastname == null ? "" : lastname);
}
}
- The example is functional equivalent to previous one, but written in reversed meaning. It means when any one of 2 properties (firstname, lastname) change, fullname should be reloaded.
Notify Object Change
If you bind a component to a bean and want to reload it after the object's property changed, you should apply the following syntax on the setter of that target property:
@NotifyChange(".")
A dot(.) is a bit different from an asterisk(*) because an asterisk, @NotifyChange("*") , just reloads those components that bound to the bean's properties, e.g. @bind(vm.person.firstname) . The component bound to the bean itself, such as vm.person , will not be reloaded. If a component's attribute bound to the bean itself(i.e. vm.person ), the component's attribute won't be updated upon dependent data changes.
Most of time, you will bind one component attribute to only one bean property. However, in some use cases, you might want to bind the attribute to a calculation result of multiple bean properties; yet you don't want to put that calculation logic in bean. Then you can bind the component attribute to the whole bean and have a converter to convert the data upon the bean's multiple properties.
For example, assume there is a page in a tour website, a customer has to fill the leave and return day of a trip. And we use a converter to calculate days of a trip in order to remind the customer after he fills both fields.
Bind to an object example
<hlayout>
Leave Date: <datebox value="@save(vm.trip.leaveDate)"/>
</hlayout>
<hlayout>
Return Date: <datebox value="@save(vm.trip.returnDate)"/>
</hlayout>
Duration including leave and return(day): <label value="@load(vm.trip) @converter(vm.durationConverter)"/>
- Because durationConverter needs to access 2 properties (leaveDate, returnDate), we have to bind label's value to the trip object itself not an individual property. (line 7)
@NotifyChange(".") example
public class DurationViewModel {
private Trip trip = new Trip();
public class Trip{
private Date leaveDate;
private Date returnDate;
public Date getLeaveDate() {
return leaveDate;
}
@NotifyChange(".")
public void setLeaveDate(Date leaveDate) {
this.leaveDate = leaveDate;
}
public Date getReturnDate() {
return returnDate;
}
@NotifyChange(".")
public void setReturnDate(Date returnDate) {
this.returnDate = returnDate;
}
}
public Converter getDurationConverter(){
return new Converter() {
public Object coerceToUi(Object val, Component component, BindContext ctx) {
if (val instanceof Trip){
Trip trip = (Trip) val;
Date leaveDate = trip.getLeaveDate();
Date returnDate = trip.getReturnDate();
if (null != leaveDate && null != returnDate){
if (returnDate.compareTo(leaveDate)==0){
return 1;
}else if (returnDate.compareTo(leaveDate)>0){
return ((returnDate.getTime() - leaveDate.getTime())/1000/60/60/24)+1;
}
return null;
}
}
return null;
}
public Object coerceToBean(Object val, Component component, BindContext ctx) {
return null;
}
};
}
}
- About how to implement a converter, please refer to ZK Developer's Reference/MVVM/DataBinding/Converter.
Notify Programmatically
Sometimes the changed properties we want to notify depends on value at run-time, so we cannot determine the property's name at design time. In this case, we can use BindUtils.postNotifyChange() to notify change dynamically. The underlying mechanism for this notification is binder subscribed event queue that we talk about in the binder section. It uses desktop scope event queue by default.
Dynamic Notification
String data;
// setter & getter
@Command
public void cmd(){
if (data.equal("A")){
//other codes...
BindUtils.postNotifyChange(null,null,this,"value1");
}else{
//other codes...
BindUtils.postNotifyChange(null,null,this,"value2");
}
}
- The first parameter of postNotifyChange() is queue name and second one is queue scope. You can just leave it null and default queue name ans scope (desktop) will be used.
Version History
Version | Date | Content |
---|---|---|
6.0.0 | February 2012 | The MVVM was introduced. |