Creating a menu for your ASP.NET MVC website using HTML helpers
I've been working with ASP.NET MVC for a few months and it has been quite a ride. I'm working on a website where the menu is loaded from the database. It should be rendered as an unsorted list and a CSS class should be added to the active item. Here is an example of the menu:
<ul id="navigation">
<li class="active"><a href="about-us.aspx">About us</a></li>
<li><a href="location.aspx">Location</a></li>
<li><a href="what-can-we-do-for-you.aspx">What can we do for you?</a></li>
<li><a href="works-on-my-machine.aspx">Works on my machine</a></li>
</ul>
This is certainly no rocket science, a normal website navigation build with XHTML and CSS. Because ASP.NET MVC has no things as viewstate, server controls or postbacks we need another approach then what we're used to. In ASP.NET Web forms the unsorted list would probably be a server control which we then would manipulate in the code behind. But we're not using ASP.NET Web forms, we're using ASP.NET MVC and that's a good thing!
The first solution I found was adding a property to my Navigation model class called Active. So the Navigation class looked like this:
public class Navigation
{
public virtual Int32 Id
{
get; set;
}
public virtual String Label
{
get; set;
}
public virtual String Link
{
get; set;
}
public virtual Boolean Active
{
get; set;
}
}
Notice that the Link property maps to the action we want to perform.
In the controller I would first fetch the navigation items from the database and copy them to the ViewData property, then iterate through the collection of navigation items and compared the action of the current page to the navigation items in the collection. If the actions matched then the active item was found and the property was set. There were a few moral problems I had with this approach. First it was quite some code to do something simple as setting the active item of a menu. Second, and this was the worst problem, you had to add logic to your view in order to output the correct HTML for the active item. In the view something like this had to be done:
<% foreach(var navigationItem in (ViewData["navigation"] as TwotoContent.Domain.Models.Navigation[]))
{
if(navigationItem.Active)
{
%><li class="active"><%
}
else
{
%><li><%
}
%>
<a href="<%= navigationItem.Link + ".aspx" %>">
<%= navigationItem.Label %>
</a>
</li>
<% }%>
Oh crap! This resulted in logic in the view which could not be tested, on top of that it polluted the view. I started searching Google for better solutions and found this thread. The last post is actually the most valuable one. Ben Scheirman, who has a most excellent blog by the way, posted the tip to use HTML helpers for this purpose. I had read about HTML helpers, but unfortunately none of the helper methods that existed actually did what I needed. So I decided to create my own. Drew Miller wrote a nice post about writing your own HTML helper.
Here are a few advantages about creating your own HTML helper:
- Your view doesn't get cluttered
- The code in the HTML helper is testable
- Less code in your controller, this means less code to maintain which is always a good thing
So I created the following extension on the HTML helper class:
public static String NavigationItem(this HtmlHelper htmlHelper,
String actionLink,
String controller,
String itemTitle)
{
var htmlTemplate = htmlHelper.ActionLink(itemTitle, actionLink, controller) + "</li>";
var action = htmlHelper.ViewContext.RouteData.Values["action"];
if(String.Compare(actionLink, action.ToString(), true, CultureInfo.InvariantCulture) == 0)
{
htmlTemplate = "<li class=\"active\">" + htmlTemplate;
}
else
{
htmlTemplate = "<li>" + htmlTemplate;
}
return String.Format(htmlTemplate, actionLink, itemTitle);
}
As you can see I do use one helper method, the ActionLink method, in order to create a link for the given action for the given controller. The advantage of using ActionLink is that it takes the defined routes into account so you don't have to worry about that. Then we compare the given action to the current action if those two are equal we generate a list item with the correct class, otherwise we just generate a list item. Now we can write something like this in our view:
<% foreach(var navigationItem in (ViewData["navigation"] as TwotoContent.Domain.Models.Navigation[]))
{
Html.NavigationItem(navigationItem.Link, "mycontroller", navigationItem.Label);
}
%>
If you have any suggestions to improvement, questions or remarks I'll be glad to listen to them. This will generate the menu just as we want it to be. I'm sure this solution still has drawbacks but I hope you agree this is a big improvement compared to the first solution.