Friday, May 05, 2006

Of Swing Frameworks and threaded Actions

I've been trying to come up with an implementation that properly encapsulates the most common cases of triggering, tracking, and responding to the results of a long-running operation from a Swing UI. If the JSR for a Swing app framework produces nothing else, it should address this issue, given its ubiquitousness with AWT's single-thread model.

Basically, you have this:
  • User trigger (button, menu, drop)
  • Collect information from the UI
  • Switch to another thread
  • Perform processing
  • Update the UI (success or failure)
Ideally, I'd like to just write straight code:

public void actionPerformed(ActionEvent e) {
Object[] o = list.getSelectedItems();
try {
Object result = processObjects(o);
list.setSelectedItem(result);
}
catch(CanceledException e) {
// ignore the results
}
catch(Exception e) {
list.setSelectedItem(null);
status.setText("Error: " + e.getMessage());
}
}



Which makes it very clear what's going on. However, as anyone who has used Swing for more than five minutes is aware, this makes your UI freeze for the duration of the operation.

I'd prefer an idiom that looks as close as possible to the original code, perhaps an "executor" block:

public void actionPerformed(ActionEvent e) {
Object[] o = list.getSelectedItems();
try {
Object result = null;
Handle handle = execute-off-edt {
result = processObjects(o);
}
list.setSelectedItem(result);
}
catch(CanceledException e) {
// ignore the results
}
catch(Exception e) {
list.setSelectedItem(null);
status.setText("Error: " + e.getMessage());
}
}

This idiom would let me transparently collect inputs and pass them to a threaded invocation without a lot of messy assignments (which do absolutely nothing to communicate what the code is trying to do). The handle would give me some sort of control over the spawned thread. And when the thing is done or fails, I don't have to do messy assignments to get its results. It's the sort of thing that Java's anonymous inner classes overcome; it's syntactic sugar, but it sure as hell simplifies how the code looks.

Spinplays some neat tricks with interfaces and proxies to let you use something close to this idiom (as do FoxTrot and SwingWorker, albeit less elegantly), but it has some unfortunate restrictions . For instance, if you start a Spin invocation from a mouse press event handler, the mouse release event is likely to be processed during your long-running method, and the rest of the mouse press handling will happen after the mouse release handling.

The reason the idiom above can'tbe used as is, is that in order for the event dispatch processing to continue, the current method needs to return immediately. This is the Spin issue. If you don't return, then you may have event handlers that haven't run yet, and end up running out of turn. I doubt that part of AWT is going to change, so I have to look at variations of the idiom instead.

So I've been playing around with a code pattern that handles the before/during/after pattern, including thread-hopping, cancelation and failure. It's probably not universal, since there are some situations it fits much better than others. I've put it in the form of a javax.swing.Action, since that's a pretty common method of triggering actions in Swing.

abstract class AsynchronousAction {
public void actionPerformed(final ActionEvent e) {
preInvoke(e);
// could use Executor here; implement the before/during/after pattern
// so you don't have to reimplement it on every action
new Thread() {
public void run() {
Throwable failure = null;
try {
invoke(e);
}
catch(Throwable e) {
failure = e;
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
postInvoke(failure);
}
});
}
}.start();
}
/** Change/update UI state here, collect input */
protected abstract void preInvoke(ActionEvent e);
/** Invoked from a non-UI thread. */
protected abstract void invoke(ActionEvent e);
/** Invoked when everything happens normally. */
protected abstract void postInvoke();
protected void handleFailure(Throwable failure) { }
protected abstract void postInvoke(Throwable failure) {
if (failure == null) postInvoke();
else {
handleFailure(failure);
}
}
}


I've left out the "cancel" functionality (and automatic disabling while the action is active) for clarity (using thread local variables, the spawned thread can check whether it should bother continuing or reporting its results).

So this works pretty well for, say, a "navigate" method which loads some data from a remote source and displays it. I can cancel the current one and invoke it with a different target. Any canceled action never reaches the postInvoke() method, so I don't have to explicitly add and remove listeners to yet another listener interface. It gets a bit clunky when there's a lot of data to pass to the invoke() method. Where you'd like to just pass parameters, you have to store in member data, which introduces synchronization problems when dealing with multiple invocations.

No comments: