Introduction
This is the 7th article of
ZK CDT (ZK Component Development Tutorial) walkthrough, this article describe how to add event listener into model to update components while the data in model is changed.
Add Listener into Model
The goal of this walkthrough is to create a quicknote component that help you do some quick note on web page (similar to you crop some screenshot, highlight something and add some text in photo editor), and describe each part of a component through each article in this walkthrough.
This is the 7th part: Add listener into model.
Result
View demo online:
http://screencast.com/t/48GXGDlDIPaO
What you changed at client side will be updated to server side, and update all other components that use the same model at client side.
Any change via API of model within event thread (e.g., with a button click) at server side will also update all components that use the model at client side.
All of the works are handled by model with the event listener pattern automatically, do not need to do anything in VM or Composer.
Pre-request
ZK CDT: Create Custom Event for RecordableTextNote
http://ben-bai.blogspot.tw/2013/06/zk-cdt-create-custom-event-for.html
Program
updatabletextnote.zul
Contains two updatabletextnote, they use the same model so their status will be sync by model automatically.
<zk>
<!--
completely new sample for updatabletextnote,
two updatabletextnote use the same model,
will sync automatically
-->
<div apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm') @init('custom.zk.samples.quicknote.UpdatableTextNoteVM')">
<button label="remove selected" onClick="@command('removeSelected')" />
<button label="clear" onClick="@command('clear')" />
<hlayout>
<!-- The updatabletextnote component that will cover
its children by a mask
you can click on the mask to add a textarea
and type text in it
-->
<updatabletextnote width="150px" height="55px"
model="@load(vm.model)"
selectedTextNoteIndex="@save(vm.selectedIndex)"
onTextNoteBlockChanged="@command('checkResult')">
<hlayout style="margin-top: 15px;">
<div width="35px" />
<label value=" x " />
<div width="35px" />
<label value=" = 6" />
</hlayout>
</updatabletextnote>
<updatabletextnote width="150px" height="55px"
model="@load(vm.model)"
selectedTextNoteIndex="@save(vm.selectedIndex)"
onTextNoteBlockChanged="@command('checkResult')">
<hlayout style="margin-top: 15px;">
<div width="35px" />
<label value=" + " />
<div width="35px" />
<label value=" = 5" />
</hlayout>
</updatabletextnote>
Result: <label value="@load(vm.result)"
style="@load(vm.result eq 'Wrong'? 'color: red;' : '')" />
</hlayout>
</div>
</zk>
UpdatableTextNoteVM.java
VM used in updatabletextnote.zul, provide data, do command.
package custom.zk.samples.quicknote;
import java.util.ArrayList;
import java.util.List;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.model.UpdatableTextNoteModel;
/**
* VM used by updatabletextnote.zul<br>
* <br>
* As you can see, simply provide data and handle command,<br>
* will not refresh model by VM<br>
*
* @author benbai123
*
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class UpdatableTextNoteVM {
private UpdatableTextNoteModel _model;
private int _selectedIndex = -1;
private String _result = "Wrong";
// getters, setters
public UpdatableTextNoteModel getModel () {
if (_model == null) {
List l = new ArrayList();
l.add(new TextNoteData(5, 10, 25, 25, ""));
l.add(new TextNoteData(50, 10, 25, 25, ""));
_model = new UpdatableTextNoteModel(l);
}
return _model;
}
public void setSelectedIndex (int selectedIndex) {
_selectedIndex = selectedIndex;
}
public String getResult () {
return _result;
}
@Command
@NotifyChange("result")
public void checkResult () {
List<TextNoteData> datas = getModel().getTextNoteData();
try {
int valOne = Integer.parseInt(datas.get(0).getText());
int valTwo = Integer.parseInt(datas.get(1).getText());
if (valOne*valTwo == 6
&& valOne+valTwo == 5) {
_result = "Correct";
} else {
_result = "Wrong";
}
} catch (Exception e) {
_result = "Wrong";
}
}
@Command
public void removeSelected () {
List<TextNoteData> datas = getModel().getTextNoteData();
if (_selectedIndex >= 0
&&datas.size() > _selectedIndex) {
getModel().remove(datas.get(_selectedIndex));
}
}
@Command
public void clear () {
getModel().clear();
}
}
UpdatableTextNote.java
Java class of UpdatableTextNote component, extends RecordableTextNote and add TextNoteDataListener into model to handle TextNoteDataChangeEvent.
package custom.zk.components.quicknote;
import org.zkoss.json.JSONObject;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteDataChangeEvent;
import custom.zk.components.quicknote.event.TextNoteDataListener;
import custom.zk.components.quicknote.model.UpdatableTextNoteModel;
import custom.zk.components.quicknote.model.TextNoteModel;
/** UpdatableTextNote, extends {@link custom.zk.components.quicknote.RecordableTextNote},<br>
* hold UpdatableTextNoteModel ({@link custom.zk.components.quicknote.model.UpdatableTextNoteModel})<br>
* and add event listener ({@link custom.zk.components.quicknote.event.TextNoteDataListener})<br>
* into model<br>
* <br>
* One new thing:<br>
* Add event listener into model, the onChange function of event listener<br>
* will be called when data of model is changed<br>
* <br>
* Two effects:<br>
* will update change from server side to client side if<br>
* the change is made within UI thread<br>
* <br>
* If two or more components use the same UpdatableTextNoteModel,<br>
* and one of them is changed at client side, all other components<br>
* will be updated when the change event is sent to server<br>
* <br>
* the effects is similar to multiple components use<br>
* the same model and refresh whole model when there are<br>
* any changes<br>
* <br>
* but there are two advantages with the listener pattern<br>
* <br>
* Advantages:<br>
* Clean code: the update is handled by model and listener in component,<br>
* you do not need to add a lot of @Command, @NotifyChange<br>
* to update the model<br>
* <br>
* Save bandwidth and improve client side performance:<br>
* it can update only the specific text note block<br>
* instead of refresh whole model<br>
* <br>
* @author benbai123
*
*/
public class UpdatableTextNote extends RecordableTextNote {
private static final long serialVersionUID = -5105526564611614334L;
// prevent unnecessary update
private boolean _ignoreModelChangedEvent = false;
// model, support text note data event listener
private UpdatableTextNoteModel _model;
// event listener, listen the change of model
private TextNoteDataListener _textNoteDataListener;
// setter
public void setModel (TextNoteModel model) {
// model is changed
if (model != _model) {
if (_model != null) {
// remove listener from old model
_model.removeListDataListener(_textNoteDataListener);
}
if (model != null) {
if (model instanceof UpdatableTextNoteModel) {
_model = (UpdatableTextNoteModel)model;
} else {
// create new model if the instance of model
// is not UpdatableTextNoteModel
// in this case, each component will use different instance of
// UpdatableTextNoteModel even they use the same model
_model = new UpdatableTextNoteModel(model.getTextNoteData());
}
// add listener into model
initTextNoteDataListener();
} else {
_model = null;
}
}
// call super
super.setModel(_model);
}
private void initTextNoteDataListener () {
if (_textNoteDataListener == null) {
// create listener as needed
_textNoteDataListener = new TextNoteDataListener () {
public void onChange (TextNoteDataChangeEvent event) {
onTextNoteDataChange(event);
}
};
}
// add listener into model
_model.addTextNoteDataListener(_textNoteDataListener);
}
// called by listener
private void onTextNoteDataChange (TextNoteDataChangeEvent event) {
// change type
int type = event.getType();
if (!_ignoreModelChangedEvent) {
if (type == TextNoteDataChangeEvent.UPDATE_NOTE_BLOCK) {
// update specific text note block
smartUpdate("textNoteBlockToUpdate", getBlockInfo(event));
} else if (type == TextNoteDataChangeEvent.ADD_NOTE_BLOCK) {
// add a new text note block
smartUpdate("textNoteBlockToAdd", getBlockInfo(event));
} else if (type == TextNoteDataChangeEvent.REMOVE_NOTE_BLOCK) {
// remove a specific text note block
smartUpdate("textNoteBlockToRemove", getBlockInfo(event));
} else if (type == TextNoteDataChangeEvent.REFRESH_MODEL) {
// re-render all text note blocks
updateNoteBlocks();
}
}
}
// get the JSON String that represents a text note block
private String getBlockInfo (TextNoteDataChangeEvent event) {
// index of changed note block
int index = event.getIndex();
// data of changed note block
TextNoteData data = event.getData();
JSONObject obj = new JSONObject();
obj.put("index", index);
obj.put("left", data.getPosX());
obj.put("top", data.getPosY());
obj.put("width", data.getWidth());
obj.put("height", data.getHeight());
obj.put("text", data.getText());
return obj.toJSONString();
}
// override
public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
_ignoreModelChangedEvent = true;
super.service(request, everError);
_ignoreModelChangedEvent = false;
}
}
UpdatableTextNote.js
Widget class of UpdatableTextNote component, extends RecordableTextNote and add functions to update, add or remove a specific text note block.
/** UpdatableTextNote
*
* Extends RecordableTextNote and add functions to update, add or remove
* a specific text note block
*
* No new thing
*
*/
custom.zk.components.quicknote.UpdatableTextNote = zk.$extends(custom.zk.components.quicknote.RecordableTextNote, {
// update a specific text note block
setTextNoteBlockToUpdate: function (blockInfo) {
this._updateNoteBlock(jq.evalJSON(blockInfo));
},
// add a specific text note block
setTextNoteBlockToAdd: function (blockInfo) {
var data = jq.evalJSON(blockInfo),
index = data.index,
blocks = jq(this.$n()).find('.' + this.getZclass() + '-noteblock'),
len = blocks.length; // keep length before the new one is added
// add to the tail at first
this._renderNoteBlock(data.left, data.top, data.width, data.height, data.text);
// insert case,
// the specified index is smaller than the length
// of text note blocks
if (index < len) { // insert
// newDom: last block
// ref: the block at the specified index
var newDom = this._getTextNoteBlockByIndex(len),
ref = this._getTextNoteBlockByIndex(index);
// insert newDom before ref
ref.parentNode.insertBefore(newDom, ref);
}
},
// remove a specific text note block
setTextNoteBlockToRemove: function (blockInfo) {
// find block by specified index
var block = this._getTextNoteBlockByIndex(jq.evalJSON(blockInfo).index);
if (block) {
// remove it
block.parentNode.removeChild(block);
}
},
// update a specific note block
_updateNoteBlock: function (data) {
// get attributes from data
// find the block by specified index
var index = data.index,
block = this._getTextNoteBlockByIndex(index),
textarea = block.firstChild,
bstyle = block.style,
tstyle = textarea.style,
zattr = textarea.zkAttributes;
// update block,
// also update the stored attributes of block
zattr.left = bstyle.left = data.left + 'px';
zattr.top = bstyle.top = data.top + 'px';
zattr.width = tstyle.width = data.width + 'px';
zattr.height = tstyle.height = data.height + 'px';
zattr.text = data.text;
jq(textarea).val(data.text);
},
// find text note block by the given index
_getTextNoteBlockByIndex: function (index) {
var current = this.$n('mask').nextSibling,
idx = 0;
// for each text note block
while (current) {
if (idx == index) {
return current;
}
current = current.nextSibling;
idx++;
}
return null;
}
});
UpdatableTextNoteModel.java
Model used in UpdatableTextNote component, extends TextNoteModel and override functions to fire event when the content of model is changed.
package custom.zk.components.quicknote.model;
import java.util.ArrayList;
import java.util.List;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.event.TextNoteDataChangeEvent;
import custom.zk.components.quicknote.event.TextNoteDataListener;
/** UpdatableTextNoteModel<br>
* <br>
* <p>Extends TextNoteModel and override functions to handle data change event,
* will fire proper event to each event listener within this model to
* update corresponding component</p>
*
* <p>Used by {@link custom.zk.components.quicknote.UpdatableTextNote}</p>
*
* @author benbai123
*
*/
public class UpdatableTextNoteModel extends TextNoteModel {
private List<TextNoteDataListener> _listeners = new ArrayList<TextNoteDataListener>();
// Constructor
@SuppressWarnings("rawtypes")
public UpdatableTextNoteModel (List textNodeDatas) {
super(textNodeDatas);
}
// fire data change event to each event listener
protected void fireEvent(TextNoteData data, int index, int type) {
final TextNoteDataChangeEvent evt = new TextNoteDataChangeEvent(this, data, index, type);
for (TextNoteDataListener listener : _listeners) {
listener.onChange(evt);
}
}
// add an event listener
public void addTextNoteDataListener (TextNoteDataListener listener) {
if (listener == null) {
throw new NullPointerException("Listener cannot be null");
}
_listeners.add(listener);
}
// remove an event listener
public void removeListDataListener(TextNoteDataListener listener) {
_listeners.remove(listener);
}
// get index of a text note data
public int indexOf (TextNoteData data) {
return super.getTextNoteData().indexOf(data);
}
// override
// add
public void add (TextNoteData data) {
super.add(data);
int index = getTextNoteData().indexOf(data);
// fire event to add text note block at client side
fireEvent(data, index, TextNoteDataChangeEvent.ADD_NOTE_BLOCK);
}
// add at specific position
public void add (int index, TextNoteData data) {
super.add(index, data);
// fire event to add text note block at client side
fireEvent(data, index, TextNoteDataChangeEvent.ADD_NOTE_BLOCK);
}
// remove
public void remove (TextNoteData data) {
int index = getTextNoteData().indexOf(data);
super.remove(data);
// fire event to remove text note block at client side
fireEvent(data, index, TextNoteDataChangeEvent.REMOVE_NOTE_BLOCK);
}
public void update (int index, TextNoteData data) {
super.getTextNoteData().remove(index);
super.add(index, data);
// fire event to update text note block at client side
fireEvent(data, index, TextNoteDataChangeEvent.UPDATE_NOTE_BLOCK);
}
// clear all data
public void clear () {
super.clear();
// refresh whole model
fireEvent(null, -1, TextNoteDataChangeEvent.REFRESH_MODEL);
}
}
TextNoteDataListener.java
Define the method used to listen when the content of UpdatableTextNoteModel is changed.
package custom.zk.components.quicknote.event;
/** Define the method used to listen when the content of
* UpdatableTextNoteModel ({@link custom.zk.components.quicknote.model.UpdatableTextNoteModel}) is changed
*
* @author benbai123
* @see custom.zk.components.quicknote.model.UpdatableTextNoteModel
* @see custom.zk.components.quicknote.event.TextNoteDataChangeEvent
*/
public interface TextNoteDataListener {
/** Sent when the contents of text note blocks have been changed.
*/
public void onChange(TextNoteDataChangeEvent event);
}
TextNoteDataChangeEvent.java
Defines an event that encapsulates changes to text note blocks.
package custom.zk.components.quicknote.event;
import custom.zk.components.quicknote.Data.TextNoteData;
import custom.zk.components.quicknote.model.TextNoteModel;
/**
* Defines an event that encapsulates changes to text note blocks.
*
* @author benbai123
*/
public class TextNoteDataChangeEvent {
/** Identifies whole model should be refreshed */
public static final int REFRESH_MODEL = 0;
/** Identifies a text note block should be updated */
public static final int UPDATE_NOTE_BLOCK = 1;
/** Identifies a new text note block should be addad */
public static final int ADD_NOTE_BLOCK = 2;
/** Identifies a text note block should be removed */
public static final int REMOVE_NOTE_BLOCK = 3;
/* the model that trigger this event */
private TextNoteModel _model;
/* the affected data (which represents a text note block) */
private TextNoteData _data;
/* index of affected text note block */
private int _index;
/* action type of this event */
private int _type;
/** Constructor
*
* @param data the changed text note data for a text note block if any
* @param index the index of the changed text note block if any
* @param type one of {@link #REFRESH_MODEL}, {@link #UPDATE_NOTE_BLOCK},
* {@link #ADD_NOTE_BLOCK}, {@link #REMOVE_NOTE_BLOCK}
*/
public TextNoteDataChangeEvent (TextNoteModel model, TextNoteData data,
int index, int type) {
_model = model;
_data = data;
_index = index;
_type = type;
}
// getters
public TextNoteModel getModel () {
return _model;
}
public TextNoteData getData () {
return _data;
}
public int getIndex () {
return _index;
}
public int getType () {
return _type;
}
}
zk.wpd
Define components under "custom.zk.components.quicknote"
* only the added part, not the full code
NOTE: more components will be added with other articles later
...
<widget name="UpdatableTextNote" />
...
lang-addon.xml
Define all components in the project
* only the added part, not the full code
NOTE: more components will be added with other articles later
...
<!-- 7th, updatabletextnote component
extends recordabletextnote and handle data change event
within model to update component at client side as needed
-->
<component>
<component-name>updatabletextnote</component-name>
<extends>recordabletextnote</extends>
<component-class>custom.zk.components.quicknote.UpdatableTextNote</component-class>
<widget-class>custom.zk.components.quicknote.UpdatableTextNote</widget-class>
</component>
...
References
Listbox.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/Listbox.java
AbstractListModel.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/AbstractListModel.java
ListDataListener.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/event/ListDataListener.java
ListDataEvent.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/event/ListDataEvent.java
Download
Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Components_Development__Series/001_walkthrough/ZKQuickNote
updatabletextnote_component.swf
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/component_development_series/001_walkthrough/updatabletextnote_component.swf