Autor en Google+
Saltar al contenido

Variable not found. Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo de software, internet, u otros temas relacionados con la tecnología. C#, ASP.NET, ASP.NET MVC, HTML, Javascript, CSS, jQuery, Ajax, VB.NET, componentes, herramientas...

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo
de software, internet, u otros temas relacionados con la tecnología

¡Microsoft MVP!
martes, 12 de abril de 2011
La ruta por defecto de ASP.NET MVC es válida para la gran mayoría de escenarios simples, permitiéndonos acceder a las acciones a través de URLs del tipo
http://{servidor:puerto}/{controlador}/{accion}
Así, dada una clase controlador con acciones como las siguientes:

public class InformacionCorporativaController : Controller
{
    public ActionResult QuienesSomos()
    {
        return View();
    }
    public ActionResult MisionVisionYValores()
    {
        return View();
    }
    public ActionResult UneteANuestroEquipo()
    {
        return View();
    }
}

… podríamos acceder a ellas utilizando direcciones tan naturales como /InformacionCorporativa/QuienesSomos, o /InformacionCorporativa/MisionVisionYValores. Sin duda, un gran avance vistas a ofrecer a nuestros usuarios un esquema de rutas limpio, intuitivo, y de paso, mejorar nuestro posicionamiento.

Sin embargo, ¿no os parece que sería mejor aún poder acceder a acciones y controladores utilizando guiones para separar los distintos términos? Me refiero a poder utilizar, por ejemplo, la URL /Informacion-corporativa/Quienes-Somos, o /Informacion-corporativa/Unete-a-nuestro-equipo.

Obviamente no podemos modificar el nombre de nuestros controladores y acciones, puesto que nuestros lenguajes de programación no permiten el uso de guiones en el nombre de los identificadores.

Ante este escenario, una posibilidad sería asignar mediante el atributo [ActionName] un nombre alternativo a cada una de estas acciones. Sin embargo, además del trabajo que esto implica, no habría una forma sencilla de conseguirlo también para los nombres de las clases controlador. Otra opción sería registrar en la tabla de rutas una entrada específica para cada una de las acciones. Aunque no tiene contraindicaciones y cubriría perfectamente nuestras necesidades, uuffff… sin duda es demasiado trabajo a estas alturas.

Afortunadamente, existen en el framework bastantes puntos de extensión donde podemos “enganchar” nuestra propia lógica al proceso de las peticiones, y emplearlos para conseguir solucionar de forma global problemas como este. Y uno de ellos es el sistema de routing.

1. ¡Convenciones al poder!

A continuación vamos a ver cómo conseguirlo de forma global utilizando el sistema de routing, y basándonos en una convención muy simple que vamos a establecer: los identificadores de controladores y acciones que contengan un guión bajo (“_”) serán convertidos a nivel de rutas en guiones.

Dado que el guión bajo es perfectamente válido en los nombre de clases y métodos, sólo tendremos que introducir en ellos este carácter en el lugar donde queremos que aparezcan los guiones, y dejar que el sistema de rutado se encargue de realizar la transformación de forma automática. Y, por supuesto, realizaremos esta transformación en sentido bidireccional, es decir, tanto cuando se analiza la URL para determinar el controlador y acción a ejecutar, como cuando se genera la dirección a partir de los parámetros de ruta.

2. La clase FriendlyRoute

A continuación, vamos a crear una nueva clase, a la que llamaremos FriendlyRoute, y que dotaremos de la lógica de transformación de guiones medios en guiones bajos, y viceversa. Nada que no podamos resolver en una escasa veintena de líneas de código:
public class FriendlyRoute : Route
{
    public FriendlyRoute(string url, object defaults) :
        base(url, new RouteValueDictionary(defaults), new MvcRouteHandler()) { }
 
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var routeData = base.GetRouteData(httpContext);
        RouteValueDictionary values = routeData.Values;
        values["controller"] = (values["controller"] as string).Replace("-", "_");
        values["action"] = (values["action"] as string).Replace("-", "_");
        return routeData;
    }
 
    public override VirtualPathData GetVirtualPath(RequestContext ctx, RouteValueDictionary values)
    {
        values["controller"] = (values["controller"] as string).Replace("_", "-").ToLower();
        values["action"] = (values["action"] as string).Replace("_", "-").ToLower();
        return base.GetVirtualPath(ctx, values);
    }
}

