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:

Unknown said...

That's bloody useful Mike. Thank you.

Mike Hadlow said...

Thanks Ed, you're welcome!

Mike Hadlow said...

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.

Saiful Alam said...
This comment has been removed by a blog administrator.
Anonymous said...

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

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

Unknown said...

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

Mike Hadlow said...

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

Unknown said...

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

Mike Hadlow said...

Kiran,

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

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

Unknown said...

Hi Mike,

Thanks a lot.
It working fine now.

Thanks & Regards,
Kiran Kirdat

Anonymous said...

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

Anonymous said...

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

Roberto Cervantes said...

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.

Vinoj said...

<%= 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

Tomislav said...

Nice and clean.
Works like a charm.

Thank you

abhishek madhekar said...

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

KHAN said...

thanxxxxxxx A lot MiKe

Ryan said...

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?)

Tricky Dicky said...

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






....

Anonymous said...

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

suraj.nair said...

Thanks Mike

Unknown said...

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

Shimmy said...

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

Anonymous said...

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!

Anonymous said...

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