Extending Html.RouteLink()

7 Jun

The other day I was tasked with converting a raw HTML site into MVC3/Razor. Each individual HTML page had its own copy of the navigation menu, with the link to the current page manually set to include class=”current” in order to automatically expand the menu and style the link differently. However, I wanted to place the navigation menu into a partial so that I could reuse it between multiple layouts. That meant  needed to have a way to determine the current page, compare that to the URL in the link, and add the “current” class whenever the two matched.

I wanted to use named routes for these links. Named routes are great for maintainability, because the references to action and controller names are contained in a single place–Global.asax.  However, neither route links nor action links provide the functionality I was looking for, which meant I would need to extend the existing HtmlHelpers to add a new kind of link helper. I wanted to cause as little disruption as possible with the new helper, so I made sure to match the syntax of Html.RouteLink() as closely as possible.

First, I needed to create my helper method.

using System.Collections.Generic;
using System.Web.Routing;

namespace System.Web.Mvc.Html {
	public static class Html {
		public static MvcHtmlString NavigationRouteLink(this HtmlHelper<dynamic> htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues = null, IDictionary<string, object> htmlAttributes = null) {
			return new MvcHtmlString(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, RouteTable.Routes, linkText, routeName, routeValues, htmlAttributes));
		}
	}
}

Next, I needed a way to determine whether or not the current page’s URL matched the URL generated by the named route. To do that, I pulled the current request’s absolute path out of the HtmlHelper object, and I pulled the named route’s absolute path out of an instance of UrlHelper.

using System.Collections.Generic;
using System.Web.Routing;

namespace System.Web.Mvc.Html {
	public static class Html {
		public static MvcHtmlString NavigationRouteLink(this HtmlHelper<dynamic> htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues = null, IDictionary<string, object> htmlAttributes = null) {
			if (htmlHelper.ViewContext.RequestContext.HttpContext.Request.Url.AbsolutePath == new UrlHelper(htmlHelper.ViewContext.RequestContext).RouteUrl(routeName))
				htmlAttributes.Add("class", "current");
			return new MvcHtmlString(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, RouteTable.Routes, linkText, routeName, routeValues, htmlAttributes));
		}
	}
}

With that out of the way, I needed to make sure that I wouldn’t run into any problems if htmlAttributes were null (which is extremely likely).

using System.Collections.Generic;
using System.Web.Routing;

namespace System.Web.Mvc.Html {
	public static class Html {
		public static MvcHtmlString NavigationRouteLink(this HtmlHelper<dynamic> htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues = null, IDictionary<string, object> htmlAttributes = null) {
			IDictionary<string, object> dict;
			if (htmlAttributes == null)
				dict = new Dictionary<string, object>();
			else
				dict = htmlAttributes;
			if (htmlHelper.ViewContext.RequestContext.HttpContext.Request.Url.AbsolutePath == new UrlHelper(htmlHelper.ViewContext.RequestContext).RouteUrl(routeName))
				dict.Add("class", "current");
			return new MvcHtmlString(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, RouteTable.Routes, linkText, routeName, routeValues, dict));
		}
	}
}

As a possible improvement, I could allow the name of the assigned class to be passed in. Alternatively, I could provide another (required) htmlAttributes parameter, which would only be applied if the named route matched the current URL. However, given the small scope of the project and the extra effort involved, I decided I wasn’t going to need it. The final product was an easy-to-use HtmlHelper that allowed me to add navigation links to my heart’s content without having to stop and think about URL-comparison logic.

<div id="nav">
    <ul class="level1">
        <li>
            <a>Who We Are</a>
            <ul class="level2">
                <li>@Html.NavigationRouteLink("About Us", "About")</li>
                <li>@Html.NavigationRouteLink("The Blog", "Blog")</li>
            </ul>
        </li>
    </ul>
</div>

produces:

<div id="nav">
	<ul class="level1">
		<li>
			<a>Who We Are</a>
			<ul class="level2">
				<li><a class="current" href="/AboutUs">About Us</a></li>
				<li><a href="/Blog">The Blog</a></li>
			</ul>
		</li>
	</ul>
</div>
Advertisements
%d bloggers like this: