Search |
||
Swing and Roundabouts 1: Event DTsPosted by evanx on May 30, 2006 at 7:30 AM PDT
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.
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.
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.
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.
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."
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!
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.
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.
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.
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 :)
»
Comments
Comments are listed in date ascending order (oldest first)
|
||
|
|