
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 treeview
s 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 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.
public class ocTreeView : TreeView
{
private string _KeyedPathSeparator = "/";
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]];
if (i != keys.Length - 1)
{
tn.Expand();
nodes = tn.Nodes;
}
}
else
{
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)
{
if (last != -1 && Equals(expandedNodes[last], node.Parent))
{
expandedNodes[last] = node;
}
else
{
expandedNodes.Add(node);
last++;
}
node = node.FirstNode;
}
else if (node.NextNode != null)
{
node = node.NextNode;
}
else
{
node = node.NextVisibleNode;
}
}
expandedNodes.TrimExcess();
return expandedNodes;
}
The System.Configuration
namespace offers the CommaDelimitedStringCollection
and CommaDelimitedStringCollectionConverter
classes to convert multiple string
s 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