Introduction

Object oriented programming is an ancient art. When you hear about inversion of control or dependency injection, you should know that these are new names for concepts that have been around for a long time. Today we are going to explore one such ancient technique: a pattern for populating a tree control using the inversion of control; from the early days of computing when resources were sparse.

Let us create a Windows Forms derive Tree View to visualise a hierarchy in an elegant manner. When I say elegant I mean:
- minimal memory signature,
- making control reusable,
- lazy loading data as the user drills down the tree, and
- allow various data objects to be attached to tree nodes.

There is really only one trick behind it: populate top level nodes and check if they have any children. If they do - insert a dummy node under them so that + appears left of the tree node. This will enable tree node expansion. Detect it and populate sub-tree using exactly the same technique.

In 2002 I have published an article on modelling hierarchies using SQL based DBMS. If the data that you visualise uses such storage there are some good tricks there to consider.

The Tree Node

First we need to define a tree node data structure. This is basic building block of our tree structure. It is independent of presentation method. You will be surprised to see that the class has no reference to its parent or its children. Because we are using lazy loading these references are resolved when needed. The code for resolving them is separated to the hierarchy feed class.

public class Node
{
    public Node(string unique, string name, bool hasChildren)
    { Unique = unique; Name = name; HasChildren = hasChildren; }

    public string Unique { get; set; }
    public string Name { get; set; }
    public bool HasChildren { get; set; }
}

The three fields are:
Unique This is the identifier (or the key) of this particular node.
Name This is human readable name for the node.
HasChildren True if node has children and can be expanded.

The Feed

We want the user to be able to use our control for visualizing any hierarchy with minimal effort. Here is a minimalistic tree feed interface. All you really need to implement is a function to query children of a node (or root nodes if parent is not given).

public interface IHierarchyFeed
{
    List<Node> QueryChildren(Node parent);
}

For better understanding of how this feed works let us observe an implementation of this interface for enumerating files and folders in the file system.

public class FileSysHierarchyFeed : IHierarchyFeed
{
    private string _rootPath;
    private string _filter;

    public FileSysHierarchyFeed(string rootPath, string filter)
    {
        _rootPath = rootPath;
        _filter = filter;
    }

    public List<Node> QueryChildren(Node parent)
    {
        List<Node> children = new List<Node>();
        if (parent == null)
            AddFilesAndFolders(_rootPath, children);
        else
            AddFilesAndFolders(parent.Unique, children);
        return children;
    }

    #pragma warning disable 168 // Ex variable is never used.
    private void AddFilesAndFolders(string path, List children) {
        foreach (string fso in Directory.EnumerateDirectories(path,"*.*",SearchOption.TopDirectoryOnly)) {
            string unique=Path.Combine(path,fso);
            try { children.Add(new Node(unique, Path.GetFileName(fso), Directory.EnumerateFileSystemEntries(unique).Count() > 0)); }
            catch (UnauthorizedAccessException ex) { } // Ignore unauthorized access violations.
        }
        foreach(string file in Directory.EnumerateFiles(path,_filter)) children.Add(new Node(Path.Combine(path,file),Path.GetFileName(file),false));
    }
}

Simple, isn’t it? You initialize the feed object with root path and filter, for example c:\ and *.*. When you call QueryChildren with null parameter it returns files and folders from the root path. It uses entire path as the node unique. When calling QueryChildren on a particular node it extracts path from the unique and uses it to enumerate files and folders under this folder.

You can easily write feeder class for database items, remote items, etc.

The TreeView Control

Last but not least - here is the tree view derived control.

public class NavigatorTree : TreeView
{
    private class ExpandableNode
    {
        private Node _node;
        private IHierarchyFeed _feed;
        public ExpandableNode(Node node, IHierarchyFeed feed) { _node = node; _feed = feed; }
        public void Expand(TreeNode treeNode) {
            treeNode.TreeView.BeginUpdate();
            treeNode.Nodes.RemoveAt(0); // Remove expandable node.
            foreach (Node childNode in _feed.QueryChildren(_node))
            {
                // Add company.
                TreeNode childTreeNode = treeNode.Nodes.Add(childNode.Name);
                childTreeNode.Tag = childNode;

                // Check if there are any children.
                if (childNode.HasChildren)
                {
                    TreeNode toExpandNode = childTreeNode.Nodes.Add("");
                    toExpandNode.Tag = new ExpandableNode(childNode, _feed);
                }
            }
            treeNode.TreeView.EndUpdate();
        }
    }

    private IHierarchyFeed _feed;

    public void SetFeed(IHierarchyFeed feed)
    {
        _feed = feed;
        Populate();
    }

    private void Populate()
    {
        Nodes.Clear();
            
        BeginUpdate();
        foreach (Node node in _feed.QueryChildren(null))
        {
            // Add company.
            TreeNode treeNode = Nodes.Add(node.Name);
            treeNode.Tag = node;

            // Check if there are any children.
            if (node.HasChildren)
            {
                TreeNode toExpandNode = treeNode.Nodes.Add("");
                toExpandNode.Tag = new ExpandableNode(node, _feed);
            }
        }
        EndUpdate();
    }

    protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
    {
        // Check if node has only one child and that child is expandable.
        if (e.Node.Nodes.Count == 1)
        {
            ExpandableNode expandable = e.Node.Nodes[0].Tag as ExpandableNode;
            if (expandable != null)
                expandable.Expand(e.Node);
        }
    }
}

Voila. It doesn’t get any simpler that that. You initialize tree control by calling SetFeed and providing feed class. For example:

navigatorTree.SetFeed(new FileSysHierarchyFeed("c:\\", "*.*"));

The control then calls Populate() which in turn populates first tree level and links every tree node with corresponding Node object via the Tag field. If a node has children the populate function adds a fake node of type ExpandableNode under it.

In OnBeforeExpand function the control checks for ExpandableNode. If it founds it - it calls it’s expand function to populate next tree level … and removes the fake node.

0 comment(s) :

Newer Post Older Post Home

Blogger Syntax Highliter