Del código anterior, destacar algunos aspectos:
  • sólo he creado un constructor, he de decirlo, por pura pereza. En realidad, si quisiéramos cubrir más casuística, como la introducción de restricciones o de espacios de nombre que sí permiten las distintas sobrecargas de la clase Route, deberíamos crear todas ellas para nuestra clase. Pero vaya, es trivial en cualquier caso.
  • el método GetRouteData() es utilizado por el framework para obtener los datos de ruta de una petición entrante. Como puede observarse en el código anterior, simplemente ejecutamos la lógica de la clase base, y aplicamos las oportunas transformaciones (de guiones medios a bajos).
  • el método GetVirtualPath() es utilizado para la transformación inversa, es decir, para generar una URL partiendo de valores de los parámetros de ruta. En este caso, primero retocamos ligeramente los valores del nombre de controlador y acción, transformando los guiones bajos en medios y pasándolos a minúsculas, y posteriormente invocamos a la lógica de la clase base para que genere la URL por nosotros.
A partir de este momento ya podríamos añadir en la tabla de rutas objetos de esta clase con un código como el siguiente:
routes.Add("default", 
    new FriendlyRoute(
        "{controller}/{action}/{id}", // URL con parámetros
        new { controller = "inicio", action = "portada_principal", id = UrlParameter.Optional }
    )
);

Observad que una de las ventajas de utilizar esta técnica es que a nivel de código utilizaremos siempre el nombre real de los controladores y acciones (con el guión bajo), es el routing el que realizará las transformaciones. Esto, entre otros beneficios, nos permite seguir utilizando el imprescindible T4MVC para evitar las “magic strings” en nuestro código.

3. Pero es queeee… yo estoy acostumbrado a usar routes.MapRoute()… O:-)

No pasa nada, también podemos ponértelo así de fácil. Los métodos MapRoute() que solemos usar en el método RegisterRoutes del global.asax no son más que extensores de la clase RouteCollection, por lo que podemos basarnos en esta misma idea y crear extensiones personalizadas que nos pongan más a mano el mapeo de nuestras amigables rutas:
public static class RouteCollectionExtensions
{
    public static Route MapFriendlyRoute(this RouteCollection routes, string name, string url, object defaults)
    {
        var route = new FriendlyRoute(url, defaults);
        routes.Add(name, route);
        return route;
    }
}

De esta forma, ahora bastaría con referenciar desde el global.asax el espacio de nombres donde hemos definido la clase anterior y ya podemos llenar la tabla de rutas utilizando sentencias como la siguiente:
routes.MapFriendlyRoute(
    "Default",                    // Nombre de ruta
    "{controller}/{action}/{id}", // URL con parámetros
    new { controller = "inicio", action = "portada_principal", id = UrlParameter.Optional } 
);

Así, una vez introducida la ruta anterior en lugar de la que viene por defecto en proyectos ASP.NET MVC, ya nuestro sistema podrá recibir una petición como GET /Nombre-Controlador/Nombre-Accion y mapearla hacia a la acción Nombre_Accion de la clase controlador Nombre_ControladorController.

Por si os interesa, he dejado código y demo en SkyDrive.


Friendly Route Demo


Publicado en: Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

4 Comentarios:

admin dijo...

Me parece una solución genial cuando se trata de sitios pequeños o hasta medianos.

Para aplicaciones mucho más grandes me parece que el mejor es este acercamiento http://www.kindblad.com/Search-engine-friendly-URLs-in-ASP_NET-MVC

José M. Aguilar dijo...

Gracias por comentar, @admin, y por el aporte!

Saludos!

Yurivan dijo...

Hola y como se haria si tuviera un nivel mas como son las areas, ya que mi aplicacion tiene areas. gracias por responder.

José M. Aguilar dijo...

Hola, Yurivan.

El área que corresponde a una petición se almacena también en la tabla de rutas, en un parámetro llamado "area". El funcionamiento, al menos en teoría, sería similar al mostrado: leer el contenido del parámetro "area", reemplazar los caracteres, y volver a dejarlo en su sitio, p.e., así:

values["area"] = (values["area"] as string).Replace("-", "_");

Un saludo.