Delegates and func to the rescue

Published on den 27 December 2009

I have recently been spending some time implementing T4Mvc in a project I started a while ago. Everything went fine untill I realised a html helper I had got some nasty side effects. Fortuanly I could solve it with delgates.

The problem

In the project items are arranged by categories. Each item can belong to many categories and each category can have many subcategories. The hierarchical structure and the fact that there might be quite some categories in the end made me use a treeview for representation, more specifically the treeview by Jörn Zaefferer.

treeview example

This treeview will be used in many placed in the site and the urls on the categories will point to different places. Also because building the hierarchy is not trivial I decided to build a HtmlHelper that handles this. In the view I use the following code to create the treeview above,

<%: Html.CategoryTreeView("/News/ByCategory/{0}", Model.Categories)%>

Internally the HtmlHelper uses string.Format to create the real url of the pattern above. Thereare some issues with this though. As I said I'm adding T4Mvc by David Ebbo and I do this because I want to get rid of all string based url so I can get compile time checking of my Url's and also design time support.

I thought this would be quite easy to fix by simply changing the code to this:

<%: Html.CategoryTreeView(Url.Action(MVC.News.ByCategory()) + "/{0}", Model.Categories)%>

This worked just fine when I tested it and it produced the same url's and I have more safety. A bit later when I clicking around to test another thing I realized there was no news showing even though I just clicked a category that should have many items in it. A look a the Url revealed the problem:

Instead of http://localhost/News/ByCategory/13 it noew was http://localhost/News/ByCategory/2/13. After  a little more research I found that the 2 came from the category I was viewing when I clicked the link. So apparently my now not soo good solution did not work. To be honest I have not researched exactly where it went wrong but the reason is atleast that when you call

Url.Action(MVC.News.ByCategory())

with the empty overload for ByCategory and you are visting that action it adds the route value you are currently at so instead of a url like "News/ByCategory" as I expected I got "News/ByCategory/2" which caused me some headache. 

The solution

I could either go back to the string based solution or I could figure out a way to solve this problem. Luckily I remebered an excellent post by Rick Strahl- Fun with Func<T,TResult> Delegates, Events and Async Operations and thsi gave me the idea to try to use the Func<t, tresult=""> and delgates to handle this issue.</t,>

First of all I should mention that my HtmlHelper only wrapped a class that did all the rendering of the tree. The HtmlHelpers are static and I needed an instance of a class to render the tree.

The HtmlHelper originally looked like this

/// <summary>
/// Creates a category tree with links for category specified by the UrlPattern.
/// </summary>
/// <param name="helper"></param>
/// <param name="urlPattern">Should be in the form /Category/Details/{0}</param>
/// <param name="categories">The root level categories. These should have their children loaded</param>
/// <returns></returns>
public static MvcHtmlString CategoryTreeView(this HtmlHelper helper, string urlPattern, IEnumerable<Gmok.Category> categories)
{
    CategoryTreeviewCreator treeCreator = new CategoryTreeviewCreator(categories, CategoryTreeviewCreator.TreeType.ListingTree);
    treeCreator.UrlPattern = urlPattern;

    return MvcHtmlString.Create(treeCreator.CreateTree(helper));
}

The CategoryTreeviewCreator.TreeType.ListingTree only say that the tree should be read only. I have a version with checkboxes too. What I wanted instead of this was:

/// <summary>
/// Creates a category tree with links for category specified by func.
/// </summary>
/// <param name="helper"></param>
/// <param name="func"></param>
/// <param name="categories">The root level categories. These should have their children loaded</param>
/// <returns></returns>
public static MvcHtmlString CategoryTreeView(this HtmlHelper helper, Func<Gmok.Category,string> func, IEnumerable<Gmok.Category> categories)
{
    CategoryTreeviewCreator treeCreator = new CategoryTreeviewCreator(categories, CategoryTreeviewCreator.TreeType.ListingTree);
    treeCreator.CreateUrl += func;

    return MvcHtmlString.Create(treeCreator.CreateTree(helper));
}

Notice that instead of seting the UrlPattern we now add a delegate to the CreateUrl event. The event is defined like this in CategoryTreeviewCreator. If you haven't seen event declared like this before look at Rick's blog to understand this. Also notice that it now takes a Func<Category,string> instead of the urlPattern as parameter.

public event Func<Gmok.Category, string> CreateUrl;

And in my method that creates the hyperlinks it looks like this:

private string TreeCreator_SetListingContent(Category item, int level, HtmlHelper helper)
{
    TagBuilder tb = new TagBuilder("a");
    tb.MergeAttribute("href", this.CreateUrl.Invoke(item));
    tb.SetInnerText(item.Name);
    return tb.ToString();
}

Note that I actually got more flexibility using this version. Because I now include the category as inparameter to the Func I have access to the Category I'm building the link for in my view. I doubt I will use anything but the ID of the category but who knows what happends in the future.

I still have not showed how this is used though. It's very simple if you have used lambada expression before. My code in the view would now look like this:

<%: Html.CategoryTreeView(c => Url.Action(MVC.News.ByCategory(c.ID, null)), Model.Categories)%>

ByCategory takes two parameters. The first one is the ID of the category and the second one is the pageIndex. Here I want to load the default(or first) page so I set the parameter to null. 

Summary

By using the Func<> construct toghether with delegates I could avoid both decalring my delegate and declaring methods that are called when the event is invoked. I think that Func<> and Expression<> is a hidden gem in c# that I have looked in to way too little. It was first when I read Rick's post my eyes opened.

By getting this to work I finished the update of my site to use T4Mvc. If you haven't used this I really recommend you to check it out. Getting rid of unsafe strings is always good.

Then feel free to it or if you have any comments or questions mention @MikaelEliasson on Twitter.

CTO and co-founder at Bokio with a background as an elite athlete. Still doing a lot of sports but more for fun.

#development, #web, #orienteering, #running, #cycling, #boardgames, #personaldevelopment