LongOperations"

From Documentation
Line 167: Line 167:
 
</source>
 
</source>
  
Non-blocking operations (e.g. Thread.sleep(), Object.wait()) will naturally throw InterruptedException, otherwise you can call checkCancelled() at any granularity between your steps or long running loops.
+
Non-blocking operations (e.g. Thread.sleep(), Object.wait()) will naturally throw InterruptedException, otherwise you can call '''checkCancelled()''' at any granularity between your steps or long running loops.
  
 
= More examples =  
 
= More examples =  

Revision as of 03:57, 9 January 2015

Documentationobertwenzel
obertwenzel

Author
Robert Wenzel, Engineer, Potix Corporation
Date
January XX, 2015
Version
ZK 7.0.4


Introduction

Long operations are useful - bla - leverage Java Threads - bla - here how to hide and reuse the technical details. support MVVM and MVC programming model

Long Operations made simple

Minimal Usage

The LongOperation class provides a basic abstraction over the required ZK and Java Threading details to handle a (cancellable) long running operation using ServerPush to give immediate feedback to the user.

The minimal usage looks like this:

@Command
public void startLongOperation() {
	new LongOperation() {
		@Override
		protected void execute() throws InterruptedException {
			Thread.sleep(5000); //you'll do the heavy work here instead of sleeping
		}
	}.start();
	Clients.showNotification("background task started");
}

This does not do much, it simply waits for 5 seconds without blocking the UI, and then silently finishes.

UI feedback at end

Lets give the user some feedback that the task is done using onFinish().

@Command
public void startLongOperation() {
	new LongOperation() {
		
		@Override
		protected void execute() throws InterruptedException {
			Thread.sleep(5000); //you'll do the heavy work here instead of sleeping
		}

		@Override
		protected void onFinish() {
			Clients.showNotification("done sleeping for 5 seconds");
		};
	}.start();
		
	Clients.showNotification("starting, you'll be notified when done.");
}

This time the operation will display an information message when the background task is done.

UI feedback in between the task

To update the UI between your steps simply call activate() and deactivate() around your UI updates.

	@Override
	protected void execute() throws InterruptedException {
		doFirstHalf();

		activate();
		Clients.showNotification("50% Done"); // any UI updates in here
		deactivate();

		doSecondHalf();
	}

or with more steps

	@Override
	protected void execute() throws InterruptedException {
		updateStatus("step 1");
		step1();
		updateStatus("step 2");
		step2();
		updateStatus("step 3");
		step3();
		updateStatus("step 4");
		step4();
	}

	private void showStatus(String message) {
		activate();
		Clients.showNotification(message);
		deactivate();
	}

Cleanup after the operation

The callback onCleanup() is always called after the long operation has terminated (like a finally block) here you can restore your UI. e.g. clearing a "busy" overlay.

	@Command
	public void startLongOperation() {
		new LongOperation() {

			@Override
			protected void execute() throws InterruptedException {
				Thread.sleep(5000);
			}

			@Override
			protected void onCleanup() {
				Clients.clearBusy();
			};
		}.start();

		Clients.showBusy("Please wait, this may take some time.");
	}


Cancel the operation

Use the cancel() method to notify your operation that it should interrupt and implement the onCancel() callback.

	private LongOperation longOperation;

	@Command
	public void startLongOperation() {
		new LongOperation() {
			
			@Override
			protected void execute() throws InterruptedException {
				//performing a long loop
				for(long i = 0; i < 100000000L; i++) {
					if(i % 1000 == 0) { // check every 1000 steps
						checkCancelled(); //will throw and InteruptedException and exit when cancelled from outside
					}
				}
			}

			@Override
			protected void onFinish() {
				//give the user some feedback the task is done
				Clients.showNotification("done sleeping for 5 seconds");
			};

			@Override
			protected void onCancel() {
				Clients.showNotification("operation aborted...");
			};
		}.start();
		
		Clients.showNotification("starting, you'll be notified when done.");
	}

	@Command
	public void cancelOperation() {
		longOperation.cancel();
	}

