Code Highlighting

Friday, August 3, 2012

Binding a TreeView to a custom data type

I'm currently working on an MVC-based website. For managing the database I've elected to go with a classic GridView/DetailsView WebForms solution though, mostly because I have the webcontrols and the old code to make that the quicker solution.
One of the things that needs editing is the menu: a simple Key | ParentKey | MenuKey | Name | Url table (MenuKey because I have more than one menu). I bound a TreeView control to this menu using a set of classes I wrote a while ago to bind a TreeView to a folder structure. I decided to share my code, because it's considerably easier to use than anything else I've been able to find online.
I've based my code on the following article: http://www.codeproject.com/Articles/19639/Implementing-IHierarchy-Support-Into-Your-Custom-C . I was able to get my menu items bound to my TreeView using this method. It does not seem reasonable to have to write the same type of code every time I want to bind to a custom data type though. What's more; I'm adding a collection class, simply to implement the IHierarchicalEnumerable interface. The methods I'm simply patching through to existing methods and properties. I'm a programmer, not a plumber.
So here is a generic implementation of IHierarchyData and IHierarchicalEnumerable (download link at the end):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;

namespace Tabeoka
{
    /// <summary>
    /// Utility class for creating a hierarchical data source
    /// </summary>
    public static class HierarchyData
    {
        /// <summary>
        /// Returns a hierarchical data list containing only the root item
        /// </summary>
        /// <typeparam name="T">The data type used for building up the hierarchical data source</typeparam>
        /// <param name="root">The root data item</param>
        /// <param name="childSelector">A delegate that returns the child items of the item passed as an argument</param>
        /// <param name="parentSelector">A delegate that returns the parent item of the item passed as an argument, or null if root</param>
        /// <param name="pathSelector">A delegate that is called for the path of the current item as a string</param>
        /// <returns></returns>
        public static HierarchyDataList<T> GetData<T>(T root, Func<T, IEnumerable<T>> childSelector, Func<T, T> parentSelector, Func<T, string> pathSelector) where T : class
        {
            if (root == null)
                throw new ArgumentNullException("root");
            
            if (childSelector == null)
                throw new ArgumentNullException("childSelector");
            
            if (parentSelector == null)
                throw new ArgumentNullException("parentSelector");
            
            if (pathSelector == null)
                throw new ArgumentNullException("pathSelector");

            return new HierarchyDataList<T>(
                new List<T> { root },
                childSelector,
                parentSelector,
                pathSelector
                );
        }

        /// <summary>
        /// Wrapper class for the data type that implements IHierarchyData 
        /// </summary>
        /// <typeparam name="T">The underlying data type</typeparam>
        public class HierarchyDataItem<T> : IHierarchyData where T : class
        {
            internal T DataItem { get; set; }
            internal Func<T, IEnumerable<T>> ChildSelector { get; set; }
            internal Func<T, T> ParentSelector { get; set; }
            internal Func<T, string> PathSelector { get; set; }

            internal HierarchyDataItem(
                T dataItem,
                Func<T, IEnumerable<T>> childSelector,
                Func<T, T> parentSelector,
                Func<T, string> pathSelector
                )
            {
                DataItem = dataItem;

                this.ChildSelector = childSelector;
                this.ParentSelector = parentSelector;
                this.PathSelector = pathSelector;
            }

            #region IHierarchyData Members

            /// <summary>
            /// Gets the child items wrapped in a IHierarchicalEnumerable
            /// </summary>
            /// <returns>A HierarchyDataList&lt;T&gt;</returns>
            public IHierarchicalEnumerable GetChildren()
            {
                return new HierarchyDataList<T>(
                        ChildSelector(DataItem),
                        this.ChildSelector,
                        this.ParentSelector,
                        this.PathSelector
                    );
            }

            /// <summary>
            /// Gets the parent item, and wraps it in a HierarchyDataItem
            /// </summary>
            /// <returns>A HierarchyDataItem&lt;T&gt;, or null if not found</returns>
            public IHierarchyData GetParent()
            {
                var parent = this.ParentSelector(DataItem);
                if (parent != null)
                    return new HierarchyDataItem<T>(
                        parent,
                        this.ChildSelector,
                        this.ParentSelector,
                        this.PathSelector
                        );

                return null;
            }

            /// <summary>
            /// Checks if there are any child nodes, and returns true if there are
            /// </summary>
            public bool HasChildren
            {
                get
                {
                    return this.ChildSelector(DataItem).Count() > 0;
                }
            }

            /// <summary>
            /// The underlying data object
            /// </summary>
            public object Item
            {
                get { return DataItem; }
            }

            /// <summary>
            /// Is supposed to return the logical path according to the underlying data,
            /// just calls the pathSelector.
            /// </summary>
            public string Path
            {
                get { return PathSelector(DataItem); }
            }

            /// <summary>
            /// TypeOf(T)
            /// </summary>
            public string Type
            {
                get { return typeof(T).ToString(); }
            }

            #endregion
        }

        /// <summary>
        /// A list of T that implements IHierarchicalEnumerable
        /// </summary>
        /// <typeparam name="T">The underlying data type</typeparam>
        public class HierarchyDataList<T> : List<T>, IHierarchicalEnumerable where T : class
        {
            internal Func<T, IEnumerable<T>> ChildSelector { get; set; }
            internal Func<T, T> ParentSelector { get; set; }
            internal Func<T, string> PathSelector { get; set; }

            internal HierarchyDataList(
                IEnumerable<T> items,
                Func<T, IEnumerable<T>> childSelector,
                Func<T, T> parentSelector,
                Func<T, string> pathSelector
                )
                : base(items)
            {

                this.ChildSelector = childSelector;
                this.ParentSelector = parentSelector;
                this.PathSelector = pathSelector;
            }

            #region IHierarchicalEnumerable Members

            /// <summary>
            /// Wraps the enumeratedItem object in a HierarchyDataItem
            /// </summary>
            /// <param name="enumeratedItem">The data item</param>
            /// <returns>an instance of HierarchyDataItem&lt;T&gt;</returns>
            public IHierarchyData GetHierarchyData(object enumeratedItem)
            {
                return new HierarchyDataItem<T>(
                    enumeratedItem as T,
                    this.ChildSelector,
                    this.ParentSelector,
                    this.PathSelector);
            }

            #endregion
        }
    }
}

I'm wrapping all of it in a static class: the public static method allows me to infer the generic type parameters.

Here's how you use it:

// The root node for my folder structure
var root = Folders.GetRoot();

// Setting the delegates for getting
// children, parent and path value
var folderData = HierarchyData.GetData(
    root, 
    d => d.SubFolders, 
    d => d.ParentFolder, 
    d => d.Name);

tree = new TreeView();

this.Controls.Add(tree);

if (!this.Page.IsPostBack)
{
    // now just assign datasource and bindings
    tree.DataSource = folderData;
    tree.DataBindings.Add(new TreeNodeBinding()
    {
        TextField = "Name",
        ValueField = "Key"
    });
    tree.DataBind();
}

And here's the download link: http://www.tabeoka.be/downloads/HierarchyData.zip .

I hope this helps someone, somewhere. I welcome comments and criticism.

Menno

No comments:

Post a Comment