Tuesday, October 21, 2008

Rendering a tree view using the MVC Framework

A tree view is a wonderful way to present nested data structures or trees.  Here's an example:

treeview

I've written an HTML helper that presents tree structures as nested unordered lists in HTML. First I've got an interface to represent a tree node. It's called IComposite after the GoF pattern.

public interface IComposite<T>
{
    T Parent { get; }
    ISet<T> Children { get; }
}

Now we just need to make sure that our entity implements this interface. Here's a simple example, CompositeThing:

public class CompositeThing : IComposite<CompositeThing>
{
    public CompositeThing()
    {
        Children = new HashedSet<CompositeThing>();
    }
    public string Name { get; set; }
    public CompositeThing Parent { get; set; }
    public ISet<CompositeThing> Children { get; set; }
}

Now all we need to do is get the object graph from the database. Techniques for doing that are the subject for another post, but it's pretty straight forward with both LINQ-to-SQL and NHibernate. Once we have the graph we can pass it to our view and display it like this:

<%= Html.RenderTree(ViewData.Model.CompositeThings, thing => thing.Name) %>

And finally here's the code for  the RenderTree HtmlHelper extension method:

public static class TreeRenderHtmlHelper
{
    public static string RenderTree<T>(
        this HtmlHelper htmlHelper,
        IEnumerable<T> rootLocations,
        Func<T, string> locationRenderer)
        where T : IComposite<T>
    {
        return new TreeRenderer<T>(rootLocations, locationRenderer).Render();
    }
}
public class TreeRenderer<T> where T : IComposite<T>
{
    private readonly Func<T, string> locationRenderer;
    private readonly IEnumerable<T> rootLocations;
    private HtmlTextWriter writer;
    public TreeRenderer(
        IEnumerable<T> rootLocations,
        Func<T, string> locationRenderer)
    {
        this.rootLocations = rootLocations;
        this.locationRenderer = locationRenderer;
    }
    public string Render()
    {
        writer = new HtmlTextWriter(new StringWriter());
        RenderLocations(rootLocations);
        return writer.InnerWriter.ToString();
    }
    /// <summary>
    /// Recursively walks the location tree outputting it as hierarchical UL/LI elements
    /// </summary>
    /// <param name="locations"></param>
    private void RenderLocations(IEnumerable<T> locations)
    {
        if (locations == null) return;
        if (locations.Count() == 0) return;
        InUl(() => locations.ForEach(location => InLi(() =>
        {
            writer.Write(locationRenderer(location));
            RenderLocations(location.Children);
        })));
    }
    private void InUl(Action action)
    {
        writer.WriteLine();
        writer.RenderBeginTag(HtmlTextWriterTag.Ul);
        action();
        writer.RenderEndTag();
        writer.WriteLine();
    }
    private void InLi(Action action)
    {
        writer.RenderBeginTag(HtmlTextWriterTag.Li);
        action();
        writer.RenderEndTag();
        writer.WriteLine();
    }
}

The resulting HTML looks like this:

<html>
<head>
    <title>Tree View</title>
    <link href="jquery.treeview.css" rel="stylesheet" type="text/css" />
    <script src="jquery-1.2.6.min.js" type="text/javascript"></script>
    <script src="jquery.treeview.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(function() {
            $("#treeview ul").treeview();
        });
    </script>
</head>
<body style="font-family : Arial; ">
    <h1>
        Tree View
    </h1>
    <div id="treeview">
        <ul>
            <li>Root
                <ul>
                    <li>First Child
                        <ul>
                            <li>First Grandchild</li>
                            <li>Second Grandchild</li>
                        </ul>
                    </li>
                    <li>Second Child
                        <ul>
                            <li>Third Grandchild</li>
                            <li>Fourth Grandchild</li>
                        </ul>
                    </li>
                </ul>
            </li>
        </ul>
    </div>
</body>
</html>
Note that I'm using the excellent jQuery.treeview to render the collapsible tree with expansion buttons.

