El Controlador en ASP.NET MVC 4 (I): Enrutado


Anteriormente hemos aprendido el funcionamiento general de la arquitectura MVC de ASP.NET. Sin embargo, hay algunas dudas que se mantienen en el aire, como por ejemplo, cómo sabe MVC a qué controlador debe acudir cada vez que se recibe una petición HTTP.

Redireccionando las peticiones HTTP

Para bien o para mal, el enrutado no es una tarea que pertenezca al framework, sino que es ASP.NET quien debe encargarse de ello en última instancia. Previamente vimos que MVC 4 realiza una redirección hacia un controlador acorde a la ruta que se invoca: si se trataba de la ruta <servidor>/Home/About, comprobamos que se invocaba el método About() del controlador HomeController.

¿Dónde puede configurarse este comportamiento? La respuesta corta es: en el archivo de configuración global, global.asax.cs.

Este fichero contiene, entre otro código, las operaciones a realizar al levantar la aplicación, previas a cualquier petición que pueda realizarse. Entre esas operaciones se encuentra la posibilidad de indicarle a nuestra aplicación cómo se realizarán las redirecciones de las peticiones HTTP. Esto lo podremos observar en la siguiente línea de configuración:


RouteConfig.RegisterRoutes(RouteTable.Routes);

La clase RouteConfig, nueva en MVC 4, encapsula la funcionalidad relacionada con la redirección de peticiones, y se encontrará en App_Start/RouteConfig.cs. Su código mostrará algo como lo siguiente:

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",                        // Nombre de la ruta
                url: "{controller}/{action}/{id}",      // URL con parámetros
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Valores por defecto
            );
        }
 

Como podemos ver, MapRoute define tres parámetros:

  • Nombre de la ruta
  • URL de la ruta, compuesta por un patrón que se divide en controlador/acción/identificador (recordemos que una acción es un método público de un controlador).
  • Valores por defecto en caso de que falte alguno de los parámetros anteriores. En este caso, el controlador por defecto será “Home”, la acción por defecto “Index” y el identificador por defecto tomará el valor UrlParameter.Optional, que representará un parámetro opcional.

En el caso de MVC, el nombre de la ruta añadirá la cadena “Controller” al contenido del atributo {controller}, de modo que si {controller} adquiere el valor de “Home”, el sistema de enrutado buscará un controlador con el nombre HomeController. A continuación buscará la acción (método público) llamada Index() y la ejecutará. El parámetro id, de carácter opcional, puede contener información para, por ejemplo, realizar una consulta en base de datos. Por lo tanto, es muy importante respetar la nomenclatura a la hora de utilizar esta tecnología.

Como nos podremos imaginar a partir de todo esto, estos valores son accesibles desde el controlador, para lo cual deberemos hacer uso de la clase RouteData. Para conocer qué valores contiene este objeto, añadiremos el siguiente código a la acción Index del controlador HomeController():


        public ActionResult Index()
        {
            // Instanciamos un StringBuilder
            StringBuilder sb = new StringBuilder();

            // Le añadimos un salto de linea y un párrafo en HTML
            sb.Append("<br/><p>");

            // Referenciamos el diccionario de información de ruta
            System.Web.Routing.RouteValueDictionary values = RouteData.Values;

            // Extraemos todos los elementos del diccionario de la forma clave: valor
            foreach(String key in values.Keys)
                sb.Append(key).Append(": ").Append(values[key].ToString()).Append("<br/>");

            // Cerramos el párrafo HTML
            sb.Append("</p>");

            // Creamos una cadena de tipo MvcHtmlString para formatear los datos en HTML
            MvcHtmlString htmlString = new MvcHtmlString(sb.ToString());

            // Asociamos el contenido a la propiedad Message del ViewBag
            ViewBag.Message = htmlString;

            return View();
        }

Resumiendo, iteramos sobre el conjunto de pares clave/valor de los datos de redirección y extraemos sus valores para construir una cadena HTML que irá en la propiedad Message del ViewBag. Debemos fijarnos en el hecho de que creamos un objeto de la clase MvcHtmlString en lugar de un String a secas, debido a que si usamos una cadena normal y corriente, ésta se mostrará tal cual, ignorando el código html. El resultado de este experimento será el siguiente si iniciamos la aplicación por defecto:

