Foxtrot

SourceForge.net Logo Java.net Logo

Overview

The Problem

Asynchronous Solution

Synchronous Solutions

Foxtrot

Foxtrot & Swing

Tips & Tricks

In this section we will discuss some tip and trick that applies when using threads in Swing Applications.

Topics are:

  • Working correctly with Swing Models
  • Working correctly with Custom Event Emitters
  • Working correctly with JComboBox

Working correctly with Swing Models

When threads are used in a Swing Application, the issue of concurrent access to shared data structures is always present. No matter if the chosen solution is asynchronous or synchronous, care must be taken to interact with Swing Models, since code working well against a plain Swing solution (i.e. without use of threads), may not work as well when using threads.

Let's make an example: suppose you have a JTable, and you use as a model a subclass of AbstractTableModel that you feeded with your data. Suppose also that the user can change the content of a cell by editing it, but the operation to validate the new input takes time.
Using plain Swing programming, this code looks similar to this:

Legend
Main Thread
Event Dispatch Thread
Foxtrot Worker Thread

public class MyModel extends AbstractTableModel
{
   private Object[][] m_data;
   ...
   public void setValueAt(Object value, int row, int column)
   {
      if (isValid(value))
      {
         m_data[row][column] = value;
      }
   }
}

If isValid(Object value) is fast, no problem; otherwise the user has the GUI frozen and no feedback on what is going on.
Thus you may decide to use Foxtrot, and you convert the old code to this:


public class MyModel extends AbstractTableModel
{
   private Object[][] m_data;
   ...
   public void getValueAt(int row, int col)
   {
      return m_data[row][col];
   }
   public void setValueAt(final Object value, final int row, final int column)
   {
      Worker.post(new Job()
      {
         public Object run()
         {
            if (isValid(value))
            {
               m_data[row][column] = value;
            }
            return null;
         }
      });
   }
}

The above is just plain wrong.
It is wrong because the data member m_data is accessed from two threads: from the Foxtrot Worker Thread (since it is modified inside Job.run()) and from the AWT Event Dispatch Thread (since any repaint event that occurs will call getValueAt(int row, int col)).

Avoid the temptation to modify anything from inside Job.run(). It should just take data from outside, perform some heavy operation and return the result of the operation.
The pattern to follow in the implementation of Job.run() is Compute and Return, see example below.


public class MyModel extends AbstractTableModel
{
   private Object[][] m_data;
   ...
   public void getValueAt(int row, int col)
   {
      return m_data[row][col];
   }
   public void setValueAt(final Object value, int row, int column)
   {
      Boolean isValid = (Boolean)Worker.post(new Job()
      {
         public Object run()
         {
            // Compute and Return
            return isValid(value);
         }
      });

      if (isValid.booleanValue())
      {
         m_data[row][column] = value;
      }
   }
}

Note how only the heavy operation is isolated inside Job.run(), while modifications to the data member m_data now happen in the AWT Event Dispatch Thread, thus following the Swing Programming Rules and avoiding concurrent read/write access to it.

Working correctly with Custom Event Emitters

Sometimes you code your application with the use of custom data structures that are able to notify listeners upon some state change, following the well-known Subject-Observer pattern.
When threads are used in such a Swing Application, you have to be careful about which thread will actually notify the listeners.

Let's make an example: suppose you created a custom data structure that emits event when its state changes, and suppose that state change is triggered by JButtons. In plain Swing programming, the code may be similar to this:


public class Machine
{
   private ArrayList m_listeners;

   public void addListener(Listener l) {...}
   public void removeListener(Listener l) {...}

   public void start()
   {
      // Starts the machine
      ...
      MachineEvent event = new MachineEvent("Running");
      notifyListeners(event);
   }

   private void notifyListeners(MachineEvent e)
   {
      for (Iterator i = m_listeners.iterator(); i.hasNext();)
      {
         Listener listener = (Listener)i.next();
         listener.stateChanged(e);
      }
   }
}

// Somewhere else in your application...

final Machine machine = new Machine();

final JLabel statusLabel = new JLabel();

machine.addListener(new Listener()
{
   public void stateChanged(MachineEvent e)
   {
      statusLabel.setText(e.getStatus());
   }
});

JButton button = new JButton("Start Machine");
button.addActionListener(new ActionListener()
{
   public void actionPerformed(ActionEvent e)
   {
      machine.start();
   }
});

The Machine class is a JavaBean, and does not deal with Swing code.
While you implement Machine.start() you discover that the process of starting a Machine is a long one, and decide to not freeze the GUI after pressing the button.
With the Foxtrot API, a small change in the listener will do the job:


button.addActionListener(new ActionListener()
{
   public void actionPerformed(ActionEvent e)
   {
      Worker.post(new Job()
      {
         pulic Object run()
         {
            machine.start();
            return null;
         }
      });
   }
});

Unfortunately, the above is plain wrong.
It is wrong because now Machine.start() is called in the Foxtrot Worker Thread, and so is Machine.notifyListeners() and finally also any registered listener have the Listener.stateChanged() called in the Foxtrot Worker Thread.
In the example above, the statusLabel's text is thus changed in the Foxtrot Worker Thread, violating the Swing Programming Rules.

Below you can find one solution to this problem (my favorite), that fixes the Machine.notifyListeners() implementation using SwingUtilities.invokeAndWait():


public class Machine
{
   ...
   private void notifyListeners(final MachineEvent e)
   {
      if (SwingUtilities.isEventDispatchThread())
      {
         notify(e);
      }
      else
      {
         SwingUtilities.invokeAndWait(new Runnable()
         {
            public void run()
            {
               notify(e);
            }
         });
      }
   }

   private void notify(MachineEvent e)
   {
      for (Iterator i = m_listeners.iterator(); i.hasNext();)
      {
         Listener listener = (Listener)i.next();
         listener.stateChanged(e);
      }
   }
}

The use of SwingUtilities.invokeAndWait() preserves the semantic of the Machine.notifyListeners() method, that returns when all the listeners have been notified. Using SwingUtilities.invokeLater() causes this method to return immediately, normally before listeners have been notified, breaking the semantic.

Working correctly with JComboBox

JComboBox shows a non-usual behavior with respect to item selection when compared, for example, with JMenu: both show a JPopup with a list of items to be selected by the user, but after selecting an item in JMenu the JPopup disappears immediately, while in JComboBox it remains shown until all listeners are processed.

Swing Applications that contain JComboBoxes that have to perform heavy operations when an item is selected will suffer of the "JPopup shown problem" when using plain Swing programming (in this case the GUI is also frozen) and when using synchronous APIs (this problem does not appear when using asynchronous APIs).

However this problem is easily solved by asking JComboBox to explicitely close the JPopup, as the example below shows:


final JComboBox combo = new JComboBox(...);
combo.addActionListener(new ActionListener()
{
   public void actionPerformed(ActionEvent e)
   {
      try
      {
          // Explicitely close the popup
         combo.setPopupVisible(false);

         // Heavy operation
         Worker.post(new Task()
         {
            public Object run() throws InterruptedException
            {
               Thread.sleep(5000);
               return null;
            }
         });
      }
      catch (InterruptedException x)
      {
         x.printStackTrace();
      }
      catch (RuntimeException x)
      {
         throw x;
      }
      catch (Exception ignored) {}
   }
});

This is the only small anomaly I've found so far using Swing with the Foxtrot API, and I tend to think it's more a Swing anomaly than Foxtrot's.