Long Operations

Events for the same desktop are processed sequentially. In other words, an event handler will block any following handlers. Worse of all, the user, due to the limitation of HTTP, got no hint but the small processing dialog shown at the left-top corner on the browser.

With the echo event and the showBusy method as described in the Echo Events section above, you can provide a more descriptive message and prevent the user from, say, clicking other buttons to slow down the performance further for long operations.

However, blocking users from access might not be acceptable for your applications. To prevent the blocking, you have to, like desktop applications, process the long operation in another working thread. Then, report the processing status back the client continuously.

With ZK, you have four options: server push, suspend and resume, timer, and piggyback.

Alternative 1: Server Push

Server push is so-called reverse-Ajax which allows the server to send content to the client actively. With the help of server push, you could send or update the content to the client in the working thread when your pre-defined condition is satisfied. To use server push is simple,and it requires only three steps as follows,

  1. Enable server push for the desktop

    Invoke Desktop.enableServerPush(boolean bool) to enable server push

  2. Passing components required to be update into the working thread

  3. Invoke the working thread in the desktop

Note: You need to install zkex.jar or zkmax.jar to have the server push, unless you have your own implementation of org.zkoss.zk.ui.sys.ServerPush.

Let’s take a look at a real example below. If you want to update the number of client using server push, first of all, you have to enable server push for the desktop and then to invoke the thread as follows,

<window title="Demo of Server Push">
<zscript>
    import test.WorkingThread;    
    WorkingThread thread;    
    void startServerpush(){    
        //enable server push        
        desktop.enableServerPush(true);        
            //invoke working thread and passing required component as parameter            
            thread= new WorkingThread(info);            
        thread.start();        
    }    
    void stopServerpush(){    
        //stop the thread        
        thread.setDone();        
        //disable server push        
        desktop.enableServerPush(false);        
    }    
</zscript>
    <vbox>    
        <button label="Start Server Push" onClick="startServerpush()"/>        
        <button label="Stop Server Push" onClick="stopServerpush()"/>        
    <label id="info"/>    
    </vbox>    
</window>

Security Issue

One thing to notice is that the problem of synchronization which happens when a desktop is accessed by multiple threads at the same time Thus, before accessing the desktop, you have to invoke Executions.activate(Desktop desktop) to get full control of the desktop to avoid this problem, and then release the control of the desktop by invoking Executions.deactivate(Desktop desktop) after the thread finishing its job as follows,

    packagetest;    

        publicclassWorkingThread extends Thread{        
    private final Desktop _desktop;    
    private final Label _info;    
    private int _cnt;    
    private boolean _ceased;    
        publicWorkingThread(Label info){        
        _desktop = info.getDesktop();        
            _info= info;            
                            }publicvoidrun(){try{                            
                while(!_ceased){                
                Threads.sleep(2000); //Update each two seconds                
                Executions.activate(_desktop); //get full control of desktop                
                    try{                    
                    _info.setValue(Integer.toString(++_cnt));                    
                                            }catch(RuntimeException ex){throw ex;                                            
                    }catch(Error ex){                    
                    throw ex;                    
                }finally{                
                    Executions.deactivate(_desktop);                    
                        //release full control of desktop                        
                }                
            }            
            }catch(InterruptedException ex){            
            }            
                }publicvoidsetDone(){                
        _ceased = true;        
    }}    
Behind the Scene

The mechanism of server push is implemented using client-polling technique which the client will query the server repetitively to invoke the working thread to do its job, and the frequency of query could be adjusted manually by invoking Executions.setDelay(int min, int max, int factor).

  • min, the minimal delay to poll the server for any pending server-push threads.

  • max, the maximum delay to poll the server for any pending server-push threads.

  • factor, The real delay is the processing time multiplies the delay factor.

Last, one thing to notice is that the frequency will be adjusted automatically depending on the loading of the server

Alternative 2: Thread Suspend and Resume

With the help of server push, you don't have to take care about the problem of multi threads. However, if you would like to handle this job by yourself, you have to conform with the following rules due to the limitations of HTTP.

  • Use the wait method in the org.zkoss.zk.ui.Executions class to suspend the event handler itself, after creating a working thread.

  • Because the working thread is not an event listener, it cannot access any components, unless the components don't belong to any desktop. Thus, you might have to pass necessary information manually before starting the working thread.

  • Then, the working thread could crush the information and create components as necessary. Just don't reference any component that belongs to any desktop.

  • Use the notify(Desktop desktop, Object flag) or notifyAll(Desktop desktop, Object flag) method in the org.zkoss.zk.ui.Executions class in the working thread to resume the event handler, after the working thread finishes.

  • The resumed event handler won't be executed immediately until another event is sent from the client. To enforce an event to be sent, you could use a timer component (org.zkoss.zul.Timer) to fire an event a moment later or periodically. This event listener for this timer could do nothing or update the progress status.

Example: A Working Thread Generates Labels Asynchronously

Assume we want create a label asynchronously. Of course, it is non-sense to do such a little job by applying multi-threading, but you can replace the task with sophisticated one.

//WorkingThread
package test;
public class WorkingThread extends Thread {
    private static int _cnt;    
    private Desktop _desktop;    
    private Label _label;    
    private final Object _mutex = new Integer(0);    
    /** Called by thread.zul to create a label asynchronously.    
    *To create a label, it start a thread, and wait for its completion.    
    */    
    public static final Label asyncCreate(Desktop desktop)    
        throws InterruptedException {        
                final WorkingThread worker = new WorkingThread(desktop);synchronized (worker._mutex) { //to avoid racing                
            worker.start();            
            Executions.wait(worker._mutex);            
            return worker._label;            
        }        
    }    
    public WorkingThread(Desktop desktop) {    
        _desktop = desktop;        
        }public void run() {        
            _label = new Label("Execute "+ ++_cnt);            
            synchronized (_mutex) { //to avoid racing            
            Executions.notify(_desktop, _mutex);            
        }        
    }    
}

Then, we have a ZUML page to invoke this working thread in an event listener, say onClick.

<window id="main" title="Working Thread">
    <button label="Start Working Thread">    
        <attribute name="onClick">        
        timer.start();        
        Label label = test.WorkingThread.asyncCreate(desktop);        
        main.appendChild(label);        
        timer.stop()        
        </attribute>        
    </button>    
    <timer id="timer" running="false" delay="1000" repeats="true"/>    
</window>

Notice that we have to use a timer to really resume the suspended the event listener (onClick). It looks odd, but it is a must due to the HTTP limitation: to keep Web page alive at the browser, we have to return the response when the event processing is suspended. Then, when the working thread completes its job and notifies the even listener, the HTTP request was already gone. Therefore, we need a way to 'piggyback' the result, which the timer is used for.

More precisely, when the working thread notifies the event listener to resume, ZK only adds it to a waiting list. And, the listener is really resumed when another HTTP request arrives (in the above example, it is the onTimer event)

In this simple example, we do nothing for the onTimer event. For a sophisticated application, you can use it to send back the progressing status.

Alternative 3: Timer (No Suspend/Resume)

It is possible to implement a long operation without suspend and resume. It is useful if the synchronization codes are going too complex to debug.

The idea is simple. The working thread save the result in a temporary space, and then the onTimer event listener pops the result to the desktop.

//WorkingThread2
package test;
public class WorkingThread2 extends Thread {
    private static int _cnt;    
    private final Desktop _desktop;    
    private final List _result;    
        
    public WorkingThread2(Desktop desktop, List result) {    
        _desktop = desktop;        
        _result = result;        
    }    
    public void run() {    
        _result.add(new Label("Execute "+ ++_cnt));        
    }    
}

Then, you append the labels in the onTimer event listener.

<window id="main" title="Working Thread2">
    <zscript>    
        int numPending = 0;        
        List result = Collections.synchronizedList(new LinkedList());        
    </zscript>    
    <button label="Start Working Thread">    
        <attribute name="onClick">        
            ++numPending;            
            timer.start();            
            new test.WorkingThread2(desktop, result).start();            
        </attribute>        
    </button>    
    <timer id="timer" running="false" delay="1000" repeats="true">    
        <attribute name="onTimer">        
            while (!result.isEmpty()) {            
            main.appendChild(result.remove(0));            
            --numPending;            
            }            
            if (numPending == 0) timer.stop();            
        </attribute>        
    </timer>    
</window>

Alternative 4: Piggyback (No Suspend/Resume, No Timer)

Instead of checking the results periodically, you can piggyback them to the client when the user, say, clicks a button or enters something.

To piggyback, all you need to do is to register an event listener for the onPiggyback event to one of the root components. Then, the listener will be invoked each time ZK Update Engine has processed the events. For example, you can rewrite the codes as follows.

<window id="main" title="Working Thread3" onPiggyback="checkResult()">
    <zscript>    
    List result = Collections.synchronizedList(new LinkedList());    

    void checkResult() {    
        while (!result.isEmpty())        
            main.appendChild(result.remove(0));            
    }    
    </zscript>    
    <button label="Start Working Thread">    
        <attribute name="onClick">        
    timer.start();    
    new test.WorkingThread2(desktop, result).start();    
        </attribute>        
    </button>    
</window>

The advantage of the piggyback is no extra traffic between the client and the server. However, the user sees no updates if he doesn't have any activity, such as clicking or typing. Whether it is proper is really up to the application requirements.

Note: A deferrable event won't be sent to the client immediately, so the onPiggyback event is triggered only if a non-deferrable event is fired. Refer to the Deferrable Event Listeners section.