Si en lugar de limitarnos a la ruta por defecto navegamos manualmente a la ruta <servidor>/Home/Index/Identificador, el aspecto de la página por defecto será el siguiente:

Como podemos comprobar, el primer parámetro corresponde al controlador, el segundo a la acción y el tercero al identificador, que se corresponden con los parámetros indicados en RouteConfig.cs. Si modificáramos el contenido de la ruta a algo como lo siguiente:


    routes.MapRoute(
                name: "Default",                                    // Nombre de la ruta
                url: "{controller}/{action}/{id}/{extrainfo}",      // URL con parámetros
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, extrainfo = UrlParameter.Optional} // Valores por defecto
            );

Podríamos navegar a la ruta <servidor>/Home/Index/Identificador/MasInformacion y obtener el siguiente resultado:

Por lo tanto, vemos que modificando la estructura de enrutado de RouteConfig.cs es posible crear una ruta personalizada indicando los elementos que necesitemos.

Añadiendo nuevas rutas

Los patrones almacenados en RouteConfig.cs realizan las redirecciones con la filosofía first match, es decir, el fichero se recorre secuencialmente buscando el primer mapa que se ajuste a la solicitud. Por lo tanto, la idea de este fichero es la de definir la ruta por defecto como la última de las rutas, especificando los elementos más específicos al principio y dejando para el final los elementos más generales.

Imaginemos que nuestra aplicación está orientada a una tienda de cómics y juegos de mesa, y debe permitir búsquedas de ambos elementos. Podríamos, por lo tanto, crear un controlador llamado Buscar que contuviese dos acciones, una llamada Comics y otra llamada JuegosMesa, que podrían ser invocados del siguiente modo:

  • Buscar/Comics/<Nombre>
  • Buscar/JuegosMesa/<Nombre>

Sin embargo, hay otra forma de realizar esta operación de una forma mucho más compacta: a través de una única acción que reciba como segundo parámetro el tipo de producto, de la forma:

  • Buscar/<tipoProducto>/<Nombre>

En este caso, tipoProducto podría ser un identificador numérico o una cadena de texto que simbolice el tipo de búsqueda a realizar. El código a añadir en el fichero RouteConfig.cs será, por lo tanto, el siguiente:


            routes.MapRoute(
                name: "Buscar",
                url: "Buscar/{tipoProducto}/{nombreProducto}",
                defaults: new { controller = "Productos", action = "Buscar", tipoProducto = "", nombreProducto = "" });

Por lo tanto, este código le dirá a MVC que una petición con el texto “Buscar/Comics/Ironman” sea procesada por el controlador ProductosController, invocando para ello su acción Buscar. Los elementos tipoProducto y nombreProducto se recuperarán en la acción para complementar la búsqueda, realizando, por ejemplo, una búsqueda en base de datos.

Si ejecutamos esta operación obtendremos, como es de esperar, un error indicando que el elemento no existe. Por tanto, deberemos crearlo haciendo click derecho sobre la carpeta Controllers y seleccionando la opción Add… > Controller…

Esto abrirá un diálogo que nos preguntará por el nombre del controlador. Respetando la nomenclatura, llamaremos al controlador ProductosController.

