pondělí 13. června 2011

Trailing slash in URLs

We are working on an eShop using ASP.Net MVC. The client wanted the URLs to look nice ... no big deal, URLs look nice in MVC, blah.com/detail/15, blah.com/category/45, ...
Not nice enough. We want the name of the article/category in the URL like this: blah.com/15/of_mice_and_men
OK, we are generating all URLs using MVC Futures so there's always some Html.Link("blah", MVC.Detail.Index(Model.ArticleId)) so how can I tweak this to generate that URL?

First thing ... make sure we accept them:

routes.MapRoute(
  "Detail and name",
  "detail/{id}/{name}/",
  new { controller = MVC.Detail.Name, action = MVC.Detail.ActionNames.Index },
  new { id = @"^\d+$" } 
);

next we need to add the name to the RouteDataDictionary somehow. We could add the name as a parameter to the action (even though it doesn't need it) and call MVC.Detail.Index(Model.ArticleId, Model.ArticleName), but we have that on quite a few places already and we do not always have the name handy. Mkay, let's tweak some .tt In this case the T4MVC.tt. And let's do it in a gerenal way ... whenever there is a protected method RenderLink_<ActionName> in the controller, let's call it from the generated pseudoaction in the T4MVC_<ControlerName>Controller in the <ControllerName>Controller.generated.cs and let it tweak the RouteValueDictionary as needed.
Then all that is left is adding


protected void RenderLink_Index(T4MVC_ActionResult callInfo) {
  if (callInfo.RouteValueDictionary.ContainsKey("id") && callInfo.RouteValueDictionary["id"is int) {
   callInfo.AddRouteValue("name"Article.TitleForId((int)callInfo.RouteValueDictionary["id"]).EscapeForNiceUrl());
  }
 }

into the controller and all URLs to details look as requested.

Until they come again that they want a slash at the end of the URL. OK, fine, but how? Including or not the slash in the MapRoute makes no difference. OK, the route object has some GetVirtualPath, is this what I need? I could let the inherited implementation to generate the URL and then append or insert (remember, there may be a query or hash in the URL!) the slash.

Now let's add (copy&paste&tweak) the extension methods to simplify adding the routes


public static class RouteCollectionExtensions {
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url) {
   return MapRouteWithSlash(routes, name, url, null /* defaults */, (object)null /* constraints */);
  }
 
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url, object defaults) {
   return MapRouteWithSlash(routes, name, url, defaults, (object)null /* constraints */);
  }
 
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url, object defaults, object constraints) {
   return MapRouteWithSlash(routes, name, url, defaults, constraints, null /* namespaces */);
  }
 
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url, string[] namespaces) {
   return MapRouteWithSlash(routes, name, url, null /* defaults */null /* constraints */, namespaces);
  }
 
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url, object defaults, string[] namespaces) {
   return MapRouteWithSlash(routes, name, url, defaults, null /* constraints */, namespaces);
  }
 
  public static Route MapRouteWithSlash(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) {
   if (routes == null) {
    throw new ArgumentNullException("routes");
   }
   if (url == null) {
    throw new ArgumentNullException("url");
   }
 
   Route route = new RouteWithSlash(url, new System.Web.Mvc.MvcRouteHandler()) {
    Defaults = new RouteValueDictionary(defaults),
    Constraints = new RouteValueDictionary(constraints),
    DataTokens = new RouteValueDictionary()
   };
 
   if ((namespaces != null) && (namespaces.Length > 0)) {
    route.DataTokens["Namespaces"] = namespaces;
   }
 
   routes.Add(name, route);
 
   return route;
  }
 
 }

and

routes.MapRouteWithSlash(
  "Detail and name",
  "detail/{id}/{name}/",
  new { controller = MVC.Detail.Name, action = MVC.Detail.ActionNames.Index },
  new { id = @"^\d+$" }
 );


and we have the slash :-)

1 komentář: