Rendering Action Links in ASP.NET MVC Applications

An entry about asp.net mvc Publication date 26. January 2008 21:39

This is going to be the first of probably many posts targeted at the new ASP.NET MVC framework, which I am loving more and more the more I use it (more!). As per usual, I won't be giving a broad introduction to the technology at hand - prominent bloggers like Scott Guthrie has already covered that. Instead, my posts will be written on the basis that you, dear reader, has taken the jump into the deep end with the rest of us, poking and prodding at the framework to see what its got. In this post, I'll be tackling how best to output links to controllers/actions in a view.

Lets say that I'm writing a blog application, and on my page I want to display a list of all the categories (tags), linking each to a page that displays any posts within that category. It might look something like this:

<% foreach(CategoryInfo category in this.ViewData.Categories)
   { %>           
        <div>
            <a href='<%= String.Format("Category/{0}", Server.UrlEncode(category.Name)) %>' 
               title='<%= String.Format("View all posts in the category '{0}'", category.Name) %>'>
                <%= category.Name %>
            </a>                                
        </div>
<% } %>

That's a bit verbose but not too bad, you might think. It is bad, though - because we've just hard-coded the URL to the Category controllers List action. If we later change the configured route to the Category controller and/or any of its actions, the above code would begin outputting broken links. Auch!

Ask for the Route

The routing system is really one of the core aspects of the MVC framework, and is mainly responsible for routing incoming requests to an action on a controller. For instance, for the above example we might have registered the following route:

RouteTable.Routes.Add(new Route
{
    Url = "Category/[Name]",
    Defaults = new { controller = "Category", action = "List", name = (string)null },
    RouteHandler = typeof(MvcRouteHandler)
});

However, the route table can also be used to dynamically construct the URLs for its registered routes, using the RouteCollections GetUrl method. The HtmlHelper class uses this internally to render anchor tags, and is exposed on the ViewPage through its Html property. Thus, we could rewrite the above category listing as follows:

<% foreach(CategoryInfo category in this.ViewData.Categories)
   { %>           
        <div>
            <%= Html.ActionLink(category.Name, new { Controller = "Category", Action="List", Name = category.Name})%>                                
        </div>
<% } %>

Now, the HtmlHelper will do a look up in the route table to figure out the appropriate URL, and should we wish to change the route it will still output a working link. As an aside, this method uses anonymous types to easily define a set of key/value pairs, which is one of the many cool new features in C# 3.0.

Oh, My Attributes!

If you paid attention, you might have noticed that we left out something in the last example. Our initial list of categories output anchor tags complete with title attributes - but unfortunately, the ActionLink method does not allow us to add any attributes to the tag that it creates. We would have to mark up the tag ourselves and use the UrlHelpers Action method to just get the action URL to accomplish this:

<% foreach(CategoryInfo category in this.ViewData.PopularCategories)
   { %>           
        <div>
            <a href='<%= Url.Action(new { Controller = "Category", Action="List", Name = category.Name})%>'
               title='<%= String.Format("View all posts in the category '{0}'", category.Name) %>'>
                <%= category.Name %>
            </a>                                              
        </div>
<% } %>

We've solved the hard-coded URL problem, but now we're back to the verbose, unfriendly markup we had in the beginning. Would it not be cool if the ActionLink method allowed us to also tell it what attributes to render? Something like this:

<% foreach(CategoryInfo category in this.ViewData.PopularCategories)
   { %>           
        <div>
            <%= Html.ActionLink(category.Name, 
                    new { Controller = "Category", Action="List", Name = category.Name }, 
                new { title = "View all posts in the category " + category.Name })%>
        </div>
<% } %>

Here, I'm using the same anonymous types trick to tell the ActionLink method which attributes that I wish rendered as part of the anchor tag. With an extension method, we can make the above work:

public static class HtmlHelperExtension
{ 
    public static string ActionLink(this HtmlHelper helper, string linkText, object routeValues, object attributeValues)
    {
        // get the URL route
        string actionUrl = RouteTable.Routes.GetUrl(helper.ViewContext, routeValues);
 
        // start building the anchor tag
        HtmlGenericControl a = new HtmlGenericControl("a");
        a.InnerText = linkText;
        a.Attributes["href"] = actionUrl;
 
        // add the attributes to the anchor tag
        foreach (KeyValuePair<string, object> attribute in GetAttributeValues(attributeValues))
        {
            a.Attributes[attribute.Key] = attribute.Value.ToString();
        }
 
        StringBuilder html = new StringBuilder();
        StringWriter sWriter = new StringWriter(html);
        HtmlTextWriter htmlWriter = new HtmlTextWriter(sWriter);
 
        // render the html
        a.RenderControl(htmlWriter);
 
        return html.ToString();
    }
 
    private static IEnumerable<KeyValuePair<string, object>> GetAttributeValues(object attributeValues)
    {
        if (values != null)
        {
            Type type = attributeValues.GetType();
            PropertyInfo[] properties = type.GetProperties();
 
            foreach (PropertyInfo property in properties)
            {
                string attributeName = property.Name;
                object value = property.GetValue(attributeValues, null);
 
                yield return new KeyValuePair<string, object>(attributeName, value);                    
            }
        }
    }
}

And there you have it; a fairly concise and loosely coupled way of generating action links.

Not Quite, Though

There are still a few things that bug me about the action linking though - primarily the fact that I've still hard coded both the controller and action names. As it stands, the MvcRouteHandler expects these names to map directly onto the controller class and its action methods, meaning that if I opt to refactor any of those, my links will break.

In part two of his series of posts on the MVC framework, Scott Guthrie wrote about a generic overload for ActionLink which employed lambda expressions to safely type the controller name and action, which would mostly mitigate this, leaving only any argument names as literals. However, it seems like this feature did not make it into the CTP - at least I can't seem to find it anywhere :(

Update (27/1): Well what do you know; the MVC Toolkit that comes as a separate download includes - among other things - the above mentioned ActionLink<T> method implemented as an extension method for the HtmlHelper class. Yay!

Currently rated 2.8 by 4 people

  • Currently 2.75/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Powered by BlogEngine.NET 1.4.5.0

Welcome!

My name is Fredrik Kalseth, and this is my blog - thanks for visiting! I am fortunate enough to work with what I love for a living, and this blog is essentially the biproduct of that.

I work as a senior consultant for Capgemini, and am also an active participant in the Norwegian .NET community, as an avid attendee but also as a speaker (most recently at NNUG and MSDN Live).

As a developer, I have a wide circle of interest. My primary passion is for agile, test-driven development, with focus on best practices and clean code. That said, I also love to work on the frontend, especially with web development.

On Twitter? My handle is fkalseth. On LinkedIn? I`m there too.

NDC 2010

The conference to attend this summer happens June 16th-18th in Oslo, Norway. Are you going? Be sure to catch my talk on AOP while you're there!

 

Disclaimer

This is a personal blog; any opinions expressed here are my own and do not necessarily reflect those of my employer. All content herein is my own original creation, and as such is protected by copyright law. Unless otherwise stated, all source code posted on this blog is freely usable under the Microsoft Permissive License.

What Readers Talk About

Comment RSS