Click here to Skip to main content
15,891,253 members
Articles / Programming Languages / XML

Persisting Selected and Expanded State of Data-Driven Treeviews

Rate me:
Please Sign up or sign in to vote.
3.33/5 (6 votes)
28 Mar 2008CPOL3 min read 29.1K   302   13  
Treeview navigation using keyed paths

Introduction

Reflector is an indispensable 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:

C#
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 its 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.

C#
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:

C#
[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:

C#
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:

C#
[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)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --