http://www.developer.com/

Back to article

Managing Long-Running Tasks in a Swing App


January 24, 2008

Getting a Swing application to work can often be its own challenge. Getting it to work "just right" all the more taxes one's experience and patience. One of the big challenges often revolves around the concept of a "long running task"—something, such as a big database query or a slow network connection, which takes longer than a second to execute. After all, you want the task to effectively run in parallel with the thread that handles the UI display to keep the application from seeming like it has gone into limbo (and looking amateurish when it stops repainting). And when the task finishes, determining whether it succeeded or failed, and knowing what information to show the user, can be its own headache.

In a previous article (MVC in a Java/Swing Application, May 2007), I introduced the concept of an Application Controller to separate the display logic from the decision making logic of a Swing-based application. I will expand on that topic a little further to show how to manage a long-running task. In fact, the main "JFrame" and "AppController" classes used there will be recycled and repurposed here. Therefore, you may want to review the previous article first. If you insist on skipping that article, at least be aware that the AppController class is a singleton, accessed via a static getInstance() method.

This article expects an intermediate level of experience, and the code snippets will include the use of Threads, generics, and enums.

Design Objectives

The typical use case is that the user presses a button and triggers a task that might take too long to complete to just execute directly in the event dispatch thread. Leaving such a task in the event dispatch thread means the entire UI freezes until the task is completed. It is completely unresponsive. (Users "dislike" unresponsive apps.) Thus, the task will be spun off in a Thread so the event dispatch thread can keep the visible UI bits painting on the screen and reacting to mouse and keyboard events. The user will need some visual cue that the task is currently in operation, and the command button that started the task probably should be disabled for safety.

When the task finishes, the UI will need to be updated. But, as experience teaches: You cannot always, cleanly, fiddle with the state of the UI directly from an external Thread and get away with it. The safe avenue is to schedule the update through the SwingUtilities class, but then that forces another Runnable instance to achieve this. And finally, those visual hints need to be turned off (and the command button turned back on) so the user knows the task has completed its work and that they can resume theirs.

But, that doesn't cover any failure scenarios: If an exception is thrown or some other recoverable error condition exists, a dialog should probably be shown with an appropriate message. Finally, out of politeness, you may want to provide a way to cancel the operation prematurely, by giving the user some form of an "abort" button.

It is a lot to coordinate, and if your Swing app hasn't achieved the right amount of MVC, any attempt at solving this quickly approaches spaghetti code. Fortunately, getting a working solution—with a solid strategy and a central application controller object such as I have discussed previously—is not that big of an ordeal.



Click here for a larger image.

Main Frame


Click here for a larger image.

"Please Wait" Dialog

Let me restate the design objectives in the following list of requirements:

  • An application controller is the point of contact and coordination to maintain a sense of MVC separation.
  • The task executes in response to a either button press or a programmatic invocation.
  • The task runs in a new thread, separate from the event dispatch thread. The user maintains a sense of being in control of the app.
  • So long as the task is executing, a visual hint of action is given to the user, and the button is disabled.
  • A cancel button is provided to abort the task early.
  • The task might finish successfully, be interrupted, or have an unexpected error. The user will be informed appropriately of any outcome.
  • To keep things even more interesting, the application will have a sense of the expected amount of time to complete the task, and will display a message to the user if the task seems to be taking an inordinate amount of time.

You will pick these items off as you go, but getting to the end result will need a couple of helper classes, so those will be covered first.

Task Details

TaskInfo

Connected to the act of a task running is the fact that there are multiple outcomes you want to keep track of: did it finish? was there a problem? what was the output? (and so forth). The task might require input data before starting, and likely has some data object of its own to represent its final state (and you will want to access this data object at a later point). If an exception is thrown, you likewise want to be able to access this exception at a later point.

The TaskInfo class I am showing here provides a completely generic way of keeping track of these details. This class does NOT include a Runnable implementation (that comes later) but instead provides you what you need to keep track of anything that goes into or comes out of a Runnable implementation.

public class TaskInfo<T> {

   private T taskData;
   private Exception exception;
   private State state;

   public static enum State {
      NOT_SPECIFIED,
      RUNNING,
      HAD_EXCEPTION,
      WAS_INTERRUPTED,
      FINISHED
   }

   public TaskInfo(T taskData) {
      this.taskData = taskData;
   }

   public T getTaskData() {
      return taskData;
   }