A continuación codificaremos la acción que se encargará de la búsqueda, denominada, en un alarde de originalidad, Buscar. En su interior simularemos la lógica de una búsqueda real, ya que el objetivo de este ejemplo es mostrar el funcionamiento del controlador, no del acceso a base de datos.


        public ActionResult Buscar()
        {
            // Recuperamos los valores tipoProducto y nombreProducto
            String tipoProducto = RouteData.Values["tipoProducto"].ToString();
            String nombreProducto = RouteData.Values["nombreProducto"].ToString();

            // Transformamos el texto plano en HTML para evitar posibles ataques
            tipoProducto = Server.HtmlEncode(tipoProducto);
            nombreProducto = Server.HtmlEncode(nombreProducto);

            // Declaramos una variable para mostrar el resultado
            String resultado = String.Empty;

            // Filtramos por tipo de producto y posteriormente, por su nombre.
            // Este código emulará una consulta a base de datos, que es lo que teóricamente
            // debería realizarse.
            switch (tipoProducto.ToLower())
            {
                case "comics":
                {
                    switch (nombreProducto.ToLower())
                    {
                        case "ironman":
                        {
                            resultado = "Disponibles números 32 (1 unidad), 33 (4 unidades) y 34 (8 unidades)";
                            break;
                        }
                        case "xmen":
                        {
                            resultado = "Disponibles números 182 (2 unidades) y 184 (1 unidad)";
                            break;
                        }
                        case "batman":
                        {
                            resultado = "Disponible número 92 (2 unidades)";
                            break;
                        }
                        default:
                        {
                            resultado = String.Format("El cómic {0} no existe en stock", nombreProducto);
                            break;
                        }
                    }
                    break;
                }
                case "juegosmesa":
                {
                    switch (nombreProducto.ToLower())
                    {
                        case "carcassonne":
                            {
                                resultado = "Disponibles 4 unidades.";
                                break;
                            }
                        case "catan":
                            {
                                resultado = "Disponibles 2 unidades.";
                                break;
                            }
                        default:
                            {
                                resultado = String.Format("El juego de mesa {0} no existe en stock", nombreProducto);
                                break;
                            }
                    }
                    break;
                }
                default:
                    break;
            }

            // En lugar de invocar la vista ProductosView, devolveremos directamente código HTML.
            // Para ello devolvemos una invocación al método Content pasándole directamente como parámetro
            // el texto HTML (o texto plano) a renderizar.
            return Content("<p>" + resultado + "</p>");
        }

Una invocación a Buscar/Comics/Ironman devolverá el siguiente resultado:

Parámetros obligatorios y opcionales

¿Qué ocurriría si, por ejemplo, omitiésemos el parámetro que indica el nombre del producto a buscar? Dado que hemos indicado en la configuración del mapa de ruta que el parámetro es opcional, la petición se resolverá correctamente, entrando por el primer switch anidado (comics) y a continuación finalizando la ejecución por el default del switch interno, mostrando lo siguiente:

Sin embargo, podemos obligar al usuario a indicar un parámetro bajo pena de error. Para ello bastaría con cambiar el código de RouteConfig.cs de la siguiente forma:


        routes.MapRoute(
                name: "Buscar",
                url: "Buscar/{tipoProducto}/{nombreProducto}",
                defaults: new { controller = "Productos", action = "Buscar"});

La misma llamada, bajo esta configuración de ruta, devolvería la siguiente respuesta:

Obteniendo datos en el controlador

Como última modificación a este código, haremos un pequeño inciso en la forma de acceder a la información recibida por el controlador. Hemos visto que el objeto RouteData proporciona, en el diccionario Values, un conjunto de pares clave-valor con los elementos de enrutado. Sin embargo, no es necesario realizar un acceso a este objeto cada vez que queramos leer parámetros en un controlador. Bastará con indicarlos como parámetros de entrada de un método normal y corriente, al cual, además, pueden aplicársele valores por defecto.

Por lo tanto, la forma correcta de leer los parámetros tipoProducto y nombreProducto sería la siguiente:


        public ActionResult Buscar(string tipoProducto, string nombreProducto)
        {
            // Transformamos el texto plano en HTML para evitar posibles ataques
            tipoProducto = Server.HtmlEncode(tipoProducto);
            nombreProducto = Server.HtmlEncode(nombreProducto);
            //...

O, en el caso de querer asignar un valor por defecto, utilizaríamos el siguiente código:


        public ActionResult Buscar(string tipoProducto = "Comics", string nombreProducto = "Batman")
        {
            // Transformamos el texto plano en HTML para evitar posibles ataques
            tipoProducto = Server.HtmlEncode(tipoProducto);
            nombreProducto = Server.HtmlEncode(nombreProducto);

            // ...

El resultado, nuevamente, vemos que será el mismo:

Como última curiosidad, cabe indicar que los parámetros no tienen por qué indicarse separados por barras, sino que puede utilizarse una QueryString de toda la vida, de la siguiente forma:

El framework de MVC, por lo tanto, se encargará de filtrar los elementos recibidos y ajustarlos de forma que el controlador pueda hacerse cargo de ellos.

Anuncios

2 comments

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s