Weird Swing Bug

We ran into a weird issue with Swing today at work. The small class below reproduces this.

 1   import javax.swing.*; 
 2   import javax.swing.event.TreeModelEvent; 
 3   import javax.swing.event.TreeModelListener; 
 4   import javax.swing.tree.DefaultMutableTreeNode; 
 5   import javax.swing.tree.DefaultTreeModel; 
 6     
 7   public class Blah extends JFrame implements TreeModelListener { 
 8     
 9       private JTree tree; 
10    
11       public Blah() { 
12           setSize(150, 150); 
13           setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
14           DefaultMutableTreeNode root = new DefaultMutableTreeNode("root"); 
15           DefaultTreeModel model = new DefaultTreeModel(root); 
16           tree = new JTree(); 
17           getContentPane().add(tree); 
18           tree.setModel(model); 
19           model.addTreeModelListener(this); 
20           DefaultMutableTreeNode firstNode = new DefaultMutableTreeNode("level 1"); 
21           model.insertNodeInto(firstNode, root, 0); 
22           DefaultMutableTreeNode childOfFirstNode = new DefaultMutableTreeNode("level 2"); 
23           model.insertNodeInto(childOfFirstNode, firstNode, 0); 
24           model.insertNodeInto(new DefaultMutableTreeNode("another level 1"), root, 1); 
25       } 
26    
27       public void treeNodesChanged(TreeModelEvent e) { } 
28    
29       public void treeNodesInserted(final TreeModelEvent e) { 
30           // auto-expand newly added nodes 
31           SwingUtilities.invokeLater(new Runnable() { 
32               public void run() { 
33                   tree.expandPath(e.getTreePath()); 
34               } 
35           }); 
36       } 
37    
38       public void treeNodesRemoved(TreeModelEvent e) { } 
39    
40       public void treeStructureChanged(TreeModelEvent e) { } 
41    
42       public static void main(String[] args) throws Exception { 
43           Blah b = new Blah(); 
44           b.show(); 
45       } 
46   }
If you run this code, the resulting window may look something like this:

Notice how the last node (added on line 24) has fallen off the screen...

Now without the treeNodesInserted() implementation, the tree would've built just fine, but we needed the tree to auto-expand new nodes (which in our real code weren't added in the constructor of course).

The solution? Swap lines 18 and 19, adding the TreeModelListener before you set the model on the JTree, and everything looks as it should:

Is this a bug, or is it documented somewhere that listeners should not be added after the model has been set or something?

TrackBack URL for this entry: http://www.hutteman.com/scgi-bin/mt/mt-tb.cgi/152
Comments

It's not a Bug.

This is what is happening:
1) your listener is notified first because it is the listener added last
2) your runnable is invoked immediately since the event dispatch thread has currently nothing to do (this might be platform dependant)
3) you are expanding a path that is not known by your tree, corrupting its state
4) now the remaing listeners are notified, i.e. the listener the tree has on your model
4) your tree gets the notification of a new node - but too late

There are several failures in your program:
- your program should not depend on the order in which listeners are notified
- the swing threading rule claims 'Access to a (visible) Swing component has to occur in the event dispatch thread (EDT)'. Yes, your tree isn't visible yet, but still you're stepping with two threads through it: the main thread and the EDT (because you're using invokeLater());

Solution:


public Blah() {
setSize(150, 150);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final DefaultMutableTreeNode root = new DefaultMutableTreeNode("root");
final DefaultTreeModel model = new DefaultTreeModel(root);
tree = new JTree();
getContentPane().add(tree);
tree.setModel(model);
model.addTreeModelListener(this);

SwingUtilities.invokeLater(new Runnable() {
public void run() {
DefaultMutableTreeNode firstNode = new DefaultMutableTreeNode("level 1");
model.insertNodeInto(firstNode, root, 0);
DefaultMutableTreeNode childOfFirstNode = new DefaultMutableTreeNode("level 2");
model.insertNodeInto(childOfFirstNode, firstNode, 0);
model.insertNodeInto(new DefaultMutableTreeNode("another level 1"), root, 1);
}
});

Posted by svenmeier at June 23, 2004 10:17 AM

So all access to the Model always needs to occur on the EDT as well? I was under the impression that you should be able to update the model on your own thread as long as any operations on the Swing component itself occurred in the EDT. and it seems like you can indeed update the model in a non-EDT, as long as any of your handlers that update the GUI are attached prior to the call to setModel().

this kind of stuff reminds me why I prefer the server-side :-)

btw For what it's worth, even though this small sample works by bracketing the model-update code in a SwingUtilities.invokeLater(), we did actually try this approach on our real code as well (before we found the solution of simply swapping those two lines) and still ran into these weird swing behaviors. I guess it's possible we may have missed some though...

thanks for your help,

Luke

Posted by Luke Hutteman at June 23, 2004 11:16 AM

Probably completely irrelevant, but you should declare either the entire class or all the TreeModelListener methods as final, as they are being called from the constructor. (If you extend the class and override one of those methods, it will fall in a big heap). You'd probably be better off using an anonymous inner class to deal with the event handling.

Posted by Daniel Sheppard at June 23, 2004 8:43 PM

Daniel: thank you for showing off your knowledge of one of Java's little pitfalls, but this class isn't actual production code; it's just code to reproduce the problem we were experiencing. It will therefore not be extended, enhanced, inherited or anything like that.

Posted by Luke Hutteman at June 24, 2004 12:12 AM

Greetings...
I am a student that begins their programming with java and I am using the component JTree for a project, however I want to extend their use to manipulate on the JTree the content of a chart of a database it specifies (elements with a structure type tree).

When I use the insertInto in this part I want to add the you go that it identifies to the registration of my chart and specifically the node that leaves to add the JTree, their call order and attaché is not sequential that is to say of 1, 2, 3, 100....., because I use things like recursivity and in a group of elements, a registration father (node) he can have among his children to 10 nodes that integrate it with non sequential ids (30, 40, 15, 90......).

This part is fundamental to begin or undo the use of this component, because I want to implement it in Java Servlets.

Another doubt: I dont know like to incrust directly in a Servlets the component JTree as if we were using the input (it sentences html).


With the you go (Non index), I could help to identify in a Jtree their easy elements and quickly and in a database their relationship and access in a transparent way.


If somebody can help me, I will thank them

NOTE: My English is not a very good one.

Posted by Javier Rebolledo at October 3, 2004 5:14 PM
This discussion has been closed. If you wish to contact me about this post, you can do so by email.