Thursday, 4 September 2008

Practical Example of Swing Worker

Please note that this page has moved to: http://www.javacreed.com/swing-worker-example/.

Java provides a neat way to carry out long lasting jobs without have to worry about threads and hanging the application. It's called SwingWorker. It's not the latest thing on Earth (released with Java 1.6) and you may have already read about it. What I never came across was a practical example of the swing worker.

Swing Worker

SwingWorker is an abstract class which hides the threading complexities from the developer. It's an excellent candidate for applications that are required to do tasks (such as retrieving information over the network/internet) which may take some time to finish. It's ideal to detach such tasks from the application and simply keep an eye on their progress. But before we hit the road and start working with the swing worker we have to see what "eye" are we going to put on our worker so to say.

The following example illustrates a simple empty worker that will return/evaluate to an integer when the given task is finished. It will inform the application (the "eye" thing) with what's happening using objects of type string, basically text messages.


import javax.swing.SwingWorker;
 
public class MyBlankWorker extends SwingWorker<Integer, String> {
 
  // Some code must go here
 
}

The string worker class provides two place holders (generics). The first one represents the type of object returned when the worker has finished working. The second one represents the type of information that the worker will use to inform (update) the application with its progress. The swing worker class also provides means to update the progress by means of an integer which has nothing to do with the two generics mentioned before.

Practical Example

Example: Let say we need to find the number of occurrences of a given word with in some documents. So we would be writing something like:

 
import java.io.File;
import javax.swing.SwingWorker;
 
public class SearchForWordWorker 
             extends SwingWorker<Integer, String> {
 
  private final String word;
  private final File[] documents;
 
  public SearchForWordWorker(String word, File[] documents){
    this.word = word;
    this.documents = documents;
  }
 
  @Override
  protected Integer doInBackground() throws Exception {
    int matches = 0;
    for(int i=0, size=documents.length; i<size; i++){
      // Update the status: the keep an eye on thing
      publish("Searching file: "+documents[i]);
 
      try {
        // Do the search stuff
        // Here you increment the variable matches
      } finally {
        // Close the current file
      }
 
      // update the progress
      setProgress( (i+1) * 100 / size);
    }
 
    return matches;
  }
}

The first thing that comes into mind is where is the text "Searching file: ..." is going? The swing worker class provides another method called process which accepts a list (of type string in our case) and used to process the published information (which can be an object of any kind). Overriding this method, allows us to take full control of this information.


  @Override
  protected void process(List<String> chunks){
    for(String message : chunks){
      System.out.println(message);
    }
  }

The above example is not much useful. We may need to update the status bar of an application or the text of the progress bar or a label sitting somewhere in the application. Since we may be monitoring this worker from various UI components, ideally we add a level of isolation between the worker and the UI components. In many examples, the worker was fed UI components as constructor parameters. I would go for interfaces instead to make the design pluggable when possible.

In other occasions a worker may be used to populate a table which information is coming from a slow source. In this case we may use the table model as one of the workers parameter and the array of objects representing the row. The following example makes use of the default table model as the design is simpler.

 
import java.util.List;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;
 
public class PopulateTableWorker 
             extends SwingWorker<DefaultTableModel, Object[]> {
 
  private final DefaultTableModel model;
 
  public PopulateTableWorker(DefaultTableModel model){
    this.model = model;
  }
 
  @Override
  protected DefaultTableModel doInBackground() throws Exception {
    // While there are more rows
    while(Math.random() < 0.5){
      // Get the row from the slow source
      Object[] row = {1, "Java"};
      Thread.sleep(2000);
 
      // Update the model with the new row
      publish(row);
    }
 
    return model;
  }
 
  @Override
  protected void process(List<Object[]> chunks){
    for(Object[] row : chunks){
      model.addRow(row);
    }
  }
}

Note that the table model interface does not support addition of new rows. Alternatively we can make use of the table model interface. The model must be able to add a new row when this is required as otherwise an exception may be thrown.


