The Windows Forms TreeView
does not know how to bind to an IHierarchyData
instance, which isn't surprising given that the IHierarchyData
and related interfaces are intended for consumption by web controls (especially site maps).
However, it's really not too hard to build your own data binding class. This seemed like an interesting problem so I threw one together just for fun. I'll walk you through the inner workings.
First, create a basic Component class. Visual Studio will start you off with code like this:
public partial class TreeViewHierarchyBinding : Component
{
public TreeViewHierarchyBinding()
{
InitializeComponent();
}
public TreeViewHierarchyBinding(IContainer container)
{
container.Add(this);
InitializeComponent();
}
}
One obvious piece of "state" this component needs to have is a mapping from each TreeNode
to its IHierarchyData
. Now we can hack around this by throwing it in the TreeNode
's Tag
property, but let's aim to make this component as non-invasive as possible and keep track of its own state. Hence, we'll use a dictionary. Add this field to the class:
private Dictionary<TreeNode, IHierarchyData> nodeDictionary = new
Dictionary<TreeNode, IHierarchyData>();
Now, at a minimum, this component needs to know how to populate a specific parent TreeNode
of a TreeView
class from its correspondingly bound IHierarchyData
, so let's write that code next:
private void PopulateChildNodes(TreeNodeCollection parentCollection,
IHierarchicalEnumerable children)
{
parentCollection.Clear();
foreach (object child in children)
{
IHierarchyData childData = children.GetHierarchyData(child);
TreeNode childNode = new TreeNode(childData.ToString());
if (childData.HasChildren)
{
childNode.Nodes.Add("Dummy"); // Make expandable
}
nodeDictionary.Add(childNode, childData);
parentCollection.Add(childNode);
}
}
private void UpdateRootNodes(TreeView tv, IHierarchyData hierarchyData)
{
if (tv == null)
{
return;
}
tv.Nodes.Clear();
if (hierarchyData != null)
{
IHierarchicalEnumerable roots = hierarchyData.GetChildren();
PopulateChildNodes(tv.Nodes, roots);
}
}
This part should be pretty straightforward. The first method just populates a TreeNodeCollection
(i.e. the Nodes
property of a TreeNode
) with the hierarchy obtained from an IHierarchyData
instance, using the IHierarchyEnumerable
interface. The only really interesting things this method does are:
Adding a dummy node when the IHierarchyData
instance has children; this makes the "+" visible in the tree view, otherwise we wouldn't be able to expand any deeper; and
Adding the newly-added node to the dictionary with the IHierarchyData
instance it matches with.
The second method is even simpler, it does the initial "binding work", replacing whatever is in the root of the tree with our top-level IHierarchyData
instance.
The next thing our component needs to be able to do is hook the loading events from the TreeView
to perform lazy-loading. Here's the code to do that:
private void RegisterEvents(TreeView tv)
{
tv.BeforeExpand += TreeViewBeforeExpand;
}
private void UnregisterEvents(TreeView tv)
{
tv.BeforeExpand -= TreeViewBeforeExpand;
}
private void TreeViewBeforeExpand(object sender, TreeViewCancelEventArgs e)
{
if (e.Node.Checked)
{
return;
}
IHierarchyData hierarchyData;
if (nodeDictionary.TryGetValue(e.Node, out hierarchyData))
{
PopulateChildNodes(e.Node.Nodes, hierarchyData.GetChildren());
e.Node.Checked = true;
}
}
First two methods should be self-explanatory, and the third method is the actual lazy-loading code. We're cheating a little here, using the TreeNode.Checked
property to delineate whether or not the child nodes have already been loaded so we don't do any unnecessary reloads. I always do this when I implement lazy-loaded trees because, in my experience, I almost never use the TreeNode.Checked
property. However, if you do need to use this property for something else, you can either use a different property (like Tag
), create another dictionary to hold the expanded states, or modify the existing dictionary to hold a composite class (containing the IHierarchyData
as well as an Expanded
property). I'm keeping it simple for now.
The rest should already make sense to you if you've implemented lazy-loading in a tree before, so let's skip ahead. Really the only thing left to do at this point is implement some designer/user properties that will actually wire up the tree and data:
private IHierarchyData dataSource;
private TreeView treeView;
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IHierarchyData DataSource
{
get { return dataSource; }
set
{
if (value != dataSource)
{
dataSource = value;
nodeDictionary.Clear();
UpdateRootNodes(treeView, value);
}
}
}
[Category("Behavior")]
[DefaultValue(null)]
[Description("Specifies the TreeView that the hierarchy should be bound to.")]
public TreeView TreeView
{
get { return treeView; }
set
{
if (value != treeView)
{
if (treeView != null)
{
UnregisterEvents(treeView);
}
treeView = value;
nodeDictionary.Clear();
RegisterEvents(value);
UpdateRootNodes(treeView, dataSource);
}
}
}
Easy peasy. We've got a DataSource
property that accepts the root IHierarchyData
, and a TreeView
property which you'll be able to access from the designer. Again, simple stuff here, when the DataSource
property is updated, we just reset the lookup and repopulate the root. When the TreeView
property is updated we have to do a little more work, registering the events, making sure to unregister events from the old tree view, and doing all the same stuff we do when the data source changes.
That's really all there is to it! Open up the Windows Forms designer, drop a TreeView
, then drop a TreeViewHierarchyBinding
and set its TreeView
property to the tree view you just dropped. Finally, in your code somewhere (i.e. in the Form_Load
event), give it a data source:
private void Form1_Load(object sender, EventArgs e)
{
DirectoryInfo dir = new DirectoryInfo("C:\\");
treeViewHierarchyBinding1.DataSource = new FileSystemHierarchyData(dir);
}
(Note - this uses the example FileSystemHierarchyData
that's on the MSDN page for IHierarchyData. The example isn't very robust, it doesn't check for UnauthorizedAccessException
or anything, but it's good enough to demonstrate this).
And that's it. Run your app and watch it bind. You can now reuse the TreeViewHierarchyBinding
component anywhere - just drop it on a form, assign it a TreeView
, and give it an IHierarchyData
instance as a data source.
I've put the complete code on PasteBin if you want a copy-and-paste version.
Have fun!