Click here to Skip to main content
13,147,540 members (29,974 online)
Click here to Skip to main content
Add your own
alternative version

Stats

21.5K views
269 downloads
12 bookmarked
Posted 28 Mar 2008

Persisting Selected and Expanded State of Data-Driven Treeviews

, 28 Mar 2008
Rate this:
Please Sign up or sign in to vote.
Treeview navigation using keyed paths.

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)

    Share

    About the Author

    OrlandoCurioso
    Germany Germany
    No Biography provided

    You may also be interested in...

    Comments and Discussions

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