import java.util.List;
import javax.swing.SwingWorker;
import javax.swing.table.TableModel;
 
public class PopulateTableWorker 
             extends SwingWorker<TableModel, Object[]> {
 
  private final TableModel model;
  private int rowIndex = 0;
 
  public PopulateTableWorker(TableModel model){
    this.model = model;
  }
 
  @Override
  protected TableModel doInBackground() throws Exception {
    // While there are more rows
    while(Math.random() < 0.5){
      // Get the row from the slow source
      Object[] row = {1, "Java"};
      Thread.sleep(2000);
 
      // Update the model with the new row
      publish(row);
    }
 
    return model;
  }
 
  @Override
  protected void process(List<Object[]> chunks){
    for(Object[] row : chunks){
      for(int columnIndex=0, size=row.length; 
                                      columnIndex<size; 
                                      columnIndex++){
        model.setValueAt(row[columnIndex], rowIndex, columnIndex);
      }
    }
    rowIndex++;
  }
}

Back to our example, we can have an interface called Informable (or you can pick a name of your liking) with one method: messageChanged(String message), which will be invoked whenever some progress is made and published.

 
public interface Informable {
    void messageChanged(String message);
}

The worker also requires an instance of the interface as it will be used to publish the results through.

 
import java.io.File;
import java.util.List;
import javax.swing.SwingWorker;
 
public class SearchForWordWorker 
             extends SwingWorker<Integer, String> {
 
  private final String word;
  private final File[] documents;
  private final Informable informable;
 
  public SearchForWordWorker(String word,
                                File[] documents,
                                Informable informable){
    this.word = word;
    this.documents = documents;
    this.informable = informable;
  }
 
  @Override
  protected Integer doInBackground() throws Exception {
    int matches = 0;
    for(int i=0, size=documents.length; i<size; i++){
      // Update the status: the keep an eye on thing
      publish("Searching file: "+documents[i]);
 
      try {
        // Do the search stuff
        // Here you increment the variable matches
      } finally {
        // Close the current file
      }
 
      // update the progress
      setProgress( (i+1) * 100 / size);
    }
 
    return matches;
  }
 
  @Override
  protected void process(List<String> chunks){
    for(String message : chunks){
      informable.messageChanged(message);
    }
  }
}

The above worker can be easily plugged into the application as show in the following example. This application has three graphical components: a label acting as a title displaying the latest message published by the worker; a text area which displays all messages published by the worker; and a progress bar showing the progress made.

Demo: Swing Worker Application

The label and text area are governed by Informable interface. The progress bar is governed by the worker's progress property.

 
import java.awt.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
 
public class Application extends JFrame {
 
  // The UI Components
  private JLabel label;
  private JProgressBar progressBar;
  private JTextArea textArea;
 
  private void initComponents(){
    // The interface will update the text of the UI components
    Informable informable = new Informable(){
      @Override
      public void messageChanged(String message){
        label.setText(message);
        textArea.append(message + "\n");
      }
    };
 
    // The UI components
    label = new JLabel("");
    add(label, BorderLayout.NORTH);

    textArea = new JTextArea(5, 30);
    add(new JScrollPane(textArea), BorderLayout.CENTER);
 
    progressBar = new JProgressBar();
    progressBar.setStringPainted(true);
    add(progressBar, BorderLayout.SOUTH);
 
    //  The worker parameters
    String word = "hello";
    File[] documents = {new File("Application.java"),
                        new File("Informable.java"),
                        new File("SearchForWordWorker.java")};
 
    // The worker
    SearchForWordWorker worker =
           new SearchForWordWorker(word, documents, informable){
       // This method is invoked when the worker is finished
       // its task
      @Override
      protected void done(){
        try {
          // Get the number of matches. Note that the 
          // method get will throw any exception thrown 
          // during the execution of the worker.
          int matches = get();
          label.setText("Found: "+matches);
 
          textArea.append("Done\n");
          textArea.append("Matches Found: "+matches+"\n");

          progressBar.setVisible(false);
        }catch(Exception e){
          JOptionPane.showMessageDialog(Application.this, 
                        "Error", "Search", 
                        JOptionPane.ERROR_MESSAGE);
        }
      }
    };

    // A property listener used to update the progress bar
    PropertyChangeListener listener = 
                               new PropertyChangeListener(){
      public void propertyChange(PropertyChangeEvent event) {
        if ("progress".equals(event.getPropertyName())) {
          progressBar.setValue( (Integer)event.getNewValue() );
        }
      }
    };
    worker.addPropertyChangeListener(listener);
 
    // Start the worker. Note that control is 
    // returned immediately
    worker.execute();
  }
 
