Pero además de ser una utilidad imprescindible, una de sus características más interesantes es que puede ser extendido con suma facilidad. En este post vamos a ver cómo crear un plugin sencillo que nos permita mostrar en Glimpse información que nos interese sobre nuestra aplicación.
Y como tiene que ser, lo vamos a ver desarrollando un ejemplo paso a paso.
0. Objetivo
Para no distraernos de la intención de este post, que es ver cómo se crean plugins para Glimpse, vamos a desarrollar una extensión muy sencilla, un visor que nos permita observar información sobre el usuario actualmente conectado.El plugin, al que llamaremos “Authentication” se instalará en el panel de Glimpse y mostrará información como la siguiente:
- Nombre de la aplicación
- Nombre del usuario conectado
- Último login
- Roles del usuario en el sistema
Para obtener esta información de forma rápida utilizaremos los sistemas integrados de membresía y funciones (membership y roles) de ASP.NET. Obviamente, podéis utilizar los mismos conceptos que vamos a ver para implementar visualizadores más específicos de vuestra aplicación, como objetos de sesión, caché, e incluso información obtenida desde bases de datos :-)
1. Instalación de Glimpse
Quiero imaginar que todos estáis ya utilizando Nuget a tope (¿verdad?), por lo que no me detendré a explicar cómo instalar esta indispensable herramienta, y comenzaremos directamente instalando Glimpse.Como ya describí en el otro post, la instalación de Glimpse es trivial utilizando la consola de Nuget:
Each package is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages. Some packages may include dependencies which are governed by additional licenses. Follow the package source (feed) URL to determine any dependencies.
Package Manager Console Host Version 1.3.20419.9005
Type 'get-help NuGet' to see all available NuGet commands.
PM> install-package glimpse
[...]
Successfully installed 'log4net 1.2.10'.
Successfully installed 'NLog 1.0.0.505'.
Successfully installed 'Castle.Core 1.2.0'.
Successfully installed 'Castle.DynamicProxy 2.2.0'.
Successfully installed 'Glimpse 0.80'.
Successfully added 'log4net 1.2.10' to DemoGlimpse.
Successfully added 'NLog 1.0.0.505' to DemoGlimpse.
Successfully added 'Castle.Core 1.2.0' to DemoGlimpse.
Successfully added 'Castle.DynamicProxy 2.2.0' to DemoGlimpse.
Successfully added 'Glimpse 0.80' to DemoGlimpse.
PM>
2. Añadir una referencia al proyecto
Para poder desarrollar nuestro plugin para Glimpse necesitamos añadir al proyecto una referencia al ensambladoSystem.ComponentModel.Composition
. Esto es así en la versión actual (v0.80 beta), no sé si más adelante se suprimirá esta molestia que, en cualquier caso, es bastante leve.3. Implementar el plugin
Vamos a pasar directamente a crear el plugin, que veréis que es bastante simple. En primer lugar, añadimos al proyecto una clase que implemente el interfazIGlimpsePlugin
, e implementamos los siguientes miembros definidos en el mismo:void SetupInit(HttpApplication application)
, que contiene código de inicialización que será invocado al cargarse el plugin (una vez por petición), si así se configura expresamente. Por defecto este método ni siquiera será llamado.
string name {get; }
, que debe retornar el nombre de la pestaña que aparecerá integrada en el interfaz de Glimpse.public object GetData(HttpApplication application)
, que retornará un objeto cualquiera con la información que debe ser mostrada por Glimpse en nuestra pestaña. Este objeto será serializado como JSON, y el valor de sus propiedades, tenga la estructura que tenga, mostrada en el panel de la herramienta.
GlimpsePlugin
. La implementación completa del plugin sería la mostrada a continuación.
using System;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using Glimpse.Net.Extensibility;
using System.Web.Security;
namespace Prueba.Utils
{
[GlimpsePlugin()]
public class AuthenticationPlugin: IGlimpsePlugin
{
public object GetData(HttpApplication application)
{
if (!application.Context.User.Identity.IsAuthenticated)
return new { Usuario = "No autenticado" };
MembershipUser usr = Membership.GetUser();
string[] roles = Roles.GetRolesForUser(usr.UserName);
return new
{
ApplicationName = Membership.ApplicationName,
Username = usr.UserName,
Email = usr.Email,
LastLoginDate = usr.LastLoginDate,
Roles = roles.Any()? roles: new[] { "Ninguno" }
};
}
public string Name
{
get { return "Authentication"; }
}
public void SetupInit(HttpApplication application)
{
}
}
}
Observad que el método principal
GetData()
, puede retornar cualquier tipo de objeto (incluidos los de tipo anónimo, como es el caso). Además, fijaos que el tipo de retorno es distinto cuando estamos autenticados y cuando no lo estamos; Glimpse simplemente mostrará en el panel correspondiente el objeto que le suministremos, sea del tipo que sea, recorriendo sus propiedades y visualizándolas de la forma apropiada.4. ¡Lo tenemos!
Voilá, ya tenemos nuestro plugin para Glimpse funcionando y ofreciéndonos la información que necesitamos:Obviamente el ejemplo es muy simple, pero seguro que sois capaces de imaginar escenarios en los que os puede resultar de gran utilidad: ver el contenido de objetos específicos de vuestra aplicación, mostrar trazas personalizadas, consumo de recursos, observar peticiones Ajax, y un larguísimo etcétera.
Siguiendo el ejemplo anterior e implementando a vuestra conveniencia el método de obtención de datos lo tendréis listo sin apenas esfuerzo.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 10:41 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, glimpse
Como la definen sus autores, Glimpse es al servidor lo que Firebug al cliente. En la práctica, se trata de un componente que nos permite obtener desde el navegador una visión en profundidad de lo que está ocurriendo en el servidor durante la ejecución de las peticiones. Brevemente, lo que vamos a conseguir ver con Glimpse es:
- Información sobre configuración de la aplicación
- Información sobre el entorno
- Métodos ejecutados
- Metadatos detallados de las clases del modelo
- Información enviada en la petición
- Tabla de rutas completa
- Valores retornados en las variables de servidor
- Información almacenada en variables de sesión
- Vistas completas y parciales cargadas en la respuesta
- Peticiones Ajax realizadas desde el cliente
Observad que Glimpse ocupa toda la zona inferior, y aunque podría parecer lo contrario, es puro HTML y no un complemento del navegador como ocurre con las herramientas anteriormente citadas. Todo lo que vemos es creado por el componente cliente de Glimpse e introducido en la página actual :-)
Arquitectura
Glimpse está compuesto por los siguientes elementos:- Glimpse Server Module, el componente encargado de recolectar información de depuración en servidor.
- Glimpse Client Side Viewer, encargado de obtener la información de depuración desde el módulo servidor y mostrar el interfaz de usuario que permite su estudio.
- Glimse Protocol, el protocolo que permite el intercambio de información entre servidor y cliente.
Sin embargo, el objetivo final de Glimpse es bastante más ambicioso: aunque la implementación actual está muy ligada a la tecnología .NET, su arquitectura modular hará posible la aparición de componentes de servidor para cualquier tecnología, como PHP o RoR. Éstos se comunicarán con el lado cliente mediante Glimpse Protocol, que es ajeno a la tecnología utilizada en servidor.
El componente de cliente, por su parte, es capaz de obtener datos de depuración generados en servidor y representados en Glimpse Protocol, y mostrarlo integrado en la página. Es importante saber que este cliente está construido sobre jQuery, por lo que la página donde vayamos a utilizarlo debe cargar esta biblioteca de scripts previamente para que todo funcione correctamente.
El diseño del visor se ha conceptualizado, además, para que sea extensible: será posible añadir plugins que den soporte a características avanzadas, como pestañas para mostrar información específica de plataformas o CMS como Sharepoint u Orchad, autenticación para acceder a la información de depuración, etc.
Instalación y puesta en marcha de Glimpse
Este apartado, que describe la instalación de Glimpse en nuestro proyecto ASP.NET MVC o Webforms, podría ser realmente extenso de no ser por Nuget. Glimpse depende de otros paquetes como Castle.Core, Castle.DynamicProxy o Log4net y la descarga e instalación manual de estos componentes podría acabar con la paciencia de cualquiera. Afortunadamente esta joya nos va a poner la tarea bastante fácil :-)Desde la consola Nuget, accesible a través del menú Herramientas > Library Package Manager > Package Manager Console, podemos dejarlo listo en segundos simplemente introduciendo el comando
install-package
como sigue:Each package is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages. Some packages may include dependencies which are governed by additional licenses. Follow the package source (feed) URL to determine any dependencies.
Package Manager Console Host Version 1.3.20419.9005
Type 'get-help NuGet' to see all available NuGet commands.
PM> install-package glimpse
[...]
Successfully installed 'log4net 1.2.10'.
Successfully installed 'NLog 1.0.0.505'.
Successfully installed 'Castle.Core 1.2.0'.
Successfully installed 'Castle.DynamicProxy 2.2.0'.
Successfully installed 'Glimpse 0.80'.
Successfully added 'log4net 1.2.10' to DemoGlimpse.
Successfully added 'NLog 1.0.0.505' to DemoGlimpse.
Successfully added 'Castle.Core 1.2.0' to DemoGlimpse.
Successfully added 'Castle.DynamicProxy 2.2.0' to DemoGlimpse.
Successfully added 'Glimpse 0.80' to DemoGlimpse.
PM>
Tras introducir la orden de instalación, Nuget descargará e instalará el paquete Glimpse, junto con todas sus dependencias, en nuestro proyecto.
El siguiente paso es acudir a la pantalla de configuración de Glimpse con objeto de activarlo para nuestro sitio web. Para ello, simplemente tenemos que lanzar nuestro proyecto y acudir a la dirección /glimpse/config, donde encontraremos el botón de activación “Turn Glimpse On”:
A partir de ese momento, Glimpse estará activo en nuestro proyecto, y podremos comenzar a depurar nuestro sitio Web. Como comentaba al principio, pulsando el icono que aparecerá en la esquina inferior izquierda de todas las páginas, accederemos al visor de información de depuración.
Como en el caso de Firebug, Developer Tools de IE y otros, esta información aparece dividida en pestañas, como se puede apreciar en la siguiente captura de pantalla:
A continuación se describe lo que podemos encontrar en cada una de estas pestañas, exceptuando “Binding”, que aún no está implementada, y “Glimpse warnings”, que muestra avisos internos de la propia herramienta.
Pestaña “Config”
En esta pestaña tendremos acceso a la configuración de la aplicación, o en otras palabras, obtendremos una vista estructurada de determinados parámetros indicados en el web.config. Así, tendremos acceso detallado a parámetros definidos en la secciónappSettings
, cadenas de conexión, configuración del elemento customErrors
y al modo de autenticación utilizado en el sitio. Pestaña “Environment”
Aquí podemos consultar diversos datos sobre el entorno en el que se está ejecutando nuestra aplicación, como el nombre de la máquina, sistema operativo, versión del framework, servidor web, ensamblados cargados, etc.Pestaña “Execution”
Esta interesante pestaña nos muestra información sobre los métodos que han sido ejecutados para procesar la petición, tanto en el controlador como en los filtros aplicables a la acción. Curiosamente, también encontraremos en esta pestaña los métodos no ejecutados en éstos.Como se puede observar en la captura, también ofrece información sobre el orden de ejecución y el tiempo de proceso consumido por de cada uno de estos métodos, lo que nos puede ayudar a detectar cuellos de botella.
Pestaña “Metadata”
Facilita el análisis de los metadatos asociados a la información suministrada desde el controlador a la vista a través de la propiedadModel
. En ellos veremos, con todo lujo de detalles y por cada propiedad las descripciones textuales, reglas básicas de validación y formato, el tipo de datos, etc.Pestaña “Remote”
Aunque no parece funcionar de forma correcta, o al menos no de la forma en que uno podría esperar, esta pestaña permite acceder a las peticiones realizadas por clientes del sitio web, y lanzarlas de nuevo para observar su información de depuración a través del resto de pestañas.Pestaña “Request”
Permite observar los datos enviados en la petición actual en cookies, como campos de formulario, o en la querystring.Pestaña “Routes”
Para mi gusto, una de las pestañas más interesantes que ofrece Glimpse. Nos permite consultar la tabla de rutas completa, conocer cuál de las rutas ha sido elegida para procesar la petición, y los valores para cada uno de sus parámetros.Si ya habéis estado trabajando con la última versión de RouteDebugger de Phil Haack, esta herramienta os sonará bastante, pues se parece bastante. Si todavía no lo habéis hecho, no dejéis de probarla, pues os ayudará mucho cuando os encontréis con peticiones que acaban siendo rutadas aparentemente de forma incomprensible.
Pestaña “Server”
En esta sección tendremos acceso a las variables HTTP enviadas en la respuesta del servidor.Pestaña “Session”
Aquí se nos ofrece información muy interesante: los datos almacenados en variables de sesión para el usuario actual. Podremos ver la clave de búsqueda en la colección Session, el valor almacenado y el tipo de éste.Pestaña “Trace”
En esta pestaña tendremos acceso a la información de depuración generada por la propia aplicación utilizando los métodos de trazas disponibles enSystem.Diagnostics
. Es decir, podemos llamar desde desde el código al método Trace.WriteLine()
para escribir mensajes de depuración que después consultaremos desde este punto.Pestaña “Views”
Esta pestaña nos permite, en primer lugar, consultar información sobre el proceso de localización de las vistas, tanto completas como parciales, por parte de los distintos view engines registrados en el sistema. Como se puede observar en la siguiente captura de pantalla, las dos líneas destacadas con las vistas utilizadas para componer la página actual, el resto son los intentos de localización que se han realizado de forma infructuosa.Además, en cada una de las vistas procesadas tendremos acceso a la información que le está siendo suministrada desde el controlador a través de los diccionarios
ViewData
y TempData
, y de la propiedad Model
:Pestaña “XHRequests”
En ella podremos consultar en tiempo real las peticiones Ajax que están siendo realizadas. Pulsando el enlace “launch” asociado a cada una de ellas, podemos acceder a la información de depuración del proceso de dicha petición accediendo a la pestaña que nos interese.Una curiosidad: si en esta pestaña lo que aparecen son las peticiones Ajax, ¿por qué la han llamado “XHRequest”, y no “Ajax”? La respuesta la encontramos en un twit de uno de sus creadores: en la versión actual del cliente las pestañas salen ordenadas alfabéticamente, y no quería que ésta fuera la primera :-D
Glimpse plugins
Aunque aún es pronto (recordad que Glimpse es todavía una beta), la arquitectura pluggable del producto hará posible la aparición de extensiones del producto que nos permitirán obtener más datos de depuración, y adaptarlo a plataformas específicas.Un ejemplo lo podemos encontrar ya en “Glimpse Dependencies Plugin”, un componente que podemos instalar directamente desde Nuget (“
install-package glimpse-dependencies
”). Se trata de una extensión que añade una pestaña adicional en el cliente, que ofrece información sobre el proceso de resolución de dependencias que utilizan el sistema incorporado en MVC 3:Desinstalación de Glimpse
Una vez hemos acabado con Glimpse, podemos desinstalarlo con total sencillez, de la misma forma que lo instalamos. Utilizaremos esta vez el comandouninstall-package
, e indicaremos que deseamos eliminar también las dependencias que instalamos con él en su momento:PM> Uninstall-Package -RemoveDependencies glimpse
Successfully removed 'Glimpse 0.80' from WebSCCB.
Successfully removed 'Castle.DynamicProxy 2.2.0' from WebSCCB.
Successfully removed 'Castle.Core 1.2.0' from WebSCCB.
Successfully removed 'NLog 1.0.0.505' from WebSCCB.
Successfully removed 'log4net 1.2.10' from WebSCCB.
Successfully uninstalled 'Glimpse 0.80'.
Successfully uninstalled 'Castle.DynamicProxy 2.2.0'.
Successfully uninstalled 'Castle.Core 1.2.0'.
Successfully uninstalled 'NLog 1.0.0.505'.
Successfully uninstalled 'log4net 1.2.10'.
PM>
Tras esta operación, nuestro proyecto volverá a estar igual que como lo teníamos. No me canso de decirlo: Nuget es una maravilla :-)
Conclusión
Sin duda, Glimpse es una herramienta realmente interesante para los que trabajamos con ASP.NET MVC y, aunque actualmente en menor medida, con Webforms, que nos ofrecerá una perspectiva de lo que ocurre en el servidor hasta ahora imposible de obtener de forma tan sencilla, y que nos puede ayudar bastante depurar nuestros sistemas.La facilidad y limpieza con la que la instalamos y desinstalamos en nuestros proyectos gracias a Nuget hace que sea realmente cómodo contar con ella cuando la necesitemos y volver al estado original cuando ya no sea necesaria.
Por poner alguna pega, aún se trata de un producto en beta, y que aún no implementa todas las funcionalidades. También se echa en falta algo de documentación que ayude a averiguar el significado de determinadas opciones, pero entiendo que esto llegará conforme vaya madurando.
En fin: descargadlo, probadlo, y veréis como os gusta.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 10:00 a. m.
Etiquetas: asp.net, aspnetmvc, depuración, herramientas
Seguro que alguna vez habéis intentado acceder a las herramientas de desarrollo de Internet Explorer para depurar un script en una página que estáis ya depurando desde VS, y os ha aparecido un cuadro de diálogo con el error “No se puede conectar al proceso. Debe haber otro depurador conectado al proceso”.
Esto se debe a que Visual Studio, al arrancar un proyecto web en modo debug, se “engancha” a Internet Explorer con objeto de ofrecernos una depuración integrada en el propio IDE entorno, y es la clave de que podamos introducir en scripts puntos de ruptura, observar variables, etc. Personalmente no utilizo casi nunca esta opción integrada, y siempre he preferido usar herramientas externas como Firebug, por lo que la mayoría de las veces para mí no es más que un estorbo.
También en muchos casos habréis observado que al ejecutar una web en modo depuración, en el explorador de soluciones se añade una nueva sección llamada “Documentos de script”, donde aparece el navegador Internet Explorer, y una lista con todos los scripts estáticos y dinámicos que están siendo utilizados en la página actual.
El problema es que en cada cambio de página esta lista de scripts se limpia y se vuelve a recrear, lo que en sitios con un uso intensivo de scripting y que utilicen muchas bibliotecas externas pueden ralentizar considerablemente la ejecución del sistema.
Pues bien, buscando una forma de desactivar este comportamiento que no implicara lanzar el navegador de forma manual, tocar el registro del sistema, ni hacer cosas raras (sobre todo por si más adelante quería dar marcha atrás), he dado con un post antiguo de Vishal Joshi en el que describe cómo conseguirlo de forma muy sencilla, en sólo dos pasos:
- vamos a propiedades del proyecto,
- accedemos a la pestaña "web",
- y activamos el depurador de Silverlight (!)
Al parecer, el depurador de script y el de Silverlight son excluyentes, por lo que el activar este último implica la desactivación del primero.
Espero que os sea de ayuda.
Publicado en: Variable not found.
Hace tiempo traté el tema por aquí, y aporté una solución para la versión 2 de ASP.NET MVC, que aún utilizaba las bibliotecas de scripting de Microsoft Ajax. Sin embargo, la versión 3 ha sustituido “de serie” esos componentes por jQuery Validate y el magnífico sistema de validaciones no intrusivas, por lo que todo lo dicho en aquella ocasión no vale ya para nada :-(
El problema radica en que el plugin jQuery Validate utiliza únicamente el punto como separador de decimales, por lo que la validación en cliente de valores de tipo decimal, float o double que utilicen la coma finalizará siempre en un error e impedirá el envío del formulario, como puede observarse en la captura de pantalla de la derecha.
Por cierto, antes de que se me olvide, hace unos meses reportaron este asunto como bug en Microsoft Connect. Si el tema os preocupa, podéis ir y votarlo a ver si conseguimos que este asunto se tenga en cuenta en próximas revisiones.
Sin embargo, estrictamente hablando, no se trata de un bug de ASP.NET MVC, puesto que la validación en cliente ha sido delegada por completo al plugin de jQuery, y éste es el que no tiene en cuenta los aspectos de internacionalización. Desde este punto de vista, quizás tendría más sentido, por tanto, esta issue reportada en Github sobre jQuery Validate, que propone su integración de forma nativa con jQuery Global.
Por tanto, me temo que se trata de un asunto de responsabilidad compartida (y dispersa, por tanto) entre los equipos de MVC, de jQuery Validate, y no sé si de alguno más. Esperemos que entre todos puedan solucionar de forma razonable el problema.
En cualquier caso, los que ya estamos creando aplicaciones con MVC 3 no podemos esperar las soluciones oficiales, que seguro llegarán más tarde o más temprano, y nos vemos obligados a buscar alternativas que nos permitan convivir con este problema de la forma más cómoda posible.
Y esto es lo que veremos en este post: varias posibilidades que tenemos para que la validación en cliente de valores decimales no nos compliquen demasiado la vida. Seguro que hay más, seguro que las hay mejores, pero ahí van unas cuantas opciones que nos pueden ayudar en escenarios como el descrito anteriormente.
1. Desactivar la validación en cliente
Está claro que el problema es en cliente, por lo que si desactivamos estas validaciones y dejamos que sea el servidor el que se encargue de comprobar que los valores de los distintos campos cumplen las restricciones impuestas por su tipo y las anotaciones de datos, ya no nos afectará más la absoluta indiferencia de jQuery Validate hacia las particularidades culturales.Esto podemos conseguirlo de varias formas:
- desactivar la validación en cliente de forma global, estableciendo a
false
la propiedadclientValidationEnabled
en el web.config, lo cual dejará a toda la aplicación sin validaciones en cliente. Como solución es algo drástica, pero poderse se puede. - desactivar la validación en cliente de forma local, sólo en aquellos formularios en los que existan propiedades de tipo decimal, introduciendo el siguiente código Razor (o su correspondiente en ASPX) antes de la llamada a
BeginForm()
:@{ Html.EnableClientValidation(false); }
- desactivar la validación en cliente sólo en el campo que nos interese, que podemos conseguir introduciendo el siguiente script, imaginando que el campo decimal en el que queremos anular la validación en cliente tiene como identificador “Altura”:
<script type="text/javascript"> $("#Altura").removeAttr("data-val"); </script>
2. Modificar jQuery Validate
Esta es una solución algo bestia que he encontrado por ahí, pero soluciona el problema de un plumazo: modificar el código de jQuery Validate para que acepte comas en lugar de puntos para separar los dígitos decimales de los enteros tanto en la validación numérica como en los rangos.En el blog de Lenard Gunda podéis encontrar de forma muy detallada los cambios a realizar al archivo jquery.validate.js (o a su versión minimizada). Hay, sin embargo, un par de detalles que debemos tener en cuenta si optamos por esta solución:
- primero, que nos estamos separando de la distribución oficial del plugin. Si actualizamos la biblioteca jquery.validate, por ejemplo utilizando Nuget, volveremos a tenerlo todo como al principio, y tendremos que volver a introducir los cambios oportunos.
- segundo, que esto no nos ayudará en aplicaciones adaptadas a varios idiomas; si modificamos el plugin para que acepte comas como separador, ya no volverá a aceptar el punto. Una solución rápida que se me ocurre para esto es tener dos versiones de la biblioteca (la original y la modificada), y referenciar desde la página la apropiada para la cultura actual.
3. Modificar la forma en que jQuery Validate parsea los decimales
Afortunadamente, el plugin de validación para jQuery es muy flexible, y permite introducir código personalizado para la validación de formato numérico y comprobación de rangos, lo que nos brinda la posibilidad de solucionar nuestro problema de forma muy limpia.El siguiente código sería una primera aproximación a la solución del problema. Como podéis observar, simplemente introducimos en
$.validator.methods.number
y $.validator.methods.range
las funciones que queremos utilizar para validar respectivamente los números y los rangos, reemplazando la coma por el punto antes de realizar la conversión con parseFloat()
:<script type="text/javascript">
$.validator.methods.number = function (value, element) {
value = floatValue(value);
return this.optional(element) || !isNaN(value);
}
$.validator.methods.range = function (value, element, param) {
value = floatValue(value);
return this.optional(element) || (value >= param[0] && value <= param[1]);
}
function floatValue(value) {
return parseFloat(value.replace(",", "."));
}
</script>
Si incluimos este script en la página cuando la cultura activa sea la nuestra (o cualquier otra que también utilice la coma para separar decimales), tendremos el problema solucionado.
Una fórmula más elegante y universal sería modificar la función
floatValue()
, y en lugar de reemplazar de forma manual los caracteres, utilizar el plugin Global para realizar la conversión a flotante según la cultura actual. Los detalles de esto, sin embargo, los dejo para otro post.En fin, que como habréis comprobado existen mil y un enfoques posibles para enfrentarnos al problema. Espero que las ideas que hemos ido comentando os sean de utilidad para implementar vuestras propias soluciones hasta que tengamos una vía “oficial” para conseguirlo.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 11:58 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, jquery, localizacion, trucos, validadores
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 llamaremosFriendlyRoute
, 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.
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étodosMapRoute()
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.
Publicado en: Variable not found.
[DisplayName]
nos permite especificar en cada propiedad de las entidades del Modelo un nombre descriptivo que, entre otras cosas, es utilizado como etiqueta en formularios si empleamos el helper Html.LabelFor().
Así, dada una clase del Modelo como la siguiente:
public class Persona
{
[DisplayName("Nombre completo")]
public string Name { get; set; }
[DisplayName("Edad actual")]
public int Age { get; set; }
}
si implementamos una vista de edición sobre ella similar a la mostrada a continuación, vemos el resultado que obtendríamos en tiempo de ejecución: Este mecanismo resulta muy cómodo y útil la mayoría de las veces, sin embargo, cuando estamos desarrollando sitios multiidioma, rápidamente vamos a encontrarnos con que este enfoque no es suficiente, puesto que asocia una única descripción textual a la propiedad independientemente de la cultura activa.
A diferencia de otros atributos como las anotaciones de validación,
DisplayName
no incorpora ninguna forma para indicar una referencia hacia el recurso localizado, es decir, identificar el tipo (la clase) que representa al archivo de recursos (.resx) donde se encuentra la traducción del término, así como la propiedad o clave en la que lo encontraremos. Por ejemplo, el data annotation [Required]
permite hacerlo de la siguiente forma:[Required(
ErrorMessageResourceName="NombreObligatorio",
ErrorMessageResourceType=typeof(Resources.Textos)
)]
public string Name { get; set; }
Ahora bastaría con crear la carpeta App_GlobalResources un archivo de recursos llamado “Textos.resx” (obviamente el nombre puede ser cualquiera, siempre que coincida con el referenciado en el parámetro ErrorMessageResourceType
anterior), e introducir en él la clave “NombreObligatorio” con su correspondiente valor. Para añadir la traducción al inglés, bastaría con hacer lo mismo sobre el archivo Textos.en.resx, y así sucesivamente. Afortunadamente, a partir de ASP.NET MVC 3 podemos utilizar la anotación
Display
, disponible en System.ComponentModel.DataAnnotations
a partir de la versión 4 del framework, de la siguiente forma:[Display(Name="Nombre", ResourceType=typeof(Resources.Textos))]
public string Name { get; set; }
Como podréis intuir, el parámetro Name
indica la clave del recurso incluido en la clase especificada en ResourceType
.Sin embargo, el uso de este atributo requiere que los elementos del archivo de recursos sean de acceso público, y no interno, como son por defecto cuando creamos los .resx en App_GlobalResources. Si no tenemos en cuenta este aspecto, podemos encontrarnos con un error como el siguiente:
Ante este error, tenemos varias fórmulas para conseguir acceso a los recursos localizados:Cannot retrieve property 'Name' because localization failed. Type 'Resources.Textos' is not public or does not contain a public static string property with the name 'Nombre'.
- la primera de ellos a lo bestia, accediendo a la clase generada desde el archivo de recursos (por ejemplo
Textos.Designer.cs
) y cambiando los modificadores de la clase y sus miembros de “internal” a “public”. Obviamente, este cambio sólo nos vale hasta que se vuelva a generar el código , cosa que ocurrirá al modificar o añadir nuevas cadenas de texto, por lo que no es nada recomendable. - otra, sacar los recursos de la carpeta App_GlobalResources y moverlos a cualquier otra carpeta del proyecto (por ejemplo, /Recursos). Tras ello, en las propiedades del archivo .resx podemos modificar la “Herramienta personalizada” y establecerla a “PublicResXFileCodeGenerator” para asegurar su acceso público.
- una última sería crear ensamblados satélite, lo cual requiere la añadir un nuevo proyecto de biblioteca de clases en la solución, en el que introduciremos exclusivamente los archivos de recursos localizados.
[LocalizedDisplayNameAtribute]
Es realmente sencillo implementar un atributo que nos permita conseguir algo parecido a lo descrito anteriormente. Simplemente heredando deDisplayNameAttribute
y sobrescribiendo su propiedad DisplayName
podemos hacer que su valor sea tomado desde los recursos de la aplicación: public class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
private readonly string _resourceName;
public LocalizedDisplayNameAttribute(string resourceName)
{
_resourceName = resourceName;
}
public override string DisplayName
{
get { return Resources.Textos.ResourceManager.GetString(_resourceName); }
}
}
Observad que para simplificar la sintaxis de uso y su implementación, estamos asumiendo que los recursos se encuentran en Resources.Textos
, la clase generada automáticamente a partir del archivo Textos.resx. De esta forma, ahora decoraremos las propiedades del modelo no con el texto que queremos que aparezca en pantalla, sino con la clave del recurso, y dado que la clase de recursos está definida en el atributo, no será necesario indicarla:
public class Persona
{
[LocalizedDisplayName("Nombre")]
public string Name { get; set; }
[LocalizedDisplayName("Edad")]
public int Age { get; set; }
}
Y por si os resulta de interés, he dejado un proyecto de demostración de este atributo en Skydrive. Publicado en: Variable not found.
App_GlobalResources
para ir componiendo los interfaces.De esta forma, los literales de texto de las vistas son sustituidos por expresiones que, ya en tiempo de ejecución, son tomadas del archivo de recursos correspondiente al idioma actual:
Sin embargo, recientemente me he encontrado con un escenario en el que, para facilitar el mantenimiento de los contenidos de las vistas, me resultaba mucho más cómodo disponer de versiones distintas de las mismas en sus correspondientes archivos Razor (.cshtml), y seleccionar en tiempo de ejecución cuál de ellas debía ser enviada en función de la cultura del usuario. Eso sí, en caso de no existir una vista específica para la cultura actual, era necesario retornarla en el idioma por defecto.
La idea, como se muestra en la captura de pantalla de la derecha, es modificar ligeramente la convención de nombrado de archivos de vistas del framework para incluir el código del idioma en que está escrito su contenido.
Así, en este caso, asumiremos que las vistas localizadas se llamarán siempre de la forma {vista}_{idioma}. Por ejemplo, la traducción al inglés de la vista “about” estará definida en el archivo “about_en.cshtml” (usando Razor), al francés en “about_fr.cshtml”, y así sucesivamente. Por simplificar, utilizaremos sólo los dos primeros caracteres del idioma, aunque podrían ser referencias culturales completas, como “about_es-es.cshtml”.
Una vez definida la nueva convención, sólo nos falta implementar en el controlador el mecanismo de selección de vistas en función de la cultura actual. En un primer acercamiento, podríamos implementarla directamente sobre las acciones, como en el siguiente ejemplo:
public ActionResult About()
{
string currentLang = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
var viewResult = ViewEngines.Engines.FindView
(ControllerContext, "About_" + currentLang, null);
if (viewResult.View == null)
return View("About_es");
return View("About_" + currentLang);
}
Aunque funcionalmente es correcto, es fácil ver que la introducción de esta lógica contaminaría bastante el código del controlador, además de atentar contra el principio DRY en cuanto tuviéramos varias acciones en las que introducirlo. Obviamente, debemos encontrar otra solución más apropiada y elegante a esta cuestión.Y como siempre, en ASP.NET MVC existen multitud de fórmulas para resolver problemas de este tipo. En este post vamos a ver dos de ellas: extendiendo el conjunto de ActionResults, y mediante filtros personalizados.
Usando ActionResults
Empezando por el final, el resultado que vamos a lograr es el que podemos ver en el siguiente código de controlador:public ActionResult About()
{
return LocalizedView();
}
LocalizedView()
no es sino un método rápido para instanciar un LocalizedViewResult
, cuya implementación veremos a continuación. La lógica que incluye utilizará los motores de vistas para buscar una vista compuesta por el nombre indicado como parámetro (por defecto el nombre de la acción actual) y la cultura establecida en el hilo de ejecución; de no encontrarse, se mostrará la vista correspondiente a la cultura por defecto.El código de este ActionResult es el siguiente:
public class LocalizedViewResult : ViewResult
{
private string _defaultCulture;
public LocalizedViewResult(string defaultCulture)
{
_defaultCulture = defaultCulture;
}
public override void ExecuteResult(ControllerContext context)
{
string currentCulture = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
if (string.IsNullOrWhiteSpace(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
var viewResult = ViewEngines.Engines.FindView(context, this.ViewName + "_" + currentCulture, null);
if (viewResult.View == null)
ViewName += "_" + _defaultCulture;
else
ViewName += "_" + currentCulture;
base.ExecuteResult(context);
}
}
Como se puede observar, el método
ExecuteResult()
obtiene el idioma actual y comprueba si existe una vista que siga la convención de nombrado que hemos definido previamente (vista_idioma.cshtml). Si se encuentra, se anexa al nombre original de la vista el sufijo del idioma; en caso contrario se actúa de la misma forma, pero utilizando el nombre por defecto.Por último, para hacer más rápido su uso desde las acciones, necesitamos implementar el método
LocalizedView()
en una clase base de la que deberíamos heredar nuestros controladores:public class ControladorBase: Controller
{
public LocalizedViewResult LocalizedView(string viewName = null, object model = null,
string master = null, string defaultLang = "es")
{
if (model != null)
{
ViewData.Model = model;
}
return new LocalizedViewResult(defaultLang)
{
ViewName = viewName,
MasterName = master,
ViewData = ViewData,
TempData = TempData,
};
}
}
Obviamente, el método
LocalizedView()
sólo podría ser utilizado desde controladores descendientes de ControladorBase
. Si preferís no heredar de un controlador base, siempre podríais crearlo como método extensor de Controller
, aunque entonces deberíais invocarlo con el this
por delante, y queda algo menos elegante.Las siguientes acciones muestran posibles usos de esta implementación. Como se puede observar, son bastante explícitas y dejan clara su intencionalidad:
public ActionResult Prueba()
{
return LocalizedView("About");
}
public ActionResult About()
{
return LocalizedView();
}
Usando filtros
Los filtros presentan una solución bastante menos intrusiva al problema y, para mi gusto bastante mejor, puesto que no obligaría a modificar la implementación tradicional de las acciones. Para hacernos una idea, lo que pretendemos conseguir es lo siguiente:[LocalizedView]
public ActionResult About()
{
return View();
}
El simple hecho de decorar una acción con el atributo LocalizedView
provocará que el retorno de la acción sea analizado, y se ejecute la lógica de selección de vistas ya descrita anteriormente. Además, el hecho de ser un filtro hace posible su aplicación a controladores completos, o incluso su aplicación global registrándolo en la inicialización de la aplicación.El código del filtro personalizado es el siguiente:
public class LocalizedViewAttribute : ActionFilterAttribute
{
private string _defaultLang;
public LocalizedViewAttribute(string defaultLang = "es")
{
_defaultLang = defaultLang;
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null)
{
if (string.IsNullOrWhiteSpace(viewResult.ViewName))
{
viewResult.ViewName = filterContext.RouteData.GetRequiredString("action");
}
string currentLang = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
var v = ViewEngines.Engines.FindView(
filterContext.Controller.ControllerContext,
viewResult.ViewName + "_" + currentLang, null
);
if (v.View == null)
viewResult.ViewName += "_" + _defaultLang;
else
viewResult.ViewName += "_" + currentLang;
}
base.OnResultExecuting(filterContext);
}
}
En el código se puede observar que en primer lugar se comprueba que el resultado de la acción sea de tipo ViewResultBase
; en caso contrario, es decir, cuando la acción no retorne una vista, simplemente se ejecutará la lógica por defecto. A continuación, ya asumiendo que la acción retorna una vista, puede observarse la aplicación de la convención de nombrado cuando el nombre de la misma sea nulo, tomándolo de la acción actual. Finalmente, tras comprobar la existencia o no de la vista localizada, se modifica el valor de la propiedad
ViewName
del resultado, que identifica la vista que finalmente será enviada al cliente.Desde el punto de vista de la implementación de las acciones queda prácticamente igual de claro que usando la técnica del
ActionResult
, pero en cambio, ésta requiere menos andamiaje. A continuación, dos posibles ejemplos de uso de este atributo:[LocalizedView]
public ActionResult About()
{
return View();
}
[LocalizedView("en")]
public ActionResult Prueba()
{
return View("About");
}
Y finalmente, por si os interesara ver todo esto en funcionamiento, he dejado un proyecto de demostración en Skydrive (requiere VS2010+MVC 3).
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 9:47 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, localizacion, trucos