Saturday, June 22, 2013

ZK CDT: Add EventListener into Model to Create UpdatableTextNote


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

No comments:

Post a Comment