Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IHierarchyData and IHierarchicalEnumerable in Winforms

Currently,I know how to do a lazy implementation of the loading procedure of the nodes in a treeview control, and read the related questions in stackoverflow, but I'm also reading about IHierarchyData and IHierarchicalEnumerable interfaces in asp.net (I didn't know to code asp.net) that allow to bind a collection to a treeview in order to display the items automatically.

It would like to know if I can do the same in winforms and C#. I think that the interfaces previous mentioned are not available in winforms.

Thanks.

like image 538
Jonathan Avatar asked Mar 13 '10 13:03

Jonathan


2 Answers

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:

  1. 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

  2. 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!

like image 166
Aaronaught Avatar answered Sep 30 '22 13:09

Aaronaught


The interfaces are available, but will require you to add a reference to System.Web.UI. (It might also require you to use the full .NET Framework redistributable rather than the Client Profile, although I'm not certain about that.)

The larger question is: Does the WinForms TreeView control automatically understand how to work with these interfaces? I believe the answer to that question is "No", but you would need to test/verify that.

like image 40
Scott Dorman Avatar answered Sep 30 '22 13:09

Scott Dorman