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
Espero que te resulten interesantes. :-)
- AvanadeSpain Empleo: ¿Quieres trabajar con Windows Mobile en Sevilla? Ponte en contacto con nosotros
Fecha: 25/02/2011 - campusMVP: en todos los navegadores el fantástico JSON On-Line Viewer (Si usas IE9 ponlo en modo compatibilidad)
Fecha: 25/02/2011 - Buen post! RT Eduard Tomàs: [BlogPost] Rendering de vistas parciales de MVC3 y Razor
Fecha: 25/02/2011 - Scott Guthrie: Nice post about a new DataAnnotations library on NuGet - enable even easier validation in ASP.NET MVC:
Fecha: 24/02/2011 - MongoDB con Norm, por Marc Rubiño.
Fecha: 24/02/2011 - jQuery 1.5 Cheat sheet.
Fecha: 24/02/2011 - ASP.NET MVC custom data annotation validator. Server and Client, by K. Scott Allen
Fecha: 24/02/2011 - campusMVP: 10 plugins de jQuery que te ayudarán a mejorar tus formularios
Fecha: 24/02/2011 - Phil Haack: Changing the base type of a Razor view to add your own properties
Fecha: 21/02/2011
Publicado en: Variable not found
Espero que te resulten interesantes. :-)
- ReSharper 6 Bundles Decompiler, Free Standalone Tool to Follow.
Fecha: 18/02/2011 - Malcolm Sheridan: Get Selected Row from ASP.NET MVC 3 WebGrid ASP.NET MVC
Fecha: 17/02/2011 - Cacheitis, por Rodrigo Corral. Muy bueno :-)
Fecha: 17/02/2011 - Ejemplo de uso de unobstrusive ajax en ASP.NET MVC 3.
Fecha: 17/02/2011 - Webcast de José Manuel Alarcón: Certificaciones Microsoft y sus oportunidades: Webcast gratis
Fecha: 15/02/2011 - Gisela Torres: Bing Maps & Geolocation APIs | Return(GiS)
Fecha: 15/02/2011 - Interesante... David Ebbo: Build your Web Application at runtime
Fecha: 14/02/2011 - David Ebbo: Register your HTTP modules at runtime without config
Fecha: 14/02/2011 - All jQuery UI components now on #NuGet (vía Hadi Hariri)
Fecha: 14/02/2011 - Scott Guthrie: NuGet 1.1 release
Fecha: 14/02/2011
Publicado en: Variable not found
Compilación de vistas ASP.NET MVC y el error “No se pudo cargar el tipo EntityDesignerBuildProvider”
No se pudo cargar el tipo 'System.Data.Entity.Design.AspNet.EntityDesignerBuildProvider'Pero vamos a empezar por el principio, para los que se han incorporado recientemente a ASP.NET MVC. Recordemos que esta característica permite comprobar la corrección de las vistas (Razor o Webforms) en tiempo de compilación, por lo que es bastante útil para la detección temprana de errores que de otra forma sólo podríamos detectar accediendo a la vista en tiempo de ejecución.
Para activarla, basta con abrir el archivo del proyecto MVC (el .csproj o .vbproj) con un editor y cambiar un detalle en la configuración. Esto podemos hacerlo desde cualquier editor tipo bloc de notas, o desde el mismo Visual Studio abriendo el menú contextual sobre el proyecto y seleccionando la opción “Descargar el proyecto”, como vemos en la captura de pantalla:
<MvcBuildViews>false</MvcBuildViews>
e introducir el valor true en su interior. Salvando el archivo, acudimos de nuevo al menú contextual sobre el proyecto, y seleccionamos “Volver a cargar el proyecto”, con lo que ya tendremos activa esta característica. A partir de ese momento, el proceso de construcción del proyecto incluirá la compilación de los archivos de vistas, por lo que podremos ver y corregir los errores detectados en ellas.
El único problema, al que hacía referencia al principio del post, es el extraño error “No se pudo cargar el tipo 'System.Data.Entity.Design.AspNet.EntityDesignerBuildProvider'“ que aparecerá al finalizar la compilación cuando estéis utilizando Entity Framework en el proyecto. Por alguna extraña razón, el hecho de tener activa la compilación de vistas hace que se produzca este error (!).
Bueno, pues si os aparece simplemente debéis añadir la siguiente línea al web.config:
<assemblies> ... <add assembly="System.Data.Entity.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> </assemblies>
De todas formas, ya os adelanto que aunque tengáis un último modelo de máquina, cuando el proyecto tiene cierto volumen váis a notar el tiempo extra a la hora de compilar y, si sois algo impacientes, lo tendréis la mayor parte del tiempo desactivado.
Publicado en: Variable not found.
Json
, también definida en dicho espacio de nombres, que podemos utilizar para codificar objetos en formato JSON (JavaScript Object Notation) y viceversa, crear instancias de objetos partiendo de un texto JSON.El primero de los sentidos, CLR—>JSON, no es algo que no pudiéramos hacer antes utilizando los serializadores Javascript, pero este componente nos lo pone algo más fácil. Por ejemplo, dada la siguiente clase del Modelo:
… y un controlador en el que suministramos a la vista una instancia de dicha clase:
En la Vista podríamos introducir un código como el siguiente para volcar sobre la salida la representación JSON del objeto del modelo. Observad que hasta ahora para conseguir lo mismo teníamos que escribir bastante más código:
Fijaos también que el resultado de
Json.Encode
(bueno, y la alternativa anterior también), debemos pasarlo por una llamada a Html.Raw()
para evitar que sea codificado a HTML antes de enviarlo al cliente. En cualquier caso, el resultado puede ser asignado directamente a una variable, puesto que el marcado generado es la representación JSON del objeto suministrado listo para su uso, como puede observarse en el resultado de procesar la vista anterior:Por otra parte, también podemos utilizar la clase
Json
para hacer la conversión inversa, es decir, construir un objeto del CLR desde un string
que representa un objeto codificado en JSON. Como en el caso anterior, también podíamos utilizar JavascriptSerializer
para conseguirlo, pero ahora podemos hacerlo más rápidamente:Publicado en: Variable not found.
Publicado por José M. Aguilar a las 11:32 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, javascript
Sin embargo, WebGrid también ofrece la posibilidad de recargar (al menos visualmente, como veremos después) únicamente la rejilla de datos, por lo que se consigue un comportamiento muy al estilo Ajax, como el que podemos conseguir con otras soluciones. Y de nuevo de forma realmente sencilla, en dos simples pasos.
En primer lugar, en la instanciación del objeto
WebGrid
, debemos suministrar un valor al parámetro ajaxUpdateContainerId
, indicando el identificador del elemento en cuyo interior generaremos la rejilla de datos, por ejemplo así: WebGrid grid = new WebGrid(
..., // Otros parámetros del constructor
ajaxUpdateContainerId: "contenedor-grid"
Y finalmente, ya dentro del marcado de la vista, debemos asegurar que la llamada a
GetHtml()
del helper se realice dentro de un contenedor con el identificador especificado anteriormente:fillEmptyRows: true,
alternatingRowStyle: "fila-alternativa",
headerStyle: "encabezado-grid",
footerStyle: "pie-grid",
mode: WebGridPagerModes.All,
...
})
</div>
Y eso es todo. Sin necesidad de modificar el Controlador, ni por supuesto el Modelo, las operaciones de desplazamiento entre páginas, cambio de columna o sentido de ordenación, no recargarán la página completa, sino únicamente el contenedor indicado.
Parece magia, eh? ;-)
Tras bambalinas
Pues no, no hay nada mágico, lo que está ocurriendo por detrás es bastante simple: al activarse el modo Ajax, simplemente por el hecho de asignar un valor aajaxUpdateContainerId,
WebGrid modificará la forma de generar los enlaces del paginador y las columnas ordenables, de forma que no guiarán al navegador hacia la acción de refresco, sino ejecutarán un script.En este script se utiliza el método load() de jQuery para solicitar de forma asíncrona la página completa, invocando la misma acción actual, y suministrándole los parámetros que necesita, o sea, la página actual, orden o sentido de ordenación, dependiendo de la operación realizada.
Del resultado de esta petición Ajax (la página completa de nuevo), jQuery seleccionará sólo el fragmento cuyo identificador coincida con el indicado por
ajaxUpdateContainerId
, y sustituirá el contenido del elemento con ese mismo identificador en la vista actual. De esa forma, se habrá actualizado el grid sin necesidad de recargar visualmente la página completa, aunque internamente sí se habrá generado y enviado al cliente completa. La verdad, esta forma de conseguir el efecto Ajax, un poco a lo bestia, recuerda un poco al celebérrimo UpdatePanel :-O.
Y ya puestos a criticar (constructivamente siempre, claro ;-)), seguro que podría haberse conseguido una implementación no intrusiva, de forma que un usuario con los scripts desactivados obtuviese el funcionamiento “por defecto” del Grid, con recarga total de la página.
Pero bueno, al menos es interesante saber que WebGrid cuenta con la posibilidad de ajaxificarse, y conocer el funcionamiento de esta característica, por si en alguna ocasión nos encontramos con ella.
Dejo en SkyDrive un proyecto de demostración de WebGrid en modo Ajax (VS2010+MVC3+SQLExpress), así como los ejemplos de los post anteriores sobre el mismo tema, por si os interesa verlo en funcionamiento.
Publicado en: Variable not found.