 |
Swing and Roundabouts 1: Event DTs
Posted by evanx on May 30, 2006 at 07:30 AM | Comments (14)
Introduction
"People always said there were no monsters, no real ones... but there are."
In his blog "A
Simple Framework for Desktop Applications"
John O'Conner summarised a JavaOne presentation by Hans Muller and Scott Violet
vis-a-vis JSR 296.
John reports that this framework would give some guidance to Swing developers, to avoid
common bad practices. Such as Swing apps that only support English,
and Swing apps that are "a tangle of actionPerformed methods that block the
event dispatch thread (EDT)."
I commented there that the issue of EDT-blocking event handlers is a big one for me.
In my last project, most event handlers kicked off a string of "long tasks"
(namely, communicating to a server over a GSM network), and I got into
such a tangle with SwingWorkers upon SwingWorkers, that the code became increasingly
difficult to follow.
This blog presents how I eventually untangled that application.
Application
"All we know is that there is still is no contact with the colony, and that a xenomorph may be involved."
Imagine a client-server application, where the client application communicates
to the server application via the network.
Actually, this system is deployed on a remote planet managed by aliens.
Those same class of aliens as we met in the Aliens movies, if you can believe it.
The server room is installed in a secure complex on one of the moons.
(I can't be more specific because of the NDA the Company makes everyone sign, so...)
Let's consider the login dialog. The alien enters its username and password,
and then hits the "Login" button. This invokes our event handler to communicate
to the server as follows.
Gui gui = new Gui(); // our GUI helper
Messenger messenger = new Messenger(); // our server comms helper
public void actionPerformed(ActionEvent event) {
if (event.getSource() == loginButton) {
loginActionPerformed();
}
}
protected void loginActionPerformed() {
try {
LoginResponse loginResponse = messenger.sendLogin(getLoginBean());
if (loginResponse.getErrorMessage() != null) {
gui.showMessageDialog(loginResponse.getErrorMessage());
if (loginResponse.isInvalidPassword()) passwordField.requestFocusInWindow();
else usernameField.requestFocusInWindow();
return;
}
if (loginResponse.isPasswordExpiring()) {
String password = gui.showPasswordInputDialog("Enter new password");
if (password != null) {
ChangePasswordResponse response = messenger.sendChangePassword(loginResponse, password);
if (response.getErrorMessage() != null) {
gui.showMessageDialog(response.getErrorMessage());
} else {
gui.showMessageDialog("Password changed");
}
}
}
login(loginResponse);
gui.showMessageDialog("Welcome, " + loginResponse.getAlienName());
} catch (CommsException ce) {
gui.showMessageDialog(ce, "The frelling connection is down again.");
} catch (Exception e) {
gui.showMessageDialog(e, "System failure. Try a hard reboot.");
}
}
As you can see, we communicate the login information to the server (ie. username and password),
which might return with an error such as "Unknown Alien", "Access Denied" or "Invalid password."
Or we might get a comms exception, eg. caused by the high gamma ray activity on this
particular alien planet.
Finally, if the login is valid, but the password is expiring soon, then we ask the alien
to enter a new password.
Now the problem is that we are blocking the EDT in this method! In particular
when we communicate to the server. Because it might take 10 or 20 seconds to get
a response from the server on the moon.
EDT Blocking Problem
"In case you haven't been paying attention to current events, we just got our asses kicked, pal!"
To be 100% safe, we should not "manipulate" Swing components outside the EDT.
Because, as the Java Tutorial says,
"Swing event-handling and painting code executes in a single thread, called the
event-dispatching thread. This ensures that each event handler finishes executing
before the next one executes and that painting isn't interrupted by events.
To avoid the possibility of deadlock, you must take extreme care that Swing
components and models are created, modified, and queried only from the
event-dispatching thread."
That's fine because we typically manipulate our components (causing visual changes on the screen)
in our EDT event handlers. For example, when a button is pressed, an actionPerformed()
method is invoked by, and within, the EDT. And that's where our application actually does stuff,
ie. in response an alien pressing or clicking something. Because GUI applications are event-driven.
The problem is that we should not block the EDT for any length of time, eg. while waiting
for a response from the server. Because then our Swing app is "hung" eg. if the alien starts moving
windows around, they don't get repainted, and if it presses a window's "close" button,
or a "cancel" button, or clicks on a different tab, nothing frikkin happens. Quite frustrating. Now understand that these aliens get very irritated, very quickly, and can go flying off the handle, and we really don't want that.
Waking the SwingWorker
"This a lead works isn't it? All we gotta do is lure the beast into the mould!"
So the solution is to use a SwingWorker
thread, so that our long task can run as a separate thread outside the EDT.
Then we can start a SwingWorker thread, and let the EDT exit the
actionPerformed() method immediately, so that it can service other
events if necessary (eg. in the response to the user clicking on something).
We might restructure our earlier loginActionPerformed() method to use a SwingWorker
as follows.
public void loginActionPerformed() {
SwingWorker loginWorker = new SwingWorker() {
LoginResponse loginResponse = null;
Exception exception = null;
public Object construct() {
// this is outside of the EDT, so no GUI stuff, just for our long task
try {
loginResponse = messenger.sendLogin(getLoginBean());
} catch (Exception e) {
exception = e;
}
return null;
}
public void finished() { // this is inside the EDT, for GUI stuff
if (exception != null) {
String message = null;
if (loginResponse != null) message = loginResponse.getErrorMessage();
if (message == null) message = "A frak up has occurred";
gui.showMessageDialog(exception, message);
} else if (loginResponse == null) {
gui.showMessageDialog(exception, "No response from server");
} else if (loginResponse.getErrorMessage() != null) {
gui.showMessageDialog(exception, loginResponse.getErrorMessage());
} else {
if (loginResponse.isPasswordExpiring()) {
String password = gui.showPasswordInputDialog("Enter new password");
if (password != null) {
ChangePasswordResponse response = messenger.sendChangePassword(loginResponse, password);
... // oh frell, a long task, and we are in the EDT here...
... // so this is so not gonna work
}
}
gui.showMessageDialog("Welcome, " + loginResponse.getAlienName());
}
}
};
loginWorker.start(); // start the worker thread and free the EDT
}
As you can see, we had to cajole our event handler quite significantly
to make it jump through a SwingWorker hoop. "Uh oh, this is crashing
my code-assist neural implants... I don't like it."
SwingWorker hive
"Seventeen days? Look man, I don't wanna rain on your parade, but we aint gonna last seventeen hours!"
The problem is that our "long task" is actually a chain of long tasks. First off, we communicate
the login info to the server. Clearly, this is a long task, which should run outside of the EDT.
When we get a response, we might popup a dialog to enter a new password. Uh oh, that should run in the EDT. Then we communicate to the server again, which is another
long task. Uh oh, that should run outside the EDT.
So we have to switch maybe a few times in and out of the EDT. SwingWorker is only
good for one switch out of the EDT (and then back again). "Frell it! You just can't win with these alien requirements!"
Nested SwingWorkers? Let's not even go there!
Reversing into a solution
"I say we take off and nuke the entire site from orbit. That's the only way to be sure."
Really, we wanna leave the code as it was before ie. as in the original loginActionPerformed()
implemention further above, without having to worry about blocking the EDT and SwingWorkers and all that.
But we know for sure that in order to make our event handler non-blocking, we will need
to run it via a SwingWorker thread. That's a given.
OK, so let's try the following.
public void actionPerformed(ActionEvent event) {
if (event.getSource() == loginButton) {
gui.startWorker(new Runnable() {
public void run() {
loginActionPerformed();
}
});
}
}
We have introduced a startWorker(runnable) helper method, which we implement as follows.
public void startWorker(final Runnable runnable) {
SwingWorker swingWorker = new SwingWorker() {
public Object construct() {
try {
setWaitCursor(true);
runnable.run();
} catch (Exception e) {
showExceptionDialog(e);
} finally {
setWaitCursor(false);
}
return null;
}
};
swingWorker.start();
}
Note that we have no finished() method for GUI code, and our runnable event handler is run from the construct() method ie. outside the EDT, so it must be "EDT-safe." Let's put that aside for a second...
The above method takes care of showing the hour glass, as follows.
public void setWaitCursor(boolean waitCursor) {
if (!waitCursor) applicationFrame.setCursor(null);
else applicationFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
glassPane.setVisible(waitCursor);
}
Notice that we might also activate a glass pane to disable a particular panel while our task is in progress, eg. to force the user to wait for the task to complete.
Lose on the Swing, gain on the roundabout
"Nobody never gave me nothing. So I say, let's fight this beast ourselves! This is as good a place as any to take
our first steps to heaven. Do you wanna die on your feet, or on your frikkin knees?"
The trick is that our runnable GUI code must be made "EDT-agnostic." In particular, since we
use a GUI helper class, it's methods must be made EDT-agnostic.
Firstly, let's implement an EDT-agnostic invokeAndWait() method as follows.
public void invokeAndWait(Runnable runnable) {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeAndWait(runnable);
} else {
runnable.run();
}
}
The above implementation of invokeAndWait() can be invoked safely
inside the EDT, or outside it, eg. in a SwingWorker's construct() method. As such, it is "EDT-safe."
Now we can "EDT-harden" methods invoked on Swing components in the loginActionPerformed()
further above, such as showConfirmDialog() in our Gui helper class, as follows.
int dialogOptionValue;
public int showConfirmDialog(final Object message, final String title, final int option) {
invokeAndWait(new Runnable() {
public void run() {
dialogOptionValue = JOptionPane.showConfirmDialog((Component) applicationFrame, message, title, option);
}
});
return dialogOptionValue;
}
Similarly, we EDT-harden other methods in our GUI helper class, where these are commonly in our event handlers, eg. requestFocusInWindows() as below.
public void requestFocusInWindow(final Component component) {
invokeAndWait(new Runnable() {
public void run() {
component.requestFocusInWindow();
}
});
}
While this seems verbose and tedious, it is relatively quick to EDT-harden a method in our GUI
helper class, by cutting and pasting the invokeAndWait(Runnable) boiler-plate
code from another EDT-hardened method.
At first, methods in our helper class are not necessarily EDT-agnostic, that is to
say, they do not use invokeAndWait(). When we encounter a need for that method
to be used in the construct() method of a SwingWorker, eg. via our
startWorker(runnable) helper, then we make that method
EDT-agnostic in our helper class via invokeAndWait(Runnable).
Similarly when we encounter a need for a method on a Swing component, we should
delegate that to our helper class, where we can make it EDT-safe.
Conclusion
"I dunno if I'm happy about this... I mean running around here in the dark with
that frikkin beast after us."
We should not block the EDT for any length of time, eg. while waiting
from a response from the server. Otherwise our Swing app appears to be "hung" to the user.
So we should use a SwingWorker thread, for our long tasks. However,
the SwingWorker is only good for one switch out of the EDT, and then back again.
This is a problem if our "long task" is actually a chain of long tasks, requiring
GUI invocations in between, eg. popping up a confirmation dialog before proceeding to the next long task.
If our event handler performs a long task, then we put it into a SwingWorker thread. But the trick is that we take care that any GUI manipulation is performed via an EDT-agnostic
helper class.
In our helper class, we use an invokeAndWait() method which will work
whether we are in the Swing EDT or not, by checking
SwingUtilities.isEventDispatchThread().
Our helper class may seem verbose and tedious, owing to the invokeAndWait(Runnable) boiler-plate code everywhere.
However, our application code is simpler and clearer. So it's a very good bargain.
We isolate the tedium in a single helper class, to free up the whole application for developers. It's a win-win, for us and the aliens.
Which means the aliens won't be blowing any developers out of the hatch, and visa versa, for now.
See the articles "Bean Curd"
and "Swing trashes Ajax" on my blog for further Swing reading :)
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
Nice article. I would recomend ALWAYS putting your
setWaitCursor(false);
in a finally block just to be safe. It needs to be in a normal state regardless if an exception was thrown.
I've got an application framework over in the java.net group called TFrame. Uses a very light framework with a Mediator pattern. Looking forward to seeing what Ben and Hans come up with. My framework was something very blue collar without any frills.
Posted by: cupofjoe on May 30, 2006 at 03:33 PM
-
Nice write-up, Evan. It does seem like quite a bird's nest of code, dealing with SwingWorkers bouncing in and out of the EDT.
My Aloe project (aloe.dev.java.net) provides an application framework very much along the lines of what Hans and Scott are proposing. One feature of the project, in fact, is a simple mechanism for dealing with off-loaded tasks that can communicate with listeners on the EDT by firing off events at key milestones during the behavior.
The framework defines an Activity interface that accepts registering ActivityListeners to be notified when started, stopped, paused, resumed, restarted, or completed, or when the behavior wishes to broadcast an arbitrary message. You need only implement the execute() method of AbstractActivity or Task, routinely querying canContinue() to receive pause or stop requests:
protected void execute() {
while(canContinue()) { // will block when the Activity is paused
doSomething();
reportMessage("just did something"); // Fires activityMessagePosted() on EDT
}
}
To my mind, the Activity metaphor is a little clearer than nesting SwingWorkers, especially paired with the familiar listener metaphor. Will have to experiment to see how this could apply to your example. Cheers,
-Chris
Posted by: cb on May 30, 2006 at 06:06 PM
-
Thanks joe - you're right, turning off the wait cursor should definitely be in a finally clause - i will update the article as such now.
One point i didn't make was that i like being able to follow the code using the IDE, eg. Alt-G in Netbeans. A problem with events and listener interfaces, is that you can't use this feature.
For instance, when the cursor is on a method of an interface, and you press Alt-G, it takes you to that declaration in the interface, and not to the implementation code. OK, the IDE can't know which implementation class that object might reference, but if there is a single implementation only (eg. in EJB2), it does know. Otherwise it could give you the choice, and then take you there? But I haven't given much thought to how IDEs might solve this problem - maybe there is no practical solution.
I found this a big problem with EJB2, ie. you are forever being taken to the EJB interface and not the (single) implementation, which is what you are actually interested in seeing of course.
Similarly with events - if you click on the fireSomeEvent(), the IDE doesn't know which listener implementation to take you to. So you can't follow the code easily.
I used an event/listener type solution for my nested SwingWorkers at first, but what i didn't like about it, was that i could not easily follow the sequence in the code using Alt-G. So i was losing track and found development and debugging very difficult. So i gave up on that.
Ideally the developer should be able to code naturally, without concern for the EDT, threads, and such plumbing. The developer has a hard enough job as it is implementing the required functionality, without the added strain of worrying about technical plumbing issues.
So the framework (eg. the future JSR 296 one), should, i believe, enable the developer to code relatively naturally, ie. without concern for EDT issues.
An elegant solution that someone proposed (that i came across somewhere in my reading), is to use annotations, eg. @InEdt for methods, that might cause them to be compiled to run within the EDT, eg. taking care of the boiler-plate code like "if not isEventDispatchThread(), then invokeAndWait(new Runnable() {})."
Personally i don't mind boiler-plate code, i just try to isolate it from the application code, eg. in a helper class, in a framework. It's easy enough to cut and paste boiler-plate code there.
As long as the application code looks neat and easy, i'm happy. I would say that the role of a framework is exactly that, ie. to hide away the technicalities, to enable the application code to be simplistic.
Posted by: evanx on May 31, 2006 at 12:41 AM
-
Nice article - I really like those Aliens ;).
But why the hassle with all this code? You can do all the above by changing a single line in the original code snippet:
Messenger messenger = (Messenger)Spin.off(new Messenger());
Seems easier to me - for further details take a look at http://spin.sf.net .
Sven
Posted by: svenmeier on May 31, 2006 at 03:39 AM
-
Nice Sven! I think annotating methods to run in the EDT, and then using a JDK proxy mechanism (or CGLIB) to handle the invokeAndWait(), is an elegant solution.
In my previous comment, i mentioned the problem of not being able to Alt-G through to the implementation, when using interfaces, and also event/listeners. But I guess this would not happen if using CGLIB to enhance the classes, rather than using a proxy interface to intercept the method invocations?
Posted by: evanx on May 31, 2006 at 04:37 AM
-
Spin is very elegant and all but magic. Unfortunately, it can be extremely difficult to debug when something goes wrong. Spin stack traces are often hard to decipher and thrown long after the real problem. Also, Spin has to rely on naming conventions, since there are no annotations documenting proper method usage with respect to the EDT. As such, threading mistakes are more likely.
Posted by: coxcu on May 31, 2006 at 08:26 AM
-
You are welcome evanx. I would recomend you delegate your events to a mediator. That way all of the events are in one place. Makes changing your business logic VERY easy.
TFrame has a mediator and a simple way to register all components and actions within the framework using a simple interface / static map. Trying to put a complicated GUI example up on the site now. Should be done with the complicated demo app by this weekend.
Posted by: cupofjoe on May 31, 2006 at 09:41 AM
-
Ooops, forgot to mention this. I don't do it but if Romain where to look at my application he would tell me I need to put up a glass pane to block any events from getting into the message queue while we are waiting on a background thread.
Just because you have an hour-glass cursor does NOT keep you from clicking on a button. ;-)
Unless you over ride your action event to check the current cursor state. That feels like a hack to me IMHO so I would recomend the glass pane solution.
Posted by: cupofjoe on May 31, 2006 at 09:44 AM
-
I look forward to that joe. You are right about glass pane. I put that in my setWaitCursor() method (see the last line of that method), because it makes sense to disable the panel while the hour glass is on.
Alternatively one could disable the buttons at least, especially the button whose event is currently being serviced. It might be convenient to pass this button to the startWorker() helper method as follows.
public void startWorker(final Runnable runnable, final JButton button) {
SwingWorker swingWorker = new SwingWorker() {
public Object construct() {
button.setEnabled(false);
setWaitCursor(true);
try {
runnable.run();
} catch (Exception e) {
showExceptionDialog(e);
} finally {
setWaitCursor(false);
button.setEnabled(true);
}
return null;
}
};
swingWorker.start();
}
I tend to overload methods in my helper classes a lot like this for convenience, too much :)
Posted by: evanx on June 01, 2006 at 12:08 AM
-
Hi Evan, I don't understand the following: When you introduce SwingWorker, you say (in a comment for the construct() method) that "this is outside of the EDT, so no GUI stuff, just for our long task".
Later on, however, you're enabling buttons, settting cursors etc. in the very same method. How is that supposed to work?
Posted by: mcnepp on June 01, 2006 at 04:54 AM
-
mcnepp, you're right! The setWaitCursor() method should be EDT-hardened using invokeAndWait(), and we should introduce an EDT-hardened method to enable buttons using invokeAndWait() as follows.
public void setEnabled(final JButton button, final boolean enabled) {
startWorker(new Runnable() {
public void run() {
button.setEnabled(enabled);
}
});
}
and/or i should fix my startWorker() method in the above comment, to the following.
public void startWorker(final Runnable runnable, final JButton button) {
SwingWorker swingWorker = new SwingWorker() {
Exception exception = null;
public Object construct() {
try {
runnable.run();
} catch (Exception e) {
exception = e;
}
return null;
}
public void finished() {
if (exception != null) showExceptionDialog(exception);
setWaitCursor(false);
button.setEnabled(true);
}
};
setWaitCursor(true);
button.setEnabled(false);
swingWorker.start();
}
I have moved the showExceptionDialog() into the finished() method, even though we would surely EDT-harden that method using invokeAndWait().
As tedious as it might seem to EDT-harden every GUI method we use,
actually we only have to harden them once (in our helper class),
and only those methods that we need to access amidst a series of long tasks in a SwingWorker construct() method. I found that the number of such methods is relatively limited, and i delt with them on a quick-fire "first come, first serve basis" ;) But for sure we will forget to use EDT-hardened delegates rather than direct invocations on our components, and so a careful code review of the runnables is a good idea. These can be quickly found eg. using Netbeans' "Find Usages" on the startWorker() method.
Incidently, there is potential "bug" in the above code as follows. Our runnable might disable the button, with the intention of having it disabled following the handling of this event. But then we stubbornly re-enable it in our finished() method.
Posted by: evanx on June 01, 2006 at 06:08 AM
-
Good fun read Evan!
Well, I guess I'll talk about my favorite open source hammer too. ;-)
The EventBus JavaOne slides shows a way to "nail" this problem using pub/sub eventing. There is no SwingWorker used, progress is reported, "Find Usages" can be used, and Romain's glasspane is involved (with the cancel button), even in a dockable frame context.
Basically a server side thread pool subscribes to events that contain tasks (queries) to run. As the tasks are run, progress and "completion" events are emitted (the SwingEventSerice ensures EDT thread safety), which are picked up by client side components (the progress veil and a table). The best part is that it is component-based development since the classes are loosely coupled, the contracts betweeen them are based on events only, and the components don't hold references to each other.
I'm looking lustily at the databinding and validation nails now... ;-)
Posted by: michaelbushe on June 06, 2006 at 12:18 PM
-
Thanks Micheal, I'm pleased you enjoyed it. I'll definitely be following EventBus :)
Posted by: evanx on June 06, 2006 at 01:41 PM
-
the content of this article (and series) has been revised - please see the "Gooey Beans" contents page for links, http://aptframework.dev.java.net/gooey/contents.html
Posted by: evanx on December 06, 2006 at 02:17 AM
|