How can I refresh a JTree after adding some nodes to the underlying model?

Paralife picture Paralife · Dec 16, 2009 · Viewed 59.1k times · Source

First of all, let me say that I dont use the DefaultTreeModel. I implement my own TreeModel, so i cant use the DefaultXXX stuff. The problem is this: Through some addStuff() methods which my model defines I add nodes to the underlying data structure. I then notify listeners by calling treeNodesChanged() inside the addStuff() function (I Know there are treeNodesInserted methods but it is the same thing. It just notifies listeners with a different method). Now, one of the listeners is a static class in my main form and this listener can tell the JTree, which is also contained in my main form, to refresh itself. How do I tell the JTree to "reload" some or all of its nodes from the model?

UPDATE: Found this question that although not exactly the same, it gives the answer I want.

UPDATE 2: My problem was not how to notify the viewer (the JTree), but rather in what way should the jtree be reloaded after the notification from the model.

First of all let me say that the only way i know to refresh a tree to reflect underlying changes, is to call the updateUI(), or reuse the setModel() method. Essentially, my problem is this:

Suppose the TreeModelListener has just been notified (through the TreeModelListener API) that a change has occured in the model. Ok, what now?

I have this problem because the JTree does not implement TreeModelListener. So the listener, in my situation, is the JTree's container, or an internal class implementing the Listener, living under the same container as Jtree.

So suppose I am a TreeModelListener implementation, living happily in a JForm with my brother JTree. Suddenly my method treeNodesInserted(TreeModelEvent evt) is called. What do I do now? If i call Jtree.updateUI() from inside me, then the model's listeners List throws ConcurrentModification Exception. Can I call something else other than updateUI?

I tried a number of things, but only updateUI refreshed the JTree. So I did it outside of the listener. From the JForm, I just call the model's method that alters the undrlying structure, and then i call updateUI. No TreeModelListener gets used.

UPDATE3: I found out that there are implicit TreeModelListeners registered. In my model's addTreeModelListener(TreeModelListener listener) implementation i put a debug system.out line:

System.out.println("listener added: " + listener.getClass().getCanonicalName());

and I saw this debug output just when I executed jTree.setModel(model):

listener added: javax.swing.JTree.TreeModelHandler

listener added: javax.swing.plaf.basic.BasicTreeUI.Handler

The ConcurrentModificationException is caused because a call to jtree.updateUI() re registers the listener (only the plaf, not both) so it is thrown when i call updateUI inside a listener notification loop. The only way now to refresh the tree is do it outside of TreeModelListener. Any comments or ideas for a better solution? Am I missing something?

Answer

Dmitry Frank picture Dmitry Frank · Jan 18, 2013

I faced the same "problem": calling treeNodesInserted() did not cause my JTree to update its contents.

But the problem was in other place: I used wrong constructor for TreeModelEvent. I thought that I can create TreeModelEvent for treeNodesInserted() like that:

//-- Wrong!!
TreePath path_to_inserted_item = /*....*/ ;
TreeModelEvent tme = new TreeModelEvent(my_source, path_to_inserted_item);

This doesn't work.

As stated in TreeModelEvent docs, this constructor is only needed for treeStructureChanged(). But for treeNodesInserted(), treeNodesRemoved(), treeNodesChanged() we should use another constructor:

TreePath path_to_parent_of_inserted_items = /*....*/ ;
int[] indices_of_inserted_items = /*....*/ ;
Object[] inserted_items = /*....*/ ;
TreeModelEvent tme = new TreeModelEvent(
      my_source,
      path_to_parent_of_inserted_items,
      indices_of_inserted_items,
      inserted_items
   );

This code works, and JTree updates its contents properly.


UPD: Actually, docs are unclear about using these TreeModelEvents, and especially with JTree, so, I want to tell about some questions that came to me when I tried to figure out how to deal with all this stuff.

Firstly, as Paralife noted it his comment, cases when nodes are inserted/changed/removed, or when tree structure is changed, aren't orthogonal. So,

Question #1: when should we use treeNodesInserted()/Changed()/Removed(), and when treeStructureChanged()?

Answer: treeNodesInserted()/Changed()/Removed() can be used if only all the affected nodes have the same parent. Otherwise you may make several calls to these methods, or just call treeStructureChanged() once (and pass the root node of affected nodes to it). So, treeStructureChanged() is a kind of universal way, while treeNodesInserted()/Changed()/Removed() are more specific.

Question #2: As far as treeStructureChanged() is a universal way, why do I need to deal with these treeNodesInserted()/Changed()/Removed()? Just call to treeStructureChanged() seems to be easier.

Answer: If you use JTree to display contents of your tree, then the following thing might be a surprize for you (as it was for me) : when you call treeStructureChanged(), then JTree doesn't keep expand state of sub-nodes! Consider the example, here's contents of our JTree now:

[A]
 |-[B]
 |-[C]
 |  |-[E]
 |  |  |-[G]
 |  |  |-[H]
 |  |-[F]
 |     |-[I]
 |     |-[J]
 |     |-[K]
 |-[D]

Then you make some changes to C (say, rename it to C2), and you call treeStructureChanged() for that:

  myTreeModel.treeStructureChanged(
        new TreeModelEvent(
           this,
           new Object[] { myNodeA, myNodeC } // Path to changed node
           )
        );

Then, nodes E and F will be collapsed! And your JTree will look like that:

[A]
 |-[B]
 |-[C2]
 |  +-[E]
 |  +-[F]
 |-[D]

To avoid that, you should use treeNodesChanged(), like that:

  myTreeModel.treeNodesChanged(
        new TreeModelEvent(
           this,
           new Object[] { myNodeA }, // Path to the _parent_ of changed item
           new int[] { 1 },          // Indexes of changed nodes
           new Object[] { myNodeC }, // Objects represents changed nodes
                                     //    (Note: old ones!!! 
                                     //     I.e. not "C2", but "C",
                                     //     in this example)
           )
        );

Then, expanding state will be kept.


I hope this post will be useful for somebody.