A tree view is a wonderful way to present nested data structures or trees. Here's an example:
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:
Note that I'm using the excellent jQuery.treeview to render the collapsible tree with expansion buttons.<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>
That's bloody useful Mike. Thank you.
ReplyDeleteThanks Ed, you're welcome!
ReplyDeleteI 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.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteDifficult to make it live because of source codes is not released
ReplyDeleteHaving fun now trying to make it work with my data set
Hi Mike,
ReplyDeleteI 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
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):
ReplyDeletehttp://mikehadlow.blogspot.com/2008/02/never-write-for-loop-again-fun-with.html
Thanks a Lot Mike !..
ReplyDeleteThe 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
Kiran,
ReplyDeleteTry changing this line:
Html.RenderTree((CompositeThing)ViewData.Model, thing => thing.Name)
to this:
Html.RenderTree<CompositeThing>((CompositeThing)ViewData.Model, thing => thing.Name)
Hi Mike,
ReplyDeleteThanks a lot.
It working fine now.
Thanks & Regards,
Kiran Kirdat
Hi Mike,
ReplyDeleteI 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
I am not passing any value for Html Helper in HtmlHelper.RenderTree
ReplyDeleteIf 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
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.
ReplyDeleteThis help a lot to understand how do you fill a List of CompositeThing elements in recursive mode.
Thanks in advance.
RC.
<%= Html.RenderTree((CompositeThing)ViewData.Model,thing => thing.Name)%>
ReplyDeletegives 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
Nice and clean.
ReplyDeleteWorks like a charm.
Thank you
hi mike,
ReplyDeleteexcellent 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
thanxxxxxxx A lot MiKe
ReplyDeleteHi Mike,
ReplyDeleteThank 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?)
@Ryan - you need to add the following to your web config...
ReplyDelete....
Hi Mike,
ReplyDeleteI 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..
Thanks Mike
ReplyDeleteGreat walkthrough.
ReplyDeleteEasy 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
Please add a download source.
ReplyDeleteI'm unable to get the jquery treeview to work.
Hi, I'm also unable to get this example work...
ReplyDeleteThe 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!
Beside of that, HashedSet needs to be replaced by HashSet, unless you use an own implementation of HashedSet.
ReplyDelete