Non-blocking operations (e.g. Thread.sleep(), Object.wait()) will naturally throw InterruptedException, otherwise you can call checkCancelled() at any granularity between your steps or long running loops.

More examples

A simple example

This simple example shows a basic use case of the LongOperation class. The operation creates a simple result which is added to the resultModel when it finishes. During the 3 seconds the default busy overlay is displayed, asking the user to wait.

public class SimpleLongOperationViewModel {
	private ListModelList<String> resultModel = new ListModelList<String>();

	@Command
	public void startLongOperation() {
		LongOperation longOperation = new LongOperation() {
			private List<String> result;

			@Override
			protected void execute() throws InterruptedException {
				Thread.sleep(3000); //simulate a long backend operation
				result = Arrays.asList("aaa", "bbb", "ccc");
			}

			protected void onFinish() {
				resultModel.addAll(result);
			};

			@Override
			protected void onCleanup() {
				Clients.clearBusy();
			}
		};

		Clients.showBusy("Result coming in 3 seconds, please wait!");
		longOperation.start();
	}

	public ListModelList<String> getResultModel() {
		return resultModel;
	}
}
  • Line 10: Implement the execute callback to collecting the result asynchrously
  • Line 15: Implement the onFinish callback to update the UI once the operation has finished successfully
  • Line 26: Launch the operation

In the 'startLongOperation'-command handler the "busy"-overlay is shown. In onCancel it is cleared, however the long operation terminates (successful or not).

Here the straight forward zul code using this SimpleLongOperationViewModel and posting the startLongOperation-command

    <div apply="org.zkoss.bind.BindComposer" 
    	 viewModel="@id('vm') @init('zk.example.longoperations.example.SimpleLongOperationViewModel')">
        <button onClick="@command('startLongOperation')" label="start"/>
        <grid model="@load(vm.resultModel)" height="300px"/>
    </div>

Updating the UI during the Operation

To update the UI during a long operation the desktop needs to be activated for UI updates. For this the methods activate() and deactivate() can be used as in the example below. It is advisable to activate the UI as short as possible for the UI to remain responsive. Any kind of UI updates can be performed between those 2 methods e.g.

  • change the busy message
  • adding/removing in a ListModelList
  • set any component properties (or notify change VM properties)
    • e.g. update the value of a status-<label> OR <progressmeter>
  • show/hide/enable/disable UI elements dynamically
  • post a global command MVVM (or post via EventQueue MVC)
  • ...
	private static final String IDLE = "idle";
	private String status = IDLE;

...

	@Command
	public void startLongOperation() {
		LongOperation longOperation = new LongOperation() {
			private List<String> result;

			@Override
			protected void execute() throws InterruptedException {
				step("Validating Parameters...", 10, 500);
				step("Fetching Data ...", 40, 1500);
				step("Filtering Data...", 60, 1750);
				step("Updating Model...", 90, 750);
				result = Arrays.asList("aaa", "bbb", "ccc");
			}

			private void step(String message, int progress, int duration) throws InterruptedException {
				activate();
				updateStatus(progress+ "% - " + message);
				deactivate();
				Thread.sleep(duration); //simulate processing time for the current step
			}

			@Override
			protected void onFinish() {
				resultModel.addAll(result);
				updateStatus(IDLE);
			}
		};
		longOperation.start();
	}

	private void updateStatus(String update) {
		status = update;
		BindUtils.postNotifyChange(null, null, UpdatingStatusLongOperationViewModel.this, "status");
	}
  • Line 21: activate the Thread for UI updates
  • Line 23: deactivate the Thread to send the updates back to the browser
  • Line 38: notify the change to update the UI
    <div apply="org.zkoss.bind.BindComposer"
    	 viewModel="@id('vm') @init('zk.example.longoperations.example.UpdatingStatusLongOperationViewModel')">
    	<button onClick="@command('startLongOperation')" label="start" 
    	        disabled="@load(vm.status ne 'idle')" autodisable="self" />
    	<label value="@load(vm.status)" />
    	<grid model="@load(vm.resultModel)" height="300px" />
    </div>
  • Line 3: The status label to update during the operation

