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).
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.
Collapse
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:
Collapse
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 strings to/from a single string. The expanded state can now be stored in a simple string:
Collapse
[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.