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!