Tips & Tricks
In this section we will discuss some tip and trick that applies when using threads in Swing Applications.
The examples are coded with the Foxtrot API, but are valid also for other solutions such as the SwingWorker.
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 (SwingWorker or Foxtrot), care must be taken
to interact with Swing Models, since code working well against a plain Swing solution (ie 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:
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;
}
}
}
|
Legend |
Main Thread |
Event Dispatch Thread |
Foxtrot Worker Thread |
|
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 JButton s. 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 JComboBox es 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 the
Foxtrot API (this problem does not appear when using the SwingWorker).
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 more than a Foxtrot's.
|