  // The main method
  public static void main(String[] args){
    SwingUtilities.invokeLater(new Runnable(){
      public void run(){
        Application app = new Application();
        app.initComponents();
        app.setDefaultCloseOperation(EXIT_ON_CLOSE);
        app.pack();
        app.setVisible(true);
      }
    });
  }
}

The above example builds the application and kicks off the worker. The worker updates the application through the Informable instance. While the worker is performing it task, the application can carry on doing other things (event listening and painting) without hanging.

Cancel the Worker

Can we cancel the task? Yes. The worker can be stopped or better cancelled. The worker provides a method called cancel which accepts a parameter of type boolean. This parameter determines whether or not the worker should be waked up should it be found sleeping.

 
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import javax.swing.*;
 
public class Application extends JFrame {
 
  private JLabel label;
  private JProgressBar progressBar;
  private JTextArea textArea;
  private JButton  button;
  private SearchForWordWorker worker;
 
  private void initComponents(){
    // The interface will update the text of the UI components
    Informable informable = new Informable(){
      @Override
      public void messageChanged(String message){
        label.setText(message);
        textArea.append(message + "\n");
      }
    };
 
    // The UI components
    label = new JLabel("");
    add(label, BorderLayout.NORTH);
 
    textArea = new JTextArea(5, 30);
    add(new JScrollPane(textArea), BorderLayout.CENTER);

    // The cancel button 
    button = new JButton("STOP");
    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
        // Cancel the worker and wake it up should it be sleeping
        worker.cancel(true);
      }
    });
    add(button, BorderLayout.EAST);
 
    progressBar = new JProgressBar();
    progressBar.setStringPainted(true);
        add(progressBar, BorderLayout.SOUTH);
 
    //  The worker parameters
    String word = "hello";
    File[] documents = {new File("Application.java"),
                        new File("Informable.java"),
                        new File("SearchForWordWorker.java")};
                        
    // The worker
    worker = new SearchForWordWorker(word, documents, informable){
      @Override
      protected void done(){
        try {
          int matches = get();
          label.setText("Found: "+matches);

          textArea.append("Done\n");
          textArea.append("Matches Found: "+matches+"\n");
 
          progressBar.setVisible(false);
        }catch(Exception e){
          JOptionPane.showMessageDialog(Application.this, 
                       "Error", "Search", 
                       JOptionPane.ERROR_MESSAGE);
        }
      }
    };

    // A property listener used to update the progress bar
    PropertyChangeListener listener = 
                               new PropertyChangeListener(){
      public void propertyChange(PropertyChangeEvent event) {
        if ("progress".equals(event.getPropertyName())) {
          progressBar.setValue( (Integer)event.getNewValue() );
        }
      }
    };
    worker.addPropertyChangeListener(listener);
    
    // Start the worker. Note that control is 
    // returned immediately
    worker.execute();
  }
 
  public static void main(String[] args){
    SwingUtilities.invokeLater(new Runnable(){
      public void run(){
        Application app = new Application();
        app.initComponents();
        app.setDefaultCloseOperation(EXIT_ON_CLOSE);
        app.pack();
        app.setVisible(true);
      }
    });
  }
}