25 comments:

  1. That's bloody useful Mike. Thank you.

    ReplyDelete
  2. Thanks Ed, you're welcome!

    ReplyDelete
  3. I had a question today about ISet<T>. The application I'm currently working on is using NHibernate. One of NHibernates foibles is that it likes you to define your collections using the Iesi.Collections library. ISet<T> is the interface for a set in that library. You could simply replace ISet<T> with IEnumerable<T> and HashedSet<T> with List<T> and everything should still work.

    ReplyDelete
  4. This comment has been removed by a blog administrator.

    ReplyDelete
  5. Anonymous7:32 am

    Difficult to make it live because of source codes is not released

    Having fun now trying to make it work with my data set

    ReplyDelete
  6. Hi Mike,

    I tried using IEnumerable T instead of Iset. I am getting below error:
    'System.Collections.Generic.IEnumerable T ' does not contain a definition for 'Foreach' and no extension method 'Foreach' accepting a first argument of type 'System.Collections.Generic.IEnumerable T ' could be found (are you missing a using directive or an assembly reference?)

    The code which is giving above error is:
    InUl(() => locations.Foreach(location =>
    {
    writer.Write(locationRenderer(location));
    RenderLocations(location.Children);
    }));
    Can you please let me know if I am missing anything here

    Thanks in advance.

    Regards,
    KK

    ReplyDelete
  7. Hi Kiran. That ForEach is a little extension method. There are loads of examples around. See my post here for one (called Each in this case):

    http://mikehadlow.blogspot.com/2008/02/never-write-for-loop-again-fun-with.html

    ReplyDelete
  8. Thanks a Lot Mike !..
    The program is compiling now. But again Iam having following issues.
    =========Beging HTML Content============
    Html.RenderTree((CompositeThing)ViewData.Model, thing => thing.Name)
    =========End HTML Content============

    ========Begin Model=======

    public class CompositeThing : IComposite<CompositeThing>
    {
    public CompositeThing()
    {
    Children = new List<CompositeThing>();
    }
    public string Name { get; set; }
    public CompositeThing Parent { get; set; }
    public IEnumerable<CompositeThing> Children { get; set; }
    }

    public interface IComposite<T>
    {
    T Parent { get; }
    IEnumerable<T> Children { get; }
    }

    ======End Model===============

    ====Index method===============
    public ActionResult Index()
    {
    ViewData["Message"] = "Welcome to ASP.NET MVC!";

    CompositeThing com = new CompositeThing();
    List<CompositeThing> test = new List<CompositeThing>();

    CompositeThing child1 = new CompositeThing();
    child1.Name = "C1";

    CompositeThing child2 = new CompositeThing();
    child2.Name = "C2";
    test.Add(child1);
    test.Add(child2);

    com.Children = test;

    CompositeThing Parent = new CompositeThing();
    Parent.Name = "Main";
    com.Parent = Parent;

    return View(com);
    }
    =================================


    I am trying to use the aboce code but below is the error I am getting.
    "The type arguments for method 'TestAppTree.Helper.TreeRenderHtmlHelper.RenderTree<T>(System.Web.Mvc.HtmlHelper, System.Collections.Generic.IEnumerable<T>, System.Func<T,string>)' cannot be inferred from the usage. Try specifying the type arguments explicitly."

    It will be of great help If you let me know the reason for this error

    Thanks & Regards,
    KK

    ReplyDelete
  9. Kiran,

    Try changing this line:
    Html.RenderTree((CompositeThing)ViewData.Model, thing => thing.Name)

    to this:
    Html.RenderTree<CompositeThing>((CompositeThing)ViewData.Model, thing => thing.Name)

    ReplyDelete
  10. Hi Mike,

    Thanks a lot.
    It working fine now.

    Thanks & Regards,
    Kiran Kirdat

    ReplyDelete
  11. Anonymous12:51 pm

    Hi Mike,
    I am getting following error in my
    HtmlHelper.RenderTree

    Argument type CompositeThing is not assignable to parameter type System.Collections.Generics.IEnumerable of CompositThing

    Could you please tell me what is the problem.

    Thanks

    ReplyDelete
  12. Anonymous12:55 pm

    I am not passing any value for Html Helper in HtmlHelper.RenderTree

    If I wont pass a value it gives me error saying argument not specified.

    So I have removed the parameter in RenderTree

    will it be a problem.

    Could you please tell me what to pass their..

    Thanks

    ReplyDelete
  13. Hi Mike, I believe came late to your post activity but reviewing I found it very interesting, just have a question, do you have any sample where you implement this post but all-in-one, using the 3 concerns: Model, View, Controller.

    This help a lot to understand how do you fill a List of CompositeThing elements in recursive mode.

    Thanks in advance.
    RC.

    ReplyDelete
  14. <%= Html.RenderTree((CompositeThing)ViewData.Model,thing => thing.Name)%>

    gives me server error. The system says,
    Compiler Error Message: CS1928: 'System.Web.Mvc.HtmlHelper>' does not contain a definition for 'RenderTree' and the best extension method overload 'Controls.TreeRenderHtmlHelper.RenderTree(System.Web.Mvc.HtmlHelper, System.Collections.Generic.IEnumerable, System.Func)' has some invalid arguments


    help appreciated

    ReplyDelete
  15. Tomislav1:23 am

    Nice and clean.
    Works like a charm.

    Thank you

    ReplyDelete
  16. hi mike,
    excellent post!
    I just want to know where to place the methods you described since i'm relatively new to MVC. I tried placing the code in the controller or custom models, but I am getting error
    1)extension method must be defined in non-generic static class
    2)The type or namespace name 'TreeRenderer' could not be found (are you missing a using directive or an assembly reference?)
    3)Warning as Error: Type parameter 'T' has the same name as the type parameter from outer type

    ReplyDelete
  17. thanxxxxxxx A lot MiKe

    ReplyDelete
  18. Hi Mike,

    Thank you very much for your posting, I do it the same with your way, but I get this error, what's wrong with me? Please help!

    'System.Web.Mvc.HtmlHelper' does not contain a definition for 'TreeRender' and no extension method 'TreeRender' accepting a first argument of type 'System.Web.Mvc.HtmlHelper' could be found (are you missing a using directive or an assembly reference?)

    ReplyDelete
  19. @Ryan - you need to add the following to your web config...






    ....

    ReplyDelete
  20. Anonymous2:27 pm

    Hi Mike,
    I have a requirement which is to display data on a treeview(Parent-Child format).
    When any of the child node is clicked,its id should be passed to the controller and its corresponding data should be brought and displayed on the right hand side panel of the Html Form.
    Help would be highly appreciated..
    Thanks in Advance..

    ReplyDelete
  21. Great walkthrough.
    Easy to understand and I took the opportunity to use it to render the tree structure of blog posts on my website and described this little "case study"

    Implementing a Tree View - Small Case Study

    Evgeny

    ReplyDelete
  22. Please add a download source.
    I'm unable to get the jquery treeview to work.

    ReplyDelete
  23. Anonymous12:01 pm

    Hi, I'm also unable to get this example work...

    The viewModel says that it is from type CompositeThing, see "Html.RenderTree((CompositeThing)ViewData.Model, thing => thing.Name)".

    The HtmlHelper Extension RenderTree awaits IEnumerable rootLocations as parameter.

    The compiler tells (correctly) that (CompositeThing)ViewData.Model cannot be used since IEnumerable is awaited as type for the first parameter in "Html.RenderTree((CompositeThing)ViewData.Model, thing => thing.Name)".

    Or did I overlook something?
    Many thanks!

    ReplyDelete
  24. Anonymous12:05 pm

    Beside of that, HashedSet needs to be replaced by HashSet, unless you use an own implementation of HashedSet.

    ReplyDelete

Note: only a member of this blog may post a comment.