Expand Collapse Expand Issue with JTree Lazy loading
Asked Answered
R

2

6

I have implemented a tree using Lazy Loading. The 1st level nodes are created at the time of tree creation, where as the child nodes are created only when a user expands any particular node.

The data is coming from DB, and we fire a query to the database so as to populate the child nodes. Implemented TreeExpansionListener and using overridden implementation of treeExpanded method. On expansion, I remove all child nodes for the selected node, make a DB query and add the records as child to the selected node. Before adding any node to a tree, a dummy child is added to the node. Working with DefaultMutableTreeNode.

So far so good, it was working fine per expectation.

Problem 1 - As you, its a DB call per expansion, so if a node is collapsed and again expanded I will be doing a DB trip and processing again an again... The idea is to not to load the nodes fresh if already expanded...

Problem 2 - If I had to do a force refresh i.e. re load the tree and keep expansion state. This is in working state now... How can I achieve the same along with fix to Problem 1 above?

Radiogram answered 25/2, 2013 at 20:2 Comment(3)
If a node is collapsed, don't clear the data. When it is expanded again, check if the node already has the data...Cantle
I am overridding the treeCollapsed method and doing nothing in it. How can I stop data wipe out.Radiogram
On treeExpanded, check to see if the parent node contains children. If it does. Not reload anythingValer
S
9

As explained by MadProgrammer, here is a small demo example showing a JTree loading dynamically data as the tree expands on nodes (and with a nice loading bar as a bonus):

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.Transient;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;

public class TestJTree {

    public static class MyTreeNode extends DefaultMutableTreeNode {

        private boolean loaded = false;

        private int depth;

        private final int index;

        public MyTreeNode(int index, int depth) {
            this.index = index;
            this.depth = depth;
            add(new DefaultMutableTreeNode("Loading...", false));
            setAllowsChildren(true);
            setUserObject("Child " + index + " at level " + depth);
        }

        private void setChildren(List<MyTreeNode> children) {
            removeAllChildren();
            setAllowsChildren(children.size() > 0);
            for (MutableTreeNode node : children) {
                add(node);
            }
            loaded = true;
        }

        @Override
        public boolean isLeaf() {
            return false;
        }

        public void loadChildren(final DefaultTreeModel model, final PropertyChangeListener progressListener) {
            if (loaded) {
                return;
            }
            SwingWorker<List<MyTreeNode>, Void> worker = new SwingWorker<List<MyTreeNode>, Void>() {
                @Override
                protected List<MyTreeNode> doInBackground() throws Exception {
                    // Here access database if needed
                    setProgress(0);
                    List<MyTreeNode> children = new ArrayList<TestJTree.MyTreeNode>();
                    if (depth < 5) {
                        for (int i = 0; i < 5; i++) {
                            // Simulate DB access time
                            Thread.sleep(300);
                            children.add(new MyTreeNode(i + 1, depth + 1));
                            setProgress((i + 1) * 20);
                        }
                    } else {
                        Thread.sleep(1000);
                    }
                    setProgress(0);
                    return children;
                }

                @Override
                protected void done() {
                    try {
                        setChildren(get());
                        model.nodeStructureChanged(MyTreeNode.this);
                    } catch (Exception e) {
                        e.printStackTrace();
                        // Notify user of error.
                    }
                    super.done();
                }
            };
            if (progressListener != null) {
                worker.getPropertyChangeSupport().addPropertyChangeListener("progress", progressListener);
            }
            worker.execute();
        }

    }

    protected void initUI() {
        JFrame frame = new JFrame(TestJTree.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        MyTreeNode root = new MyTreeNode(1, 0);
        final DefaultTreeModel model = new DefaultTreeModel(root);
        final JProgressBar bar = new JProgressBar();
        final PropertyChangeListener progressListener = new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                bar.setValue((Integer) evt.getNewValue());
            }
        };
        JTree tree = new JTree() {
            @Override
            @Transient
            public Dimension getPreferredSize() {
                Dimension preferredSize = super.getPreferredSize();
                preferredSize.width = Math.max(400, preferredSize.width);
                preferredSize.height = Math.max(400, preferredSize.height);
                return preferredSize;
            }
        };
        tree.setShowsRootHandles(true);
        tree.addTreeWillExpandListener(new TreeWillExpandListener() {

            @Override
            public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
                TreePath path = event.getPath();
                if (path.getLastPathComponent() instanceof MyTreeNode) {
                    MyTreeNode node = (MyTreeNode) path.getLastPathComponent();
                    node.loadChildren(model, progressListener);
                }
            }

            @Override
            public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {

            }
        });
        tree.setModel(model);
        root.loadChildren(model, progressListener);
        frame.add(new JScrollPane(tree));
        frame.add(bar, BorderLayout.SOUTH);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
            UnsupportedLookAndFeelException {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new TestJTree().initUI();
            }
        });
    }
}
Startling answered 25/2, 2013 at 22:48 Comment(7)
Thank you for the demo.... I was able to resolve the issue using MadProgrammer comments....Radiogram
Can you please suggest how can I refresh this tree (populated using lazy loading) such that all expanded nodes are re fetched and the expansion state is retained. I am able to store the current expanded state but the problem is with expandRow() which expands the tree by row number, being dynamic it ends up opening some other row... expandPath is the right choice here but for some reason not working :(Radiogram
@Radiogram Use the TreePath for that: retrieve them all (provide the root TreePath as the parameter) using javax.swing.JTree.getExpandedDescendants(TreePath) and then set them back with javax.swing.JTree.expandPath(TreePath). Otherwise, try to only add/remove/update the nodes without modifying the other nodes.Startling
Hi, I was finally able to keep the nodes expanded, by mapping the row to the TreePath and then restoring it, and I ran into another issue. Upon expansion, the tree selection is shifted to the last expanded node, and the context (node on which the refresh was called) is not selected anymore. Even if I save the TreePath for the context node, before calling refresh, and then set selection path at the end of refresh , the context gets lost and is pointed to the last expanded node. Please suggestRadiogram
@Radiogram Please post another question. Comments are not there to put more question, only to clarify the original question/answers.Startling
posted a new Questio here.... #15209787Radiogram
@GuillaumePolet : why do you use a TreeWillExpandListener instead of a TreeExpansionListener ? Java doc says : "The tree-will-expand listener prevents a tree node from expanding or collapsing. To be notified just after an expansion or collapse occurs, you should use a tree expansion listener instead." and "A tree expansion listener detects when an expansion or collapse has already occured. In general, you should implement a tree expansion listener unless you need to prevent an expansion or collapse from ocurring."Mannino
V
1

Once a node has populated, simple don't repopulate it. On treeExpanded, simply check to see of the parent node has any children, if it does, don't reload anything.

You will need to supply the ability to refresh a parent node. I normally do this with a JPopupMenu.

Valer answered 25/2, 2013 at 20:30 Comment(1)
Thank you for the demo.... I was able to resolve the issue using MadProgrammer comments..Radiogram

© 2022 - 2024 — McMap. All rights reserved.