Click here to Skip to main content
Click here to Skip to main content

Persisting Selected and Expanded State of Data-Driven Treeviews

By , 28 Mar 2008
Rate this:
Please Sign up or sign in to vote.

Introduction

Reflector is an indispensible tool for .NET programming. Just for the sake of argument, I consider it here as an example of dumb software. Why doesn't it open up with the type, that I inspected yesterday? Why doesn't it expand the nodes, that I opened yesterday? It offers me Back/Forward buttons, but where is the menu to quickly select my type?

This article presents a simple way to restore (multi-)selected and expanded state of load-on-demand treeviews between sessions. The state is persisted in two string properties in Application Settings. The sole prerequisite is that the nodes must be keyed (setting the TreeNode.Name property).

The download includes two different implementations with equal functionality:

  • As a pair of derived TreeView and TreeNode classes (code samples used below)
  • As a static helper class, easily insertable in existing designs (see demo project)
  • Keyed Paths

    Central here is the concept of node identification by a keyed path. Analogous to the TreeNode.FullPath property, which is a delimited string made up of node labels, the KeyedPath property is composed from node keys.

    Keyed path has the advantage of being shorter than full path and is usable in advanced scenarios (i.e., offline editing of node labels and a later online batch update of the underlying database).

    KeyedPath property as implemented in a derived TreeNode class:

    public class ocTreeNode : TreeNode
    {
        public string KeyedPath
        {
            get
            {
                string[] keys = new string[Level + 1];
                TreeNode node = this;
    
                while (node != null)
                {
                    keys[node.Level] = node.Name;
                    node = node.Parent;
                }
                return string.Join("/", keys);
            }
        }
    }

    TreeView Code

    At first three helper functions processing keyed paths. To open a node, the path is split into the keys and it's parent nodes are expanded, thereby invoking the application specific BeforeExpand event handler, which adds appropriate nodes on demand. Note that the event handler itself should handle exceptions gracefully, as BeginUpdate/EndUpdate are called here without Try..Finally logic.

    public class ocTreeView : TreeView
    {
        private string _KeyedPathSeparator = "/";    /* not comma ! */
    
        private string getKeyedPath(TreeNode node)
        {
            ocTreeNode tn = node as ocTreeNode;
            if (tn == null || tn.TreeView != this) return null;
            return tn.KeyedPath;
        }
    
        private TreeNode setKeyedPath(string keyedPath)
        {
            string[] keys = keyedPath.Split(_KeyedPathSeparator.ToCharArray());
    
            TreeNodeCollection nodes = this.Nodes;
            TreeNode tn = null;
    
            BeginUpdate();
    
            for (int i = 0; i < keys.Length; i++)
            {
                if (nodes.ContainsKey(keys[i]))
                {
                    tn = nodes[keys[i]];
    
                    // expand, unless is last node in path
                    if (i != keys.Length - 1)
                    {
                        tn.Expand();
                        nodes = tn.Nodes;
                    }
                }
                else
                {
                    // return last node only for full resolved paths
                    tn = null;
                    break;
                }
            }
    
            EndUpdate();
            return tn;
        }
        
        private IEnumerable setKeyedPath(ICollection keyedPaths)
        {
            foreach (string path in keyedPaths)
            {
                TreeNode tn = setKeyedPath(path);
                if (tn != null)
                {
                    yield return tn;
                }
            }
            yield break;
        }
    }

    A single node can now be selected by a string:

    [Browsable(false), DefaultValue(null)]
    public string SelectedPath
    {
        get { return getKeyedPath(SelectedNode); }
        set { SelectedNode = setKeyedPath(value); }
    }
    

    A Depth First Traversal (DFT) algorithm returns a list of the deepest expanded nodes:

    private List<TreeNode> getExpandedNodes()
    {
        List<TreeNode> expandedNodes = new List<TreeNode>(10);
        TreeNode node = this.Nodes[0];
        int last = -1;
    
        while (node != null)
        {
            if (node.IsExpanded)
            {
                // is previous stored node a parent of current node ?
                if (last != -1 && Equals(expandedNodes[last], node.Parent))
                {
                    // replace previous element with this deeper path
                    expandedNodes[last] = node;
                }
                else
                {
                    // add new path
                    expandedNodes.Add(node);
                    last++;
                }
    
                // first child
                node = node.FirstNode;
            }
            else if (node.NextNode != null)
            {
                // next sibling
                node = node.NextNode;
            }
            else
            {
                // next node (visibility irrelevant, stupid naming/documentation)
                node = node.NextVisibleNode;
            }
        }
    
        expandedNodes.TrimExcess();
        return expandedNodes;
    }

    The System.Configuration namespace offers the CommaDelimitedStringCollection and CommaDelimitedStringCollectionConverter classes to convert multiple strings to/from a single string. The expanded state can now be stored in a simple string:

    [Browsable(false), DefaultValue(null)]
    public string ExpandedPaths
    {
        get
        {
            if (this.Nodes.Count == 0) return null;
            CommaDelimitedStringCollection expandedPaths =
                new CommaDelimitedStringCollection();
    
            foreach (TreeNode tn in getExpandedNodes())
            {
                expandedPaths.Add(getKeyedPath(tn));
            }
            return new CommaDelimitedStringCollectionConverter().ConvertToInvariantString(
                expandedPaths);
        }
        set
        {
            if (!string.IsNullOrEmpty(value))
            {
                CommaDelimitedStringCollection expandedPaths = (
                    CommaDelimitedStringCollection)
                    new CommaDelimitedStringCollectionConverter().ConvertFromInvariantString(
                      value);
    
                BeginUpdate();
                foreach (TreeNode node in setKeyedPath(expandedPaths))
                {
                    node.Expand();
                }
                EndUpdate();
            }
        }
    }

    For a multi-selectable treeview a SelectedPaths property can be implemented likewise.

    Using the Code

    The two provided demo projects show the usage of ocTreeView/ocTreeNode and static KeyedPaths classes in a simple load-on-demand scenario. My History<T> class uses here keyed paths for node identification as well.

    As this article generally promotes enhancing user pleasure, the form's desktop bounds are persisted. The responsible FormPlacement class allows to restore the state of multiple forms using a single string key in Application Settings.

    Another feature is the static EnsureVisibleOptimal helper method: It ensures that the node is visible and located in the upper area of the treeview, which I find more appealing than the default TreeView.EnsureVisible behaviour.

    Points of Interest

    When the application allows grouping data by different categories, all previous stored keyed paths become invalid on a change in category. You can still restore history and selected/expanded state, if you recalculate the old paths based on the new category, before refreshing treeview contents.

    History

    • February 2006: coded
    • March 2008: published

    License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

    About the Author

    OrlandoCurioso

    Germany Germany
    No Biography provided

    Comments and Discussions

     
    -- There are no messages in this forum --
    | Advertise | Privacy | Mobile
    Web02 | 2.8.140421.2 | Last Updated 28 Mar 2008
    Article Copyright 2008 by OrlandoCurioso
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Use
    Layout: fixed | fluid