Aborting a Long Operation

Referring to zk.example.longoperations.example.CancellableLongOperationViewModel you can cancel a long operation and give user feedback accordingly. The Long Operation will naturally terminate (calling onCancel) when the thread was interrupted, or by checking explicitly for cancellation between steps, in case the Long Operation was cancelled during a blocking method call.

Of course this requires you break down your task into separate steps, or use non blocking method calls that throw InterruptedExceptions themselves - the LongOperation class will handle them for you.

IMPORTANT don't swallow InterruptedExceptions (they are very helpful)

	private LongOperation currentOperation;

	@Command
	public void startLongOperation() {
		currentOperation = new LongOperation() {

			@Override
			protected void execute() throws InterruptedException {
				step("Starting query (WHAT is the 'ANSWER';). This might take a about 7.5 million Years ...", 2000);
				step("Executing your query (1 million years passed) please wait...", 500);
				step("Executing your query (2 million years passed) please wait...", 500);
				step("Executing your query (3 million years passed) please wait...", 500);
...
				result = "The answer is 42";
			}

			private void step(String message, int duration) throws InterruptedException {
				//check explicitly if the task was cancelled 
				// if cancelled it will thow an InterruptedException to stop the task
				checkCancelled(); 
				activate(); //would throw an InterruptedException if cancelled
				updateStatus(message);
				deactivate();
				Thread.sleep(duration); //will throw an InterruptedException if cancelled during sleep
			}

...
			@Override
			protected void onCancel() {
				Clients.showNotification("Now you'll never know... be more patient next time");
			}

			@Override
			protected void onFinish() {
				Clients.showNotification(result);
			}
...
		}
	}

	@Command
	public void cancelOperation() {
		currentOperation.cancel();
	}
  • Line 20: before performing a longer step call checkCancel()
  • Line 21: activation will interrupt automatically if the Operation was cancelled
  • Line 24: non blocking method calls such as sleep(), wait(), nio calls will interrupt automatically
  • Line 30: UI callback for a cancelled Long Operation
  • Line 43: cancel a task from a UI Command (MVVM or Event MVC)

Parallel Tasks

The LongOperation class also supports parallel tasks which can be visualized separately e.g. using a <grid> with a ListModelList of TaskInfo objects

check out the example in zk.example.longoperations.example.ParallelLongOperationViewModel / src/main/webapp/longop-parallel.zul

Parallel-longoperations.png

Extending LongOperation

It is easy to extend the LongOperation class to provide additional reusable functions for more compact code in your Composer/ViewModel classes, or to separate complex long operation code from your UI code.

zk.example.longoperations.BusyLongOperation enables a simpler way to update the busy message, and provides automatic cleanup once the task is done. This results in a simpler usage and a central point in your code to implement how a busy message is displayed in the UI.

also see -> zk.example.longoperations.example.BusyLongOperationViewModel


Another reason to extend your own LongOperation class could be to reuse the same execute code in different ViewModels and only define how the UI reacts to the actual result:

see:

  • zk.example.longoperations.ResultLongOperation<RESULT> - abstract basis class for LongOperations that have a result
  • zk.example.longoperations.example.DataFilterLongOperation - specific implementation of ResultLongOperation
  • zk.example.longoperations.example.DataFilterLongOperationViewModel - uses DataFilterLongOperation and updates the ViewModel when finished

Resulting Demo

The video below demonstrates the results of the two advanced usages described above. For ease of demonstration here we use a PDF printer so the resulting screen is a PDF file, but you can definitely specify a real printer to print out the desired results on papers. ERROR: Link hasn't been found

Summary

The LongOperation class is a reusable/extensible base class for your long operations. It integrates with MVC/MVVM and helps to separate UI updates from Background processing still allowing intermediate UI updates.

Refer to download .

Download

  • The source code for this article can be found in github.
  • Download the packed jar file from github.


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.