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.

12 comments:

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

Rem said...

Your blog is very nice...
visit my blog asp.net example

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

Kiran 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

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

Kiran 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