This will cause the worker's get method to throw the exception CancellationException to indicate that the worker was forced cancellation.

Conclusion

One thing to take from this article is: when possible do not refer to UI components directly from the worker. The worker is better viewed as the subject of the observer pattern. Provide an interface which the worker will use to communicate with its owner (application).

23 comments:

  1. Thanks for the write up that was great. I'm currently using swingworker in my project. What is your opinion about threads in general in regards to time it takes to create a thread. I did some timing tests where a lot of time was spent in thread creation.

    ReplyDelete
  2. Welcome. I love to put some more articles but I never find the time...

    Threads are very dependent on the environment. Both OS and load of the system may effect the outcome of the threads. Yes, like any other process, threads consume resources - these don't come for free. But when properly used, these can speed up the performance especially in parallel processing.

    An example of threads is the GC. It employs some threads. GC will kick in depending on some parameters: such as thread priority and memory availability. Being a low priority thread, it may never be invoked if the system is very busy handling higher priority threads.

    Hope this help.

    ReplyDelete
  3. Hi Albert,

    Thanks for the example, and I would appreciate if you could answer my question. Is there any particular reason for using Thread.sleep(2000) in the doInBackground() method?

    The reason I am asking you this question is that I used a while loop in my doInBackground() to load vector and used publish(Vector) to work on the objects contained in the vector in the process(List) method. I used publish() in the if statement, to make sure that it gets to the publish() only if the vector is not empty. But for some strange reason, it always skipped the last valid vector and sent the empty vector to the process(). So I put some print statements in my while loop to see what's going on in there, and to my surprise it worked some of the times after that.

    To make a long story short, thinking it could be a timing issue, I removed all the debug statements and used Thread.sleep(100) to slow it
    down a little, and viola! it worked every time after that, but I still don't know why, and I am wondering whether it will work all the time in future or not?

    Mehta

    ReplyDelete
  4. Hi Mehta,

    My Thread.sleep() is there to simulate a delay. I have a simple example and without it, it would run so fast that you'll see nothing. That's why I added the sleep there.

    As for your problem; synchronization may be your answer. If you want, email me your code and I will try to have a look. My email is albertattard@gmail.com

    Albert

    ReplyDelete
  5. Hi, Albert,

    Thanks for your post. As for your SearchForWordWorker example, if it gets canceled, does it guarantee to close the files it opened? That is, is the finally{} block in doInBackground() guaranteed to run?

    Thanks!

    ReplyDelete
  6. I'm not 100% sure about that, but I think yes. The finally block will be invoked before the try/catch/finally block is out scoped.

    The cancel action cannot force a method out (You cannot pop a method from the stack). That is the doInBackground() method cannot be terminated by an external source until it is complete. You can check from within the doInBackground() method whether the process was cancelled or not and act accordingly.

    ReplyDelete
  7. Hello Albert,

    Thanks for your post. Other things work fine but when I try to do this inside the worker
    portList=CommPortIdentifier.getPortIdentifiers();

    I get a nasty error->
    java.util.concurrent.ExecutionException: java.lang.ExceptionInInitializerError

    Could you please give some pointers about what could be the problem.
    Thanks.

    ReplyDelete
  8. This can be a hard error to find. If my memory serves me well, this error occurs when an exception is thrown during static initialisation. It has nothing to do with the instances, there is a problem with the static parts of a particular class.

    ReplyDelete
  9. Okay. Ya i read about that exception means some problem with static parts.
    But the funny part is it works very well when I debug the code in netbeans by putting breakpoints. It just throws this error when I do I "run project".
    Additionaly when I remove that instantiation, the error goes off, so does that mean some static initialization happening inside the CommPortIdentifier class ?

    ReplyDelete
  10. Declare a variable of type CommPortIdentifier, without instantiating it and see what happens. Note that NetBeans may be providing additional resources which you're missing in the normal execution. Does this class use properties or resources (from files)?

    ReplyDelete
  11. Thanks. I actually declared
    CommPortIdentifier x, it ran without errors.

    After seeing http://www.control.lth.se/~kurstr/InternDator/java/commapi/javadocs/javax.comm.CommPortIdentifier.html , I realize that the CommPort class has no constructor. The getPortIdentifier is a static method and needs to be called with the class reference.

    So that is where the static parts are coming in maybe. Your suggestions on this?

    Could you suggest how do we achieve the task of using the CommPort class in a worker. I need a serial port scanner in my worker.

    Thanks.

    ReplyDelete
  12. It shouldn't be a problem whether you're using threads or not. If you want to confirm, use it with out a worker and confirm. But the exception you're getting is related with static contents. May be you're missing some property files.

    I never worked with that, but if you want you can send me the code at albertattard@gmail.com and I'll have a look (later on this week - as I'm quite busy with other stuff).

    ReplyDelete
  13. Thanks a lot for that. I ll try out till then, and also send you my code. See whenever you get time.

    Thanks!

    ReplyDelete
  14. I figured out it has nothing to do with the worker thread certainly. Its more to do with loading a native dll and exception is coming in that.
    thanks. I ll try to fix it.

    Regards

    ReplyDelete
  15. Hi Albert,
    I have a query.
    My application has a JTextArea which displays certain data when a button is clicked on the application. This JTextArea is contained in a JScrollPane. Lets say the JTextArea can accommodate 10 lines before the vertical scroll bar is invoked.
    What happens is, when i click the button, the data starts coming on the JTextArea, but once the data crosses 10 lines, the scroll bar doesn't get activated until the process invoked by the button press is over. Once that process is over, the scroll bar appears. Until then the data in the text area just gets filled in the invisible region.
    Lemme know if my explanation is not clear.
    As an example take this text box i am typing my comment in. Imagine the scroll bar not getting activated until i finish typing the whole comment.
    That's whats happening. Can i use SwingWorker to resolve this issue? If yes how?
    Thanks
    Akshay

    ReplyDelete
  16. From what I can understand (from your description), the task is being handled by the event thread and the scroll has to wait for your task to finish.

    You can use the SwingWorker to carry out the "long" time tasks for you. This will prevent your application from hanging.

    ReplyDelete
  17. "the scroll has to wait for your task to finish." - Correct !!!
    "You can use the SwingWorker to carry out the "long" time tasks for you." - Do you mean to say that i put the task that is performing the data input in the text area in the doInBackground() method? instead of putting the scroll action in it?

    ReplyDelete
  18. Hi,

    Yes. Thus your button should do the following:

    1) Create an instance of the worker and provide all required parameters
    2) Execute the worker


    Extra things:
    3) Disable the button programmatically
    4) Enable the button when the task is finished

    Hope this helps.

    ReplyDelete
  19. This is a Great Post! It's going onto my list! I've been searching for any answer and found it with your informable interface!

    Why hadn't i thought about it!

    ReplyDelete
  20. Hi Albert,

    I have a problem with the publish method that accumulate all the values in a List and then call only once the process method with the whole bunch of values.
    Any idea why?

    Thanks for publishing this post.
    Best, JCD

    ReplyDelete
    Replies
    1. Hi,

      I think this answers your question: http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html#publish(V...)

      This is done for a good reason, as otherwise you can flood the event thread with too many requests which can easily be collapsed into one event.

      Hope this helps

      Delete
  21. hi, very interesting, thank you. I tried to follow the SwingWorker in debug, and i found a curiosity that i didn't understand.. the size of the chunk list in process method is always 1.. why SwingWorker uses a list instead a single object?

    ReplyDelete
    Replies
    1. Have a look at the updated version of this article at: http://www.javacreed.com/swing-worker-example/

      Delete