Lets say you have an action method to display products in a shopping cart
// ProductsController.cs
public ActionMethod Index(string gender) {
// get all products for the gender
}
Elsewhere, in a masthead that is displayed on every page you are using Url.RouteUrl
to create HREF links to other pages on the site :
<a href="<%= Url.RouteUrl("testimonials-route", new { }) %>" All Testimonials </a>
This testimonials-route
is defined in global.ascx
by the first route below.
Notice that the above call to RouteUrl
does not provide a gender
, but the route is defined with a default of 'neutral' so we'd expect Testimonials.Index("neutral") to be called.
routes.MapRoute(
"testimonials-route",
"testimonials/{gender}",
new { controller = "Testimonials", action = "Index", gender = "neutral" },
new { gender = "(men|women|neutral)" }
);
routes.MapRoute(
"products-route",
"products/{gender}",
new { controller = "Products", action = "Index", gender = (string)null },
new { gender = "(men|women|neutral)" }
);
If someone visits the page /products/women
we get an HREF to /testimonials/women
If someone visits the page /products
then we get an empty HREF (the call to RouteUrl returns null).
But that doesn't make sense does it? testimonials-route
is supposed to default to 'neutral'
for us if we don't provide a route value for it?
What turns out is happening is that Url.RouteUrl(routeName, routeValues)
helper extension will first look in its routeValues
parameter for a gender
route value and if it doesn't find it in that dictionary it will look at the current URL that we're on (remember that Url is a UrlHelper object which has the context of the current request available to it).
This has a possibly nice effect of giving us a link to men's testimonials if we're on a mens product page, but that probably isnt what we want if we haven't passed a value in the RouteUrl
call, and explicitly specified 'neutral' as a default in the global.asax.cs
file.
In the case where we visited /products/
we triggered the 'products-route'
route and the Products(null)
method was called. The call to Url.RouteUrl()
actually inherits THIS null value for gender
when we're creating a URL using testimonials-route
. Even though we have specified a default for gender
in 'testimionials-route
' it still uses this null value which causes the route to fail and RouteUrl
returns null. [note: the route fails because we have a constraint on (men|women|neutral) and null doesn't fit that]
It actually gets more scary - in that 'controller' and 'action' can be inherited in the same way. This can lead to URLs being generated to completely the wrong controller even when calling RouteUrl(...) with an explicit route name that has a default controller.
In this case once you've figured it out you can fix it quite easily in numerous ways, but it could in other cases cause some dangerous behavior. This may be by design, but its definitely scary.
My solution was this :
An HtmlExtension helper method :
public static string RouteUrl(this UrlHelper urlHelper, string routeName, object routeValues, bool inheritRouteParams)
{
if (inheritRouteParams)
{
// call standard method
return urlHelper.RouteUrl(routeName, routeValues);
}
else
{
// replace urlhelper with a new one that has no inherited route data
urlHelper = new UrlHelper(new RequestContext(urlHelper.RequestContext.HttpContext, new RouteData()));
return urlHelper.RouteUrl(routeName, routeValues);
}
}
I can now do :
Url.RouteUrl('testimonials-route', new { }, false)
and know for sure it will always behave the same way no matter what the context.
The way it works is to take the existing UrlHelper
and create a new one with blank 'RouteData'. This means there is nothing to inherit from (even a null value).