I happened upon the following code in a recent project for one of our ASP.NET MVC web applications today:
This line of code works, in that it does what it’s supposed to; but it’s brittle. Like Ginger snaps brittle.
Let’s imagine we have a route for this action already set up:
“showWidget”,
“/widget/show/{productId}/{*productName}”,
new { controller = widget, action = show },
new { productId = @“d+” }
);
Now, what happens to that previous URL if we change the route?
“showWidget”,
“/widget/show/{productName}/{productId}”,
new { controller = widget, action = show },
new { productId = @“d+” }
);
At best, our routes will return:
and at worst they’ll die a horrible death when your user tries to click on one. The unfortunate thing about this is that there’s no easy way to check whether or not your links 404 without the help of a third party library, not to mention checking whether they’re pretty or not without a third-party library*.
There are also other side effects, like having to change every link on your site whenever you change your routes, and dealing with the hideously bad angle bracket code mixed in with HTML code (uggh).
Here are some other ways of writing URLs in ASP.NET MVC, each cleaner than the above.
Good Ol’ Actionlink:
This particular approach creates the entire link for you; great if you’re a developer, but it sucks if you spend your time styling with CSS. To style the above link with CSS, you have to do the following:
There’s also the RouteLink method that looks pretty similar to the above, except that it takes a route name:
There are also other solutions that don’t require so much interposition of C# code in a view.
Luckily, for our CSS friends (who may or may not be familiar with MVC), we have a few other solutions, and they each center around a different way of doing things in MVC. There are two major ways of getting a URL in MVC, by the route name that you’re looking to hit, or by the Action you want to invoke.
For the action:
For the Route, there are many overloads on RouteUrl that give you many overloads that aren’t intuitive at first.
First, if you just want to match a route by route values:
all the way to even specifiying the protocol and host (!). The latter is important because it helps when you need an absolute URL from a non-SSL site to an SSL site (or vice versa).
The one I use (when I need a URL) looks almost exactly like its URL.Action() counterpart, except that it takes a route name instead:
But maintaining these Urls or ActionLinks is still a pain in the ass (if you don’t have ReSharper). Everywhere you want to change the route name, or the Action name (say we wanted to change “Show” to “ShowWidget”), you have to conduct a Find and Replace on potentially dozens of method calls. Luckily, there’s an easier way. Url.Action and Url.RouteUrl belong to the ‘UrlHelper’ class; which holds extension methods for Urls. Likewise, the Html.Action() and all its brethren belong to the HtmlHelper class. These allow us to create extension methods to the extension methods:
{
return MvcHtmlString.Create(
helper.ActionLink(
Model.ProductName,
“Show”,
“Widget”,
new {
productID = product.ProductId,
productName = product.ProductName.ToUrlSlug()
},
null
)
);
}
and similarly for the UrlHelper:
{
return MvcHtmlString.Create(
helper.RouteUrl(
“widgetlink”,
new {
productID = product.ProductId,
productName = product.ProductName.ToUrlSlug()
}
)
);
}
Then both of those become:
and:
But that still doesn’t fix our issue of icky views. Do we really want our views to look like this?
<ul>
<li><a href=“<%= Url.Action(“Show“, “Widget“, new { productID = Model.ProductId, productName = Model.ProductName.ToUrlSlug() }) %>”><h2><%= product.ProductName %></h2></a>
<% if (product.Id.HasManySellers()) { %>
<ul>
<% foreach (var seller in product.Sellers) { %>
<% if (seller.BuyerId == product.MainSupplier) { %>
<li><%= Html.ActionLink(seller.Name, “Details”, “Seller”, new { id = buyer.Id, sellerName = seller.Name.ToUrlSlug() }, null)%></li>
<% } %>
<% } %>
</ul>
</li>
<% } %>
<% else { %> </li> <% } %>
Gross, right? Now imagine maintaining that?
Luckily MVC gives us an easy way to simply that logic, or at least hide it away from unsuspecting front-end developers, and it is all because of the Extension methods we used before.
Since they’re extension methods, it’s really easy to move all the logic from the above into an HtmlHelper method:
{
StringBuilder html = new StringBuilder();
foreach (var product in Model.Products)
{
html.Append(“<ul>”);
html.Append(“<li>”);
html.Append(helper.ActionLink(product.Name, “Show”, “Widget”, new { productId = Model.ProductId, productName = Model.ProductName.ToUrlSlug() });
if (product.Id.HasManySellers())
{
html.Append(“<ul>”);
foreach (var seller in product.Sellers)
{
if (seller.BuyerId == product.MainSupplier)
{
html.Append(String.Format(“<li> {0} </li>”, helper.ActionLink(seller.Name, “Details”, “Seller”, new { id = buyer.Id, sellerName = seller.Name.ToUrlSlug() }, null));
}
}
html.Append(“</ul></li>”);
}
else
{
html.Append(“</li>”);
}
}
}
return MvcHtmlString.Create(html.ToString());
}
It’s messy, but far less messy than having that in the HTML markup, and now the resulting View becomes simply:
which is exactly what we wanted to express, not to mention it vastly improves our relations with the maintenance programmer (or ourselves in six months).
Important things to remember:
*todo, find a .NET tool that does this at compile time.