Showing posts with label MVVM. Show all posts
Showing posts with label MVVM. Show all posts

Saturday, October 5, 2013

ZK ServerPush with WebSocket - Rework


Introduction

This article describe how to use WebSocket to do ServerPush in ZK MVVM, modified from previous article ZK: Server Push with WebSocket (http://ben-bai.blogspot.tw/2013/07/zk-server-push-with-websocket.html) with the desktop based implementation from ZK AURequest with WebSocket (http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html).

It integrates WebSocket in ZK for ServerPush in more general way and has better performance (compared with previous component based version).

Environment: Tomcat 7.0.42, ZK 6.5.2

NOTE: This is just a POC with some customized components.

Pre-request

ZK AURequest with WebSocket
http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html

ZK Create Helper Tag to Make Programmer Happier
http://ben-bai.blogspot.tw/2013/09/zkcreatehelpertag-to.html

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

Result

View demo on line
http://screencast.com/t/zp1rcVhYa

Program

There are lots of source files, will only post code for some of them and post link for others.

index.zul

Entry page

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/index.zul

push_to_all.zul

Push value to all desktop (i.e., Application scope) with button click.

<zk>
    <!-- Tested with ZK 6.5.2 -->
    <window apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.TestPushToAllVM')">
        Counter:
        <!-- intbox, listen to positive integer in the begining -->
        <intbox readonly="true" />
            <contextBinding field="value" context="@load(vm.counter)" />
        listen to: <label value="@load(vm.counter)" />
        <div />
        inverse Counter:
        <!-- intbox, listen to negative integer in the begining -->
        <intbox id="inverseCounter" readonly="true" />
            <contextBinding field="value" context="@load(vm.inverseCounter)" />
        listen to: <label value="@load(vm.inverseCounter)" />
        <div />
        <!-- update value of positive/negative integer -->
        <button id="updateCounterBtn" label="updaet counter and negative counter"
            onClick="@command('updateCounter')" />
        <!-- switch listening context of the two intboxess above -->
        <button label="switch context" id="switchBtn"
            onClick="@command('switchCounter')" />
    </window>
</zk>


test.TestPushToAllVM.java

VM for push_to_all.zul

package test;

import impl.serverpush.ServerPushUtil;

import java.util.concurrent.atomic.AtomicInteger;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;

/** Tested with ZK 6.5.2
 * 
 * @author benbai123
 *
 */
public class TestPushToAllVM {
    /** context listened by intboxes */
    private String _counter = "positive";
    private String _inverseCounter = "negative";
    /** desktop of this VM */
    private Desktop targetDesktop;

    /** counter used to update positive/negative integer */
    private static AtomicInteger _cntCounter = new AtomicInteger(0);

    // getters
    public String getCounter () {
        return _counter;
    }
    public String getInverseCounter () {
        return _inverseCounter;
    }
    /**
     * update value to context 'positive' and 'negative' via
     * WebSocket
     * 
     * All components that listen to these context will be updated
     */
    @Command
    public void updateCounter () {
        int val = _cntCounter.incrementAndGet();
        push(val, "positive", false);
        push(-1*val, "negative", false);
    }
    /** switch listening components of 'positive' and 'negative' context
     * current desktop only
     * 
     */
    @Command
    @NotifyChange({"counter", "inverseCounter"})
    public void switchCounter () {
        int val = _cntCounter.get();
        // push to opposite (for self desktop) before switch context
        push(val, "negative", true);
        push(-1*val, "positive", true);
        String tmp = _counter;
        _counter = _inverseCounter;
        _inverseCounter = tmp;
        
    }

    /** push value
     * 
     * @param val
     * @param context
     */
    private void push (Object val, String context, boolean desktopOnly) {
        try {
            if (targetDesktop == null) {
                targetDesktop = Executions.getCurrent().getDesktop();
            }
            if (desktopOnly) {
                // push to current desktop only
                ServerPushUtil.pushVlaue(val, context, targetDesktop);
            } else {
                // push to all desktop
                ServerPushUtil.pushVlaue(val, context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


automatic_push.zul

Update value with java thread, Desktop scope.

<zk>
    <!-- Tested with ZK 6.5.2 -->
    <window apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.TestAutoPushVM')">
        Self:
        <!-- intbox that will be updated -->
        <intbox readonly="true" />
            <contextBinding field="value" context="@load(vm.task)" />
        <!-- start update -->
        <button label="start" onClick="@command('start')" />
        <!-- stop update -->
        <button label="stop" onClick="@command('stop')" />
        <!-- textbox that bind rows/cols to integer -->
        <textbox />
            <contextBinding id="cbd" field="@load(vm.prop)" context="@load(vm.task)" />
        <button label="switch field (rows/cols)" onClick="@command('switchRowCol')" />
    </window>
</zk>


test.TestAutoPushVM.java

VM for automatic_push.zul

package test;

import impl.serverpush.ServerPushUtil;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;

/** Tested with ZK 6.5.2
 * 
 * @author benbai123
 *
 */
public class TestAutoPushVM {

    /** Counter used to update integer */
    private AtomicInteger _cnt = new AtomicInteger(0);
    /** ServerPush task timer */
    private Timer timer;
    /** Desktop of this VM */
    private Desktop targetDesktop;
    /** Binded field of textbox */
    private String _prop = "rows";

    // getters
    public String getTask () {
        return "timerTask";
    }
    public String getProp () {
        return _prop;
    }
    /**
     * start server push with WebSocket for
     * specific context "timerTask"
     * 
     * current desktop only
     */
    @Command
    public void start () {
        if (timer == null) {
            // update once immediately when first time start
            if (_cnt.get() == 0) {
                push(_cnt.incrementAndGet(), "timerTask");
            }
            timer = new Timer();
            timer.schedule(getTimerTask(), 1000, 1000);
        }
    }
    /**
     * stop server push with WebSocket for
     * specific context "timerTask"
     * 
     * current desktop only
     */
    @Command
    public void stop () {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }
    @Command
    @NotifyChange("prop")
    public void switchRowCol () {
        if ("rows".equals(_prop)) {
            _prop = "cols";
        } else {
            _prop = "rows";
        }
    }
    // task to be scheduled to update context "timerTask" every second
    private TimerTask getTimerTask () {
        return new TimerTask() {
            public void run () {
                push(_cnt.incrementAndGet(), "timerTask");
            }
        };
    }
    /** push value to specific context
     * 
     * @param val value to push
     * @param context context to push
     */
    private void push (Object val, String context) {
        try {
            if (targetDesktop == null) {
                targetDesktop = Executions.getCurrent().getDesktop();
            }
            // push to current desktop only
            ServerPushUtil.pushVlaue(val, context, targetDesktop);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


components.IWebSocketEnhancedComponent.java

Copied from AURequestWithWebSocket, completely the same.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/IWebSocketEnhancedComponent.java

components.Intbox.java

Copied from AURequestWithWebSocket, change _useWebSocketAU default to true and override smartUpdate instead of setter.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Intbox.java

components.Textbox.java

Created for this POC, override smartUpdate instead of override setters separately.
Almost the same with Intbox (actually completely the same after serialVersionUID

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Textbox.java

components.serverpush.IWebSocketServerPushEnhancedComponent.java

Define a "WebSocket Enhanced Component -- ServerPush type" for ServerPush pattern

package components.serverpush;

import impl.serverpush.Binding;
import components.IWebSocketEnhancedComponent;

/** Tested with ZK 6.5.2<br>
 * 
 * Define a "WebSocket Enhanced Component -- ServerPush type"
 * for ServerPush pattern
 * 
 * @author benbai123
 *
 */
public interface IWebSocketServerPushEnhancedComponent extends IWebSocketEnhancedComponent {
    /** Add Binding for specific field/context pair
     * 
     * The added Binding will be stored to a Binding List of a Component
     * 
     * @param field field to bind
     * @param context context to bind with field
     * @return the Added Binding
     */
    public Binding addSocketContextBinding (String field, String context);
    /** Remove Binding for specific field/context pair
     * 
     * Remove it from Binding List of a Component
     * 
     * @param field field of field/context pair to remove
     * @param context context of field/context pair to remove
     */
    public void removeSocketContextBinding (String field, String context);
}


components.serverpush.Intbox.java

Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Textbox (completely the same after serialVersionUID)

package components.serverpush;

import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;

/** Tested with ZK 6.5.2<br>
 * 
 * Created for this POC, implements IWebSocketServerPushEnhancedComponent to
 * support WebSocket ServerPush
 * 
 * Almost the same with Textbox (completely the same after serialVersionUID)
 * 
 * @author benbai123
 *
 */
public class Intbox extends components.Intbox implements IWebSocketServerPushEnhancedComponent {

    private static final long serialVersionUID = -3277498174057967067L;

    @Override
    public Binding addSocketContextBinding(String field, String context) {
        return ServerPushUtil.addContextBinding(this, field, context);
    }

    @Override
    public void removeSocketContextBinding(String field, String context) {
        ServerPushUtil.removeContextBinding(this, field, context);
    }

}


components.serverpush.Textbox.java

Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Intbox (completely the same after serialVersionUID)

package components.serverpush;

import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;

/** Tested with ZK 6.5.2<br>
 * 
 * Created for this POC, implements IWebSocketServerPushEnhancedComponent to
 * support WebSocket ServerPush
 * 
 * Almost the same with Intbox (completely the same after serialVersionUID)
 * 
 * @author benbai123
 *
 */
public class Textbox extends components.Textbox implements IWebSocketServerPushEnhancedComponent {

    private static final long serialVersionUID = 2354643632056197764L;

    @Override
    public Binding addSocketContextBinding(String field, String context) {
        return ServerPushUtil.addContextBinding(this, field, context);
    }

    @Override
    public void removeSocketContextBinding(String field, String context) {
        ServerPushUtil.removeContextBinding(this, field, context);
    }

}


components.helper.ContextBinding.java

Helper component to help a component to bind field with ServerPush context

package components.helper;

import impl.serverpush.Binding;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;

import components.serverpush.IWebSocketServerPushEnhancedComponent;

/** Tested with ZK 6.5.2
 * 
 * Helper component to help a component to bind field with ServerPush context
 * 
 * @author benbai123
 *
 */
public class ContextBinding extends Div {

    private static final long serialVersionUID = -7156149643515776677L;
    /** field to bind with context (required) */
    private String _field = "";
    /** context to bind with field (required) */
    private String _context = "";
    /** specified id of target component (optional) */
    private String _target;
    /** used to store current binding (relatively old) for changing field/context */
    private Binding _oldBinding = null;
    /** found target */
    private IWebSocketServerPushEnhancedComponent _foundTarget;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public ContextBinding () {
        // update target url while created
        addEventListener(Events.ON_CREATE, new EventListener () {
            public void onEvent (Event event) {
                updateTargetBinding();
            }
        });
        // do not output any html
        setWidgetOverride ("redraw", "function (out) {}");
    }
    // setters
    public void setTarget (String target) {
        _target = target;
    }
    public void setField (String field) {
        if (field == null) {
            field = "";
        }
        if (!field.equals(_field)) {
            _field = field;
            updateTargetBinding();
        }
    }
    public void setContext (String context) {
        if (context == null) {
            context = "";
        }
        if (!context.equals(_context)) {
            _context = context;
            updateTargetBinding();
        }
    }

    /** Update Binding of target Component when field/context is changed
     * 
     */
    private void updateTargetBinding () {
        if (!_field.isEmpty() && !_context.isEmpty()) {
            _foundTarget = findTarget();
            if (_foundTarget != null) {
                // remove old binding if exists
                if (_oldBinding != null) {
                    if (_field.equals(_oldBinding.getField())
                            && _context.equals(_oldBinding.getContext())) {
                        // already binded, do nothing
                        return;
                    }
                    _foundTarget.removeSocketContextBinding(_oldBinding.getField(), _oldBinding.getContext());
                }
                // add new binding and store it to _oldBinding
                _oldBinding = _foundTarget.addSocketContextBinding(_field, _context);
            }
        }
    }
    /* package */ IWebSocketServerPushEnhancedComponent getFoundTarget () {
        return _foundTarget;
    }
    /** Try to find target to update
     * The order to try: <br>
     * 1. Try to find fellow under the same space owner with specified "target" attribute.<br>
     * 2. Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.<br>
     * 3. Try to find whether previous sibling is ContextBinding and already found a target.<br>
     * 4. Try to find whether parent is IWebSocketServerPushEnhancedComponent.<br>
     * 
     * This way it can work without target attribute in most cases.
     * @return IWebSocketServerPushEnhancedComponent if any
     */
    /* package */ IWebSocketServerPushEnhancedComponent findTarget () {
        Component comp;
        Component previous = getPreviousSibling();
        // Try to find fellow under the same space owner with specified "target" attribute.
        if (_target != null && !_target.isEmpty()) {
            comp = getSpaceOwner().getFellowIfAny(_target);
            if (comp != null
                && (comp instanceof IWebSocketServerPushEnhancedComponent)) {
                return (IWebSocketServerPushEnhancedComponent) comp;
            }
        }
        // Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.
        if (previous instanceof IWebSocketServerPushEnhancedComponent) {
            return (IWebSocketServerPushEnhancedComponent) previous;
        }
        // Try to find whether previous sibling is ContextBinding and already found a target.
        if (previous instanceof ContextBinding) {
            IWebSocketServerPushEnhancedComponent previousTarget = ((ContextBinding) previous).getFoundTarget();
            if (previousTarget != null) {
                return previousTarget;
            }
        }
        // Try to find whether parent is IWebSocketServerPushEnhancedComponent.
        if (getParent() instanceof IWebSocketServerPushEnhancedComponent) {
            return (IWebSocketServerPushEnhancedComponent)getParent();
        }
        return null;
    }
}


impl.DesktopUtils.java

Modified from AURequestWithWebSocket, almost the same, add synchronized for ServerPush.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/DesktopUtils.java

impl.TestWebSocketServlet.java

Modified from AURequestWithWebSocket, almost the same, store channel at desktop so can send response with desktop itself.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/TestWebSocketServlet.java

impl.EventListener.java

Define an Event listener used to process event from Not used in this POC.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/EventListener.java

impl.RequestFromWebSocket.java

Copied from AURequestWithWebSocket, completely the same excepts this fragment. Not used in this POC

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/RequestFromWebSocket.java

impl.serverpush.Binding.java

Binding that bind a field of a component to specific context for WebSocket ServerPush.

package impl.serverpush;

import java.io.Serializable;

/** Binding that bind a field of a component to specific context for WebSocket ServerPush
 * 
 * @author benbai123
 *
 */
public class Binding implements Serializable {
    private static final long serialVersionUID = -1591783705902661276L;
    /** field to bind of component */
    private String _field;
    /** context to bind with field */
    private String _context;
    // Constructor
    public Binding (String field, String context) {
        if (field == null) {
            field = "";
        }
        if (context == null) {
            context = "";
        }
        _field = field;
        _context = context;
    }
    // getters, setters
    public void setField (String field) {
        if (field == null) {
            field = "";
        }
        _field = field;
    }
    public String getField () {
        return _field;
    }
    public void setContext (String context) {
        if (context == null) {
            context = "";
        }
        _context = context;
    }
    public String getContext () {
        return _context;
    }
    // super
    public int hashCode () {
        return (_field + _context).hashCode() * 31;
    }
    public boolean equals (Object obj) {
        if (obj != null
                && (obj instanceof Binding)) {
            Binding b2 = (Binding) obj; 
            return (_field.equals(b2.getField())
                    && _context.equals(b2.getContext()));
        }
        return false;
    }
}


impl.serverpush.ServerPushUtil.java

Utilities for ServerPush with WebSocket, concept is similar to ServerPushWithWebSocket -- Component Oriented Version, rewritten it to Desktop Oriented Version.

package impl.serverpush;

import impl.TestWebSocketServlet;

import java.beans.Statement;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;

/** Tested with ZK 6.5.2<br>
 * 
 * Utilities for ServerPush with WebSocket, concept is similar to
 * ServerPushWithWebSocket -- Component Oriented Version, rewritten it to
 * Desktop Oriented Version.
 * 
 * @author benbai123
 *
 */
@SuppressWarnings("unchecked")
public class ServerPushUtil {
    /** keep all binded Desktop */
    private static List<Desktop> bindedDesktops = new ArrayList<Desktop>();
    /** Lock for push to all desktops */
    private static Integer LOCK_FOR_PUSH_TO_ALL = 0;
    /** Lock for access bindedDesktops */
    private static Integer LOCK_FOR_ACCESS_BINDED_DESKTOPS = 0;
    /** attribute kay for binding map */
    private static final String BINDING_MAP = "BINDING_MAP_FOR_WEBSOCKET_SERVERPUSH";

    /** Add a Binding to a Component
     * 
     * @param comp Component to bind member field with update context
     * @param field field of component to bind
     * @param context context to bind with field
     * @return Binding the added Binding object
     */
    public static Binding addContextBinding (Component comp, String field, String context) {
        Binding binding = null;
        synchronized (comp.getDesktop()) {
            // get binding list of specified component
            List<Binding> bindingList = getBindingList(comp);
            // create a binding object with specified field/context
            binding = new Binding(field, context);
            // add binding into binding list if not exists
            if (!bindingList.contains(binding)) {
                bindingList.add(binding);
            }
        }
        // return created binding
        return binding;
    }
    /** Remove a Binding from a Component
     * 
     * @param comp Component to remove Binding
     * @param field field of the field-context pair to remove
     * @param context context of the field-context pair to remove
     */
    public static void removeContextBinding (Component comp, String field, String context) {
        synchronized (comp.getDesktop()) {
            Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
            // get binding list of specified component
            List<Binding> bindingList = bindingMap.get(comp);
            if (bindingList != null) {
                // create a binding object with specified field/context
                Binding target = new Binding (field, context);
                // remove binding if exists
                for (Binding b : bindingList) {
                    if (b.equals(target)) {
                        bindingList.remove(b);
                        break;
                    }
                }
            }
        }
    }

    /** Push a value to a context for all desktops 
     * 
     * @param value value to push
     * @param context context to push to
     * @throws Exception whatever
     */
    public static void pushVlaue (Object value, String context) throws Exception {
        synchronized (LOCK_FOR_PUSH_TO_ALL) {
            // used to store dead desktop
            List<Desktop> deadDesktops = new ArrayList<Desktop>();
            // make a copy to reduce lock
            List<Desktop> desktopToPush = new ArrayList<Desktop>();
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                desktopToPush.addAll(bindedDesktops);
            }
            // for each binded desktop
            for (Desktop bindedDesktop : desktopToPush) {
                if (bindedDesktop.isAlive()) {
                    // push value to context if alive
                    execPush(value, context, bindedDesktop);
                } else {
                    // store it to deadDesktops otherwise
                    deadDesktops.add(bindedDesktop);
                }
            }
            // remove all dead desktops
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                bindedDesktops.removeAll(deadDesktops);
            }
            desktopToPush.removeAll(deadDesktops);
            for (Desktop dt : desktopToPush) {
                // send response via WebSocket
                TestWebSocketServlet.sendResponse(dt);
            }
        }
    }
    /** Push a value to a context for specified desktop
     * 
     * @param value value to push
     * @param context context to push to
     * @param desktop desktop to apply this push
     * @throws Exception whatever
     */
    public static void pushVlaue (Object value, String context, Desktop desktop) throws Exception {
        if (desktop.isAlive()) {
            // push value to context for specified desktop if alive
            execPush(value, context, desktop);
        } else {
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                // remove specified desktop from bindedDesktops otherwise
                bindedDesktops.remove(desktop);
            }
        }
        // send response via WebSocket
        TestWebSocketServlet.sendResponse(desktop);
    }
    /** Apply "push value to context" for specified desktop
     * 
     * @param value value to push
     * @param context context to push to
     * @param desktop desktop to apply
     * @throws Exception whatever
     */
    public static void execPush (Object value, String context, Desktop desktop) throws Exception {
        // execute push if WebSocket of desktop is ready
        if (TestWebSocketServlet.isWebSocketReady(desktop)) {
            synchronized (desktop) {
                // get binding map
                // (Map<String, List> where String is component ID and List is Binding objects)
                Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)desktop.getAttribute(BINDING_MAP);
                if (bindingMap != null) {
                    for (Map.Entry<Component, List<Binding>> e : bindingMap.entrySet()) {
                        // get component by ID
                        Component comp = e.getKey();
                        // get binding list
                        List<Binding> bindings = e.getValue();
                        // for each binding
                        for (Binding binding : bindings) {
                            // push value to component if
                            // context of binding is equal to specified context
                            if (binding.getContext().equals(context)) {
                                // get field from binding
                                String field = binding.getField();
                                // build setter name
                                String method = "set" + field.substring(0, 1).toUpperCase() + field.substring(1);
                                // call setter to set value to component
                                Statement stat = new Statement(comp, method, new Object[]{value});
                                stat.execute();
                            }
                        }
                    }
                }
            }
        }
    }
    /** Get Binding list of a Component
     * 
     * @param comp Component to get Binding list
     * @return Binding list for specified Component
     */
    private static List<Binding> getBindingList (Component comp) {
        // get binding map
        Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
        // try to get binding list
        List<Binding> bindingList = bindingMap.get(comp);
        // create and add binding list if not exists
        if (bindingList == null) {
            bindingList = new ArrayList<Binding>();
            bindingMap.put(comp, bindingList);
        }
        // return (created) binding list
        return bindingList;
    }
    /** Get Binding map of a Desktop
     * 
     * @param dt specified Desktop
     * @return Map<Component, List<Binding>> Binding map of specified Desktop
     */
    private static Map<Component, List<Binding>> getBindingMap (Desktop dt) {
        // try to get binding map
        Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)dt.getAttribute(BINDING_MAP);
        // create and add binding map if not exists
        if (bindingMap == null) {
            bindingMap = new Hashtable<Component, List<Binding>>();
            dt.setAttribute(BINDING_MAP, bindingMap);
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                bindedDesktops.add(dt);
            }
        }
        // return (created) binding map
        return bindingMap;
    }
}


zk.xml

Modified from AURequestWithWebSocket, add config for lang-addon.xml, also change the way to get path for WebSocket (line 61~)

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/WEB-INF/zk.xml

lang-addon.xml

Define components used in this POC.

<!-- Tested with ZK 6.5.2
    Define components
-->

<language-addon>
    <addon-name>param</addon-name>
    <language-name>xul/html</language-name>

    <!-- It specifies what language addons this addon
        depends on. If specified, this addon will be
        parsed after all the specified addons are parsed -->
    <depends>zul</depends>

    <!-- define param -->
    <component>
        <component-name>contextBinding</component-name>
        <extends>div</extends>
        <component-class>components.helper.ContextBinding</component-class>
    </component>
    <component>
        <component-name>intbox</component-name>
        <extends>intbox</extends>
        <component-class>components.serverpush.Intbox</component-class>
    </component>
    <component>
        <component-name>textbox</component-name>
        <extends>textbox</extends>
        <component-class>components.serverpush.Textbox</component-class>
    </component>
</language-addon>


Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Integrate/WebSocket/ServerPushWithWebSocket

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/ServerPushWithWebSocket.swf

Friday, April 26, 2013

ZK MVVM: Form Binding with Validator


Introduction

This article describe how to use Validator with Form Binding in ZK MVVM.

The Result

View demo on line
http://screencast.com/t/caPtmte73J

Pre-request

ZK MVVM: Form Binding
http://ben-bai.blogspot.tw/2013/04/zk-mvvm-form-binding.html

The Program

MVVM_FormBinding.zul

The inner div bind to vm.person, i.e., use vm.person as a form object. It will save form content before savePerson command and use vm.formValidator as the validator to do validation of form fields.

The validator will store error message in validationMessages map with the specified id 'vmsgs'

<zk>
    <!-- 
        tested with ZK 6.0.2

        validationMessages="@id('vmsgs')"
        means all validation message will be stored in a map
        that mapped by the id 'vmsgs'

        i.e., you can get messages from that map by 
        @bind(vmsgs['keyOfMessageContent'])
    -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('blog.ben.test.mvvm.formbinding.FormBindingWithValidatorTestVM')"
        validationMessages="@id('vmsgs')">
        <!-- 
            @validator(vm.formValidator)
            do validation before save form with this validator
         -->
        <div form="@id('fx') @load(vm.person) @save(vm.person, before='savePerson') @validator(vm.formValidator)">
            <vlayout>
                <hlayout>
                    <label value="First Name" />
                    <textbox value="@bind(fx.firstName)" />
                    <!-- 
                        @bind(vmsgs['firstNameContentError'])
                        show error message of the key 'firstNameContentError' in
                        vmsgs map after form validation as needed
                     -->
                    <label value="@bind(vmsgs['firstNameContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['firstNameError'])" style="color: red;" />
                </hlayout>
                <hlayout>
                    <label value="Last Name" />
                    <textbox value="@bind(fx.lastName)" />
                    <label value="@bind(vmsgs['lastNameContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['lastNameError'])" style="color: red;" />
                </hlayout>
                <hlayout>
                    <label value="Age" />
                    <intbox value="@bind(fx.age)" />
                    <label value="@bind(vmsgs['ageContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['ageTooSmallError'])" style="color: red;" />
                    <label value="@bind(vmsgs['ageTooLargeError'])" style="color: red;" />
                </hlayout>
            </vlayout>
        </div>
        <button label="save" onClick="@command('savePerson')" />
        <label value="@load(vm.personContent)" />
    </div>
</zk>


FormBindingTestVM.java

Simple vm, contains getter, command and a validator

package blog.ben.test.mvvm.formbinding;

import org.zkoss.bind.ValidationContext;
import org.zkoss.bind.Validator;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.bind.validator.AbstractValidator;

/**
 * tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class FormBindingWithValidatorTestVM {
    private Person _person;
    public Person getPerson () {
        if (_person == null) {
            _person = new Person("Ben", "Bai", 123); // fake
        }
        return _person;
    }
    public String getPersonContent () {
        return _person.getFirstName() + " " + _person.getLastName() + ", age = " + _person.getAge();
    }
    /**
     * empty function as a command for
     * save in form binding and update personContent
     */
    @Command
    @NotifyChange("personContent")
    public void savePerson () {
    }
    /**
     * get the validator that will do validation while save
     * @return
     */
    public Validator getFormValidator(){
        return new AbstractValidator() {

            public void validate(ValidationContext ctx) {
                // get value from form context,
                // ctx.getProperties("firstName")[0].getValue() will
                // get the value that bind with @bind(fx.firstName)
                // in zul page
                String firstName = (String)ctx.getProperties("firstName")[0].getValue();
                String lastName = (String)ctx.getProperties("lastName")[0].getValue();
                Integer age = (Integer)ctx.getProperties("age")[0].getValue();
                if (firstName == null || firstName.isEmpty()) {
                    // put error message into validationMessages map
                    addInvalidMessage(ctx, "firstNameContentError", "firstName is required ");
                } else if (firstName.length() < 3) {
                    addInvalidMessage(ctx, "firstNameError", "firstName at least 3 chars ");
                }
                if (lastName == null || lastName.isEmpty()) {
                    addInvalidMessage(ctx, "lastNameContentError", "lastName is required");
                } else if (lastName.length() < 3) {
                    addInvalidMessage(ctx, "lastNameError", "lastName at least 3 chars ");
                }
                if (age == null) {
                    addInvalidMessage(ctx, "ageContentError", "age is required");
                } else {
                    if (age < 0) {
                        addInvalidMessage(ctx, "ageTooSmallError", "age should not be negative");
                    }
                    if (age > 130) {
                        addInvalidMessage(ctx, "ageTooLargeError", "age should smaller than 130");
                    }
                }
            }
        };
    }
}


Person.java

Simple pojo contains data of a person

package blog.ben.test.mvvm.formbinding;

public class Person {
    String _firstName;
    String _lastName;
    Integer _age;
    public Person (String firstName, String lastName, Integer age) {
        _firstName = firstName;
        _lastName = lastName;
        _age = age;
    }
    public void setFirstName (String firstName) {
        _firstName = firstName;
    }
    public void setLastName (String lastName) {
        _lastName = lastName;
    }
    public void setAge (int age) {
        _age = age;
    }
    public String getFirstName () {
        return _firstName;
    }
    public String getLastName () {
        return _lastName;
    }
    public Integer getAge () {
        return _age;
    }
}


Reference

MVVM > Data Binding > Validator
http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Data_Binding/Validator

Download

MVVM_FormBinding_with_Validator.zul
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/WebContent/MVVM_FormBinding_with_Validator.zul

FormBindingWithValidatorTestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/FormBindingWithValidatorTestVM.java

Person.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/Person.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/MVVM_FormBinding_with_Validator.swf

ZK MVVM: Form Binding


Introduction

This article describe how to do Form Binding in ZK MVVM.

The Result

View demo on line
http://screencast.com/t/yAE1f4wTALNr

Pre-request

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

The Program

MVVM_FormBinding.zul

The inner div bind to vm.person, i.e., use vm.person as a form object. It will save form content before savePerson command.

<zk>
    <!-- 
        tested with ZK 6.0.2
     -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('blog.ben.test.mvvm.formbinding.FormBindingTestVM')">
        <!-- form binding,
            @id('fx') @load(vm.person)
            here the id (fx) is mapping to vm.person

            @save(vm.person, before='savePerson')"
            save to vm.person before savePerson command
            the before/after in @save is required,
            you should always indicate when (before/after a command)
            to save form content

            save to vm.person means
            call vm.getPerson().setFirstName() to save firstName
        -->
        <div form="@id('fx') @load(vm.person) @save(vm.person, before='savePerson')">
            <vlayout>
                <!-- fx.firstName, i.e., vm.person.firstName -->
                <hlayout>
                    <label value="First Name" />
                    <textbox value="@bind(fx.firstName)" />
                </hlayout>
                <hlayout>
                    <label value="Last Name" />
                    <textbox value="@bind(fx.lastName)" />
                </hlayout>
                <hlayout>
                    <label value="Age" />
                    <intbox value="@bind(fx.age)" />
                </hlayout>
            </vlayout>
        </div>
        <!-- click this button will execute savePerson() method in vm
            and trigger save action of the form above -->
        <button label="save" onClick="@command('savePerson')" />
        <!-- show content of person,
            will be updated after savePerson command -->
        <label value="@load(vm.personContent)" />
    </div>
</zk>


FormBindingTestVM.java

Simple vm, contains getter and command

package blog.ben.test.mvvm.formbinding;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
/**
 * tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class FormBindingTestVM {
    private Person _person;
    public Person getPerson () {
        if (_person == null) {
            _person = new Person("Ben", "Bai", 123); // fake
        }
        return _person;
    }
    public String getPersonContent () {
        return _person.getFirstName() + " " + _person.getLastName() + ", age = " + _person.getAge();
    }
    /**
     * empty function as a command for
     * save in form binding and update personContent
     */
    @Command
    @NotifyChange("personContent")
    public void savePerson () {
    }
}


Person.java

Simple pojo contains data of a person

package blog.ben.test.mvvm.formbinding;

public class Person {
    String _firstName;
    String _lastName;
    Integer _age;
    public Person (String firstName, String lastName, Integer age) {
        _firstName = firstName;
        _lastName = lastName;
        _age = age;
    }
    public void setFirstName (String firstName) {
        _firstName = firstName;
    }
    public void setLastName (String lastName) {
        _lastName = lastName;
    }
    public void setAge (int age) {
        _age = age;
    }
    public String getFirstName () {
        return _firstName;
    }
    public String getLastName () {
        return _lastName;
    }
    public Integer getAge () {
        return _age;
    }
}


Reference

MVVM > Data Binding > Form Binding
http://books.zkoss.org/wiki/ZK%20Developer's%20Reference/MVVM/Data%20Binding/Form%20Binding

Download

MVVM_FormBinding.zul
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/WebContent/MVVM_FormBinding.zul

FormBindingTestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/FormBindingTestVM.java

Person.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/Person.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/MVVM_FormBinding.swf