   // more public getters and setters for
   // 'exception' and 'state' go here.
   // They are updated as the task runs.

Note this implementation uses generics, so this code will require at least Java 1.5 to run. (If you need to run on 1.4 or older, remove the generics code bits and make the "taskData" instance variable of type Object, and be careful of the effects of casting the Object where used later.) The use of generics, as opposed to a vanilla Object type, will add type-safety later when you extract the "taskData" object. By using an approach such as this, the TaskInfo can be given any sort of object to keep track of its details. (Although you will see a StringBuffer used, below, to collect a messages, you could provide your own custom type.)

The idea for this class is that, for any given task, a TaskInfo is created and given to the task. As the task executes, it will update its TaskInfo appropriately, mainly by keeping the "state" and "taskData" variables updated, and by setting the value of the exception variable, should one arise.

When the task completes, you will see that control passes back through the app controller, where code responsible for updating the display will determine exactly what to display based on the final state of the TaskInfo object.

BufferFillerTask

Being a article demo, this class doesn't do anything too exciting: it fills a StringBuffer object with the value "Hello, World!". However, to simulate the effects of a "long running task" (and to give it enough time to be properly cancelled) it first sleeps for several seconds. Also, to simulate something going wrong every once in a while, it will randomly throw an Exception saying "Something awful happened." (And, of course, if the task is interrupted by the user, an "Operation cancelled" message is shown.)



Click here for a larger image.

Operation Succeeded


Click here for a larger image.

Operation Failed


Click here for a larger image.

Operation Cancelled

Keep in mind that, as the task goes about its normal functions, it will need to keep its TaskInfo object (passed in via the constructor) updated:

import java.util.Random;

public class BufferFillerTask implements Runnable {

   private TaskInfo<StringBuffer> info;

   public BufferFillerTask(TaskInfo<StringBuffer> info) {
      this.info = info;
   }
   
   public void run() {
      info.setState(TaskInfo.State.RUNNING);
      try {
         Thread.sleep(4000);

         if (new Random().nextInt(3)==0) {
            throw new Exception("Something awful happened!");
         }

         info.getTaskData().append("Hello, World!");
         info.setState(TaskInfo.State.FINISHED);
      }
      catch (InterruptedException e) {
         info.setState(TaskInfo.State.WAS_INTERRUPTED);
      }
      catch (Exception e) {
         info.setException(e);
         info.setState(TaskInfo.State.HAD_EXCEPTION);
    }
   }
}
MVC design note: The task does not deal with the display of its result. Rather, it deals only with obtaining the result.

The AppController

Following the same usage pattern as in the preceding article where I introduced the AppController concept, the UI panel or frame that instantiates the command button will also send a reference to the button to the AppController (so that the AppController can invoke its setEnabled method). It also will register an anonymous ActionListener to call a method in the AppController to get the task started up.

The new part here involves the details of starting the task and dealing with the completion of the task. I keep these two parts in separate methods for a couple of reasons: First, it allows the task completion method to be reused by multiple related tasks, and second, it allows the task completion method to be tested and debugged independently:

public void handleBeginTaskAction() {
   StringBuffer buffer = new StringBuffer();
   final TaskInfo<StringBuffer> info =
      new TaskInfo<StringBuffer>(buffer);
   BufferFillerTask task = new BufferFillerTask(info);

   Runnable doneHandler = new Runnable() {
      public void run() { handleTaskCompleted(info); }
   };

   taskButton.setEnabled(false);
   textComponent.setText("Starting task...");
   new TaskController(task, doneHandler).start();
}

void handleTaskCompleted(TaskInfo<StringBuffer> info) {
   taskButton.setEnabled(true);
   switch (info.getState()) {
   case FINISHED:
      StringBuffer buffer = info.getTaskData();
      textComponent.setText(buffer.toString());
      break;
   case WAS_INTERRUPTED:
      textComponent.setText("Operation cancelled.");
      break;
   case HAD_EXCEPTION:
      textComponent.setText(info.getException().getMessage());
      break;
   }
}

The "taskButton" and "textComponent" are just references to widgets on the main UI display. (The design philosophy and wiring logic was presented in the original article.)

The first method is called in the event dispatch thread by the command button's anonymous ActionListener implementation. (Or, perhaps it is triggered programmatically via a direct call.) Because the task being run will utilize a StringBuffer to reflect its data state, a new one is created, and then wrapped inside a new TaskInfo object. A new task instance is created to actually perform the task, and then a simple anonymous Runnable is implemented to call the second method shown. (Were this a C-style language, a function-pointer might make sense.)

This second method receives the same TaskInfo object given to the task, and updates the UI appropriately depending on the the final state of the task. Here, everything just goes to the textComponent object to either show the message, confirm to the user the task was aborted, or display an exception message.

Both of these Runnable objects are then handed off to another helper class I will describe next.

The TaskController

This helper class completes the operational cycle: It starts the actual task in a sub-thread and then waits for it to finish. Once the sub-thread dies (either via completion, being interrupted, or due to results of an exception), the "doneHandler" is registered to be executed back in the event dispatch thread.

public class TaskController extends Thread {

   private Runnable task;
   private Runnable doneHandler;

   public TaskController(Runnable task,
                         Runnable doneHandler) {
      this.task = task;
      this.doneHandler = doneHandler;
   }

   public void run() {
      Thread taskThread =
         (task instanceof Thread ?
            (Thread)task :
            new Thread(task));

      taskThread.start();
      try {
         taskThread.join();
      }
      catch (InterruptedException e) {
         taskThread.interrupt();
      }
      SwingUtilities.invokeLater(doneHandler);
   }
}

About that conditional assignment operator: The sub-thread that is started is either a type cast of the Runnable (should the "task" in question have actually extended from the Thread class directly) or is a new Thread. This gives a little flexibility in how you actually implement the "task" code. Also, notice that TaskController is completely agnostic of what it is managing: It only cares to start a Thread to "do something," and then have the event dispatch thread invoke a Runnable to "do something else."

The above classes (TaskInfo, TaskController) and method pair in the AppController class form the backbone of this "long running task" management design. Going back to the original list, you've already accounted for several design points:

  • Uses the AppController for coordination.
  • Flexible invocation of task (button press or programmatic call).
  • Task runs in thread separate from event dispatch thread.
  • Task's many possible exit states are collected and displayed.

The remaining pieces are largely window dressing that you could probably add yourself, but I'll cover them for sake of completeness.

The Remaining Bits

Cancel Operation

Adding the ability to cancel the operation involves storing a reference to the TaskController as an instance variable in the AppController class, adding a public method called, say, handleCancelTaskAction, and having an extra button on the interface somewhere that calls this cancel method.

Note the source code for this demo takes a slightly different route to get to the same end point. It uses a standard JOptionPane (with the "OK_CANCEL_OPTION" button set) and registers a window listener to detect the JOptionPane's dialog has been closed. Then, depending on the result value of the JOptionPane, the current task is either allowed to continue running or is interrupted. (Admittedly, this was partly a result of laziness to quickly get something to illustrate the cancel operation for this article.)

Task Working Hint

Although you have a fairly static hint already in place that the task was started—the task button is disabled and the cancel button is enabled—a more dynamic display always adds a sense of confidence to the user. Even if you, the programmer, know this is a false sense of confidence, it is amazing how much more "patience" a user has when the UI shows some apparent sense of progress, or even just action in general. Perhaps the easiest way to accomplish this is to provide a JProgressBar somewhere on the interface. You do not need to know how long the task will take and compute a "percent complete" metric to make use of it, either: Simply call the setIndeterminate(boolean) method, in the same places you are toggling the state of the command buttons, to cause it to enter or leave an animated "bouncing box" display. (I remember writing a custom component to do this just slightly before JProgressBar exhibited this particular trick.) Of course, if you do have a way of communicating more information than this, you might want to include that as well.

Taking Longer than Expected

This, too, is fairly easy to accomplish. With a minor modification of the TaskController, you can use a variation of the join method that only blocks for a limited amount of time. When your code resumes, see whether the task sub-thread is still running. If it is, inform the user somehow (you might want a method in the AppController which results in a dialog box being shown, for example) and then re-join the sub-thread:

public void run() {
  ...
  try {
    taskThread.join(EXPECTED_TIME_MILLIS);
    if (taskThread.isAlive()) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          AppController.getInstance().showPleaseWaitDialog();
        }
      });
      taskThread.join();
    }
  }
  ...
}

If you adopt this, be careful to check for and programmatically close the dialog box when the task finishes. (Do this in the same area of the code as where you turn off the working hint and toggle the state of the various buttons.) Also, be careful to trigger the appearance of the dialog in the event dispatching thread. If you want (or need) to, you could put the "cancel" button on the resulting dialog box instead of on the main interface. (Although this demo does exactly that, I do not favor this approach, because it forces the user to wait at least as long as the expected timeout requires, and the user might be immediately sure they did something wrong.) Otherwise, just putting a simple message with an "Ok" button on the dialog serves its purpose: It lets the user know at least the UI portion of the application is alive and sympathetic to the user.

Closing Remarks

There you have it... a couple purpose-built helper classes, and a couple hooks added to a well-designed AppController class (you already have one, right?) and suddenly the management of all that spidery logic of a parallel task is contained. Even better, the UI stays responsive and the user is reassured that something is in fact happening. And when done correctly, the various concerns of the application are still relatively isolated from each other. Feel free to download the demo source code for this article to see everything strung together. (Remember the warning about needing 1.5 or higher to compile things as-is.) Applying this to your own tasks involves picking some sort of data object to wrap in the TaskInfo that will represent the task state (maybe a String will work, maybe you need your own custom object), and then writing your own Runnable implementation (or Thread subclass) that takes care to update the TaskInfo that is handed in.

It is worth noting that Java (as of 1.5) has a "concurrent" package. Although it has a wider range of utility than what this article has discussed, one interesting point is that there is a new Callable interface. It is conceptually similar to Runnable, except its sole implementation method call() returns a value (of generic type) and possibly throws a java.lang.Exception. (Runnable's run() does neither.) If you are interested in knowing whether all the above can be done using the java.util.concurrent classes, the answer is "possibly," but if you have existing Runnables or Thread subclasses, you'll likely face a bit of rewriting to take full advantage of everything the package provides: you'll probably end up using an ExecutorService to launch the Callable, and then dealing with a returned Future object, which you will still need to keep track of yourself. Still, it is a fairly interesting addition to the library that hasn't gotten a lot of press, so I thought I would mention it.

Download the Code

You can download the code that accompanies this article here.

About the Author

Rob Lybarger is a software guy (and a degreed aerospace engineer) in the Houston, TX area who has been using Java for nearly ten years. He has used various versions of Windows, various distributions of Linux, but loves Mac OS X. He likes exploring new techniques and new approaches to organizing and wiring applications, and loves to let the computer do as much work for him as possible.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date