January 2011 - Artículos

Muy buenas! En el post anterior comenté la característica de los templates de Razor y hoy vamos a ver como podríamos crear un helper que emule un poco el control Repeater que hay en webforms (salvando las distancias, claro).

Vamos a crear un helper externo, es decir que sea reutilizable en distintos proyectos: para ello nuestro helper va a residir en una clase (en mi ejemplo en el propio proyecto web, pero se podría situar en una librería de clases para ser reutilizable).

Esqueleto inicial

A nuestro helper se le pasará lo siguiente:

  1. Una cabecera: template Razor que se renderizará una sola vez al principio.
  2. Un cuerpo: template Razor que se renderizará una vez por cada elemento
  3. Un pie: template Razor que se renderizará una sola vez al final.
  4. Una colección de elementos (por cada elemento se renderizará el cuerpo).

Recordáis que ayer comentamos que los templates Razor son Func<T, HelperResult>?

Bien, pues vamos a declarar nuestro helper:

 

public static IHtmlString Repeat<TItem>(
IEnumerable<TItem> items,
Func<dynamic, HelperResult> header,
Func<TItem, HelperResult> body,
Func<dynamic, HelperResult> footer
)
{
var builder = new StringBuilder();
if (header != null)
{
builder.Append(header(null).ToHtmlString());
}

foreach (var item in items)
{
builder.Append(body(item).ToHtmlString());
}
if (footer != null)
{
builder.Append(footer(null).ToHtmlString());
}

return MvcHtmlString.Create(builder.ToString());
}

Los templates siempre están declarados como Func<T, HelperResult>, en este caso declaramos tres templates:

  1. header, cuyo parámetro @item es dynamic.
  2. body, cuyo parámetro @item es TItem, donde TItem es el tipo del enumerable que se le pasa al helper
  3. footer, cuyo parámetro @item es dynamic

Para invocar los templates y obtener el html asociado simplemente invocamos el Func (pasándole el parámetro del tipo correcto) y sobre el resultado llamamos a ToHtmlString: Este método nos devuelve la cadena HTML que ha parseado Razor.

Ahora p.ej. me creo una acción tal en el controlador:

public ActionResult List()
{
return View(new List<Product>
{
new Product() {Nombre="PS3", Precio=300},
new Product() {Nombre="XBox360", Precio=150},
new Product() {Nombre="Wii", Precio=100}
});
}

Y en la vista asociada:

@using MvcApplication14.Helpers
@model IEnumerable<MvcApplication14.Models.Product>

<table>
@Repeater.Repeat(Model,
@<thead><tr><td>Nombre</td><td>Precio</td></tr></thead>,
@<tr><td>@item.Nombre</td><td>@item.Precio</td></tr>,
null)
</table>

Fijaos en la llamada a Repeater.Repeat, donde le paso el modelo que recibe la vista (que es un IEnumerable de Product) y los tres templates Razor (en este caso no le paso footer y por ello pongo null).

En el segundo template (body) puedo acceder al elemento que se está renderizando mediante @item. Además, dado que en helper he declarado el template body de tipo Func<TItem, HelperResult>, tengo soporte de Intellisense:

image

Si teclease @item en el template header o footer no tendría intellisense porque los he declarado dynamic en el helper (además ojo, que en el helper le paso null, por lo que rebentaría si usamos @item en el template header o footer).

Y listos! El código HTML generado es:

<thead><tr><td>Nombre</td><td>Precio</td></tr></thead>
<tr><td>PS3</td><td>300</td></tr>
<tr><td>XBox360</td><td>150</td></tr>
<tr><td>Wii</td><td>100</td></tr>

Guay, no?

Podríamos “complicar” un poco el helper, para que desde los templates supiesemos si estamos en una fila par o impar y así aplicar clases… Para ello nos basta con declarar una clase adicional en nuestor helper:

public class RepeaterItem<TItem>
{
public int Index { get; private set; }
public bool IsEven { get { return Index % 2 == 0; } }
public TItem Item { get; private set; }

public RepeaterItem(TItem item, int index)
{
Index = index;
Item = item;
}
}

Esa clase simplemente contiene el índice del elemento actual, el propio elemento y una propiedad que indica si es par o no.

Ahora modificamos el helper para pasarle al template body un objeto RepeaterItem<TItem>:

public static class Repeater
{
public static IHtmlString Repeat<TItem>(
IEnumerable<TItem> items,
Func<dynamic, HelperResult> header,
Func<RepeaterItem<TItem>, HelperResult> body,
Func<dynamic, HelperResult> footer
)
{
var builder = new StringBuilder();
if (header != null)
{
builder.Append(header(null).ToHtmlString());
}

var count = 0;
foreach (var item in items)
{
var repeaterItem = new RepeaterItem<TItem>(item, count++);
builder.Append(body(repeaterItem).ToHtmlString());
}
if (footer != null)
{
builder.Append(footer(null).ToHtmlString());
}

return MvcHtmlString.Create(builder.ToString());
}
}

Los cambios básicamente son declarar el template domo Func<RepeaterItem<Titem>> y cuando invocamos el template, crear antes un objeto RepeaterItem y pasárselo como parámetro.

Finalmente ahora debemos modificar la vista, ya que el parámetro @item de nuestro template es ahora un RepeaterItem<TItem>:

@Repeater.Repeat(Model, 
@<thead><tr><td>Nombre</td><td>Precio</td></tr></thead>,
@<tr style="@(item.IsEven ? "background-color: white" : "background-color:pink")">
<td>@item.Item.Nombre</td><td>@item.Item.Precio</td></tr>,
null)

Fijaos como dentro del template body puedo preguntar si el elemento actual es par (@item.IsEven y acceder al propio elemento @item.Item). En este caso uso @item.IsEven para cambiar el color de fondo de la fila:

image

Y listos! Espero que esos dos posts sobre templates Razor os hayan parecido interesantes!

Un saludo a todos! ;-)

con 3 comment(s)
Archivado en:

Muy buenas!

En este post quiero comentaros una característica de Razor que yo considero que es una auténtica pasada: los templates.

Básicamente el meollo de todo está en la posibilidad de guardar el resultado de un parseo de Razor en un Func<T, HelperResult> siendo T el tipo del modelo que renderiza el template.

Veámoslo con código:

@{
Func<string, HelperResult> h =
@<h2>Esto es un template al que se le han pasado los datos: @item</h2>
;
}
<p>
Renderizamos el template: @h("datos")
</p>

Fijaos como nos guardamos en una Func<string, HelperResult> el resultado de renderizar un template razor (en este caso el template <h2>…</h2>). Fijaos en tres detalles:

  1. Dado que la variable h está declarada como Func<string, HelperResult> el tipo del modelo en este template es “string”
  2. Para acceder al modelo que se pasa al template se usa @item
  3. Al final del template Razor ponemos un ; (eso es una declaración C# y como tal debe terminar en punto y coma).

Luego más adelante renderizamos el template, con el código: @h("datos")

Eh!! Eso no es lo mismo que @helper?

Si conoces @helper te puede parecer que esto no es muy novedoso. Con @helper podemos crear helpers inline de la siguiente manera:

@helper h2(string s) {<h2>Esto es un helper: @s</h2>}

Y lo podíamos invocar con:

@h2("ufo")

Por lo que parece que viene a ser lo mismo. Y es que, en el fondo, ambas construcciones devuelven un HelperResult.

Nota: Si quieres más información sobre @helper, léete el post que hizo el maestro hace algún tiempecillo: http://www.variablenotfound.com/2010/11/y-mas-sobre-helpers-en-razor.html

Lo interesante no es que ambas construccione se parezcan, lo que quiero recalcar es que…

… Los templates son Func<T, HelperResult>!

Lo pongo así en negrita porque eso es importante, y a la vez me sirve de título. Si los templates Razor son Func<T, HelperResult>… cualquier método que reciba un Fun<T, HelperResult> puede recibir un template Razor

… Incluídos los helpers!

@helper Repeater(int count,Func<dynamic, HelperResult> template, string data) {
<ul>
@for (int idx=0; idx<count; idx++)
{
<li>@template(new { Text = data, Index = idx })</li>
}
</ul>
}

@Repeater(10, @<span>@item.Text (item: #@item.Index)</span>, "Ufo")

En este código:

  1. Declaramos un helper, llamado Repeater que acepta tres parámetros:
    1. Un entero
    2. Un Func<dynamic, HelperResult> que por lo tanto podrá ser un template Razor
    3. Una cadena
  2. El helper se limita a crear una lista y luego:
    1. Crea tantos <li> como indica el paràmetro count y en cada id
      1. Evalúa el segundo parámetro (que es el Func) y le pasa como parámetro un objeto anonimo con dos propiedades (Text e Index), donde Text es el tercer parámetro que recibe (y index el valor de la iteración actual).
  3. Cuando invocamos al helper, le pasamos como primer parámetro el número de repeticiones, como segundo parámetro un template de Razor. Fijaos que dentro de dicho template:
    1. Accedemos a las propiedades @item.Text y @item.Index. Eso podemos hacerlo porque hemos declarado el tipo del modelo de dicho template como dynamic y por eso nos compila (y nos funciona porque el helper cuando invoca el template crea esas propiedades en el objeto anónimo).

El código HTML generado por dicha llamada al helper Repeater es:

<ul>
<li> <span>Ufo (item: #0)</span></li>
<li> <span>Ufo (item: #1)</span></li>
<li> <span>Ufo (item: #2)</span></li>
<li> <span>Ufo (item: #3)</span></li>
<li> <span>Ufo (item: #4)</span></li>
<li> <span>Ufo (item: #5)</span></li>
<li> <span>Ufo (item: #6)</span></li>
<li> <span>Ufo (item: #7)</span></li>
<li> <span>Ufo (item: #8)</span></li>
<li> <span>Ufo (item: #9)</span></li>
</ul>

Espero que el post os haya resultado interesante!!! ;-)

Saludos!

con 6 comment(s)
Archivado en:

Muy buenas! Hace algunos días escribí el post ASP.NET MVC: Como recuperar datos de una cookie en cada petición, donde mostraba el uso de un route handler propio para recuperar los datos de una cookie y colocarlos en el Route Data. En el ejemplo era una cookie de cultura de la aplicación, pero se puede aplicar a lo que queráis.

Lo que más me gusta de ASP.NET MVC es que muy expandible, que muchas cosas pueden hacerse de más de una forma. Pues bien, una de las novedades más interesantes de MVC3 (al margen de Razor) son los action filters globales.

En este post os propongo una solución alternativa (aunque ya veremos que tiene una ligerísima diferencia) al mismo problema. La diferencia es que no se debe alterar la tabla de rutas para nada. Y dicha solución pasa por usar un action filter global.

Una de las cosas que en MVC nos debe quedar claro es que cuando repitamos muchas veces un mismo código de un controlador debemos considerar de ponerlo en un Action Filter. El “problema” está que los action filters deben aplicarse controlador a controlador (o acción a acción). Si tenemos un filtro que debe aplicarse a todos los controladores podemos considerar crear una clase base que lo tenga y heredar todos los controladores de ella…

… o al menos eso era así antes de MVC3.

Con MVC3 y los filtros globales podemos aplicar un filtro a todas las acciones de todos los controladores. Y todo ello con una sola línea en global.asax. Es brutal!

El filtro global…

Lo bueno es que los filtros globales se implementan igual que los filtros no globales clásicos que teníamos en MVC2. En este caso la implementación es super sencilla:

public class CookieCultureFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var cultureCookieVal = GetCultureFromCookie(filterContext.HttpContext.Request.Cookies);
var culture = new CultureInfo(cultureCookieVal);
filterContext.RouteData.Values.Add("culture", cultureCookieVal);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}

private string GetCultureFromCookie(HttpCookieCollection cookies)
{
var retValue = "ca-ES";
if (cookies.AllKeys.Contains("userculture"))
{
retValue = cookies["userculture"].Value;
}
return retValue;
}
}

El código es trivial: derivo de ActionFilterAttribute (clase que ya existía) y redefino el método OnActionExecuting que se ejecuta antes de ejecutar la acción del controlador. En este método tengo el código para leer la cookie de cultura (exactamente lo mismo que tenía antes en el Route Handler).

Activar el filtro global

Os dije que era una sóla línea en global.asax, verdad? Pues concretamente esta:

GlobalFilters.Filters.Add(new CookieCultureFilterAttribute());

Otra opción es colocar esa línea dentro de la función RegisterGlobalFilters que crea el VS2010, aunque entonces no es necesario usar la clase GlobalFilters (usad en su lugar el parámetro filters):

filters.Add(new CookieCultureFilterAttribute());

Y ya está. Nada más. Antes de cada acción de cualquier controlador se ejecutará el código de nuestro filtro global. No es necesario modificar la tabla de rutas para añadir nuestro route handler.

¿Puedo usar el filtro en una aplicación MVC2?

Si, si que puedes, pero entonces debes:

  1. Aplicarlo en cada controlador (usando [CookieCultureFilter] antes de cada acción o cada controlador que quieras que use la cookie).
  2. Derivar todos tus controladores de un controlador base que tenga [CookieCultureFilter] aplicado.

Y lo más importante… ¿Ambas soluciones son equivalentes?

Pues NO. Ambas soluciones no son equivalentes… En el caso del post anterior, si recordáis, si declaraba un parámetro culture en una acción, recibía el valor de la cookie, ya que el route handler me añadía este parámetro en el RouteData. Pues bien, eso dejará de funcionar. Es decir, en este caso la acción:

public ActionResult Foo(string culture)
{
}

que en el post anterior recibía el valor de la cookie en culture, con esa nueva aproximación siempre recibirá null.

¿Y por que si mi filtro también añade en el RouteData el valor de la cookie? Pues muy sencillo: el Model Binder (que se encarga de hacer binding a los parámetros de las acciones) se ejecuta antes que el filtro. Simplificando, el flujo de ejecuciones sería:

  1. Route Handler
  2. Model Binder
  3. Action Filters
  4. Acción del controlador

En este caso, estamos añadiendo un valor en el RouteData después de que el Model Binder haya actuado. Por eso el parámetro no tendrá el valor. Eso no quita que desde el controlador lo podáis consultar (tenéis acceso al RouteData).

Y con esto termino… espero que el post os haya ayudado un poco más a entender como funciona MVC y ver distintas alternativas de cómo hacer las cosas!

con 2 comment(s)
Archivado en:

Disclaimer: Ese post ni es, ni lo pretende, ser un tutorial de Git. Es simplemente las impresiones de alguien (yo) que ayer empezó a usar, por primera vez, Git. Seguro que hay gente que lee ese blog y que sabe mucho, pero mucho más de Git que yo… Así que sus comentarios y correciones serán bienvenidas! :)

Esos días (ayer, vamos :p) he empezado a usar Git. Para quien no lo conozca Git es un sistema de control de código fuente distribuído. A diferencia de TFS, VSS o SVN que son centralizados, en Git cada desarrollador tiene su propia copia entera del repositorio en local. Los cambios son propagados entre repositorios locales y pueden (o no) sincronizarse con un repositorio central.

La diferencia fundamental con TFS ó VSS es que en esos está claro quien es el repositorio (el servidor). Eso no existe en Git. En Git cada usuario es el repositorio y se sincronizan cambios entre repositorios (usuarios). Opcionalmente puede usarse un repositorio central pero parece que no es obligatorio.

Los cambios en Git se propagan a través de esas operaciones:

  1. commit: Envia los datos del working directory al repositorio local
  2. push: Sincroniza los cambios del repositorio local a otro repositorio remoto.
  3. fetch: Sincroniza los cambios de un repositorio remoto al repositorio local.
  4. pull: Sincroniza los cambios de un repositorio remoto al working directory.
  5. checkout: Actualiza el workind directory con los datos del repositorio local.

La siguiente imagen lo clarifica bastante:Comandos de transporte de Git

Operaciones de Git (imagen original de Oliver Steele en http://www.gitready.com/beginner/2009/01/21/pushing-and-pulling.html).

Usar Git desde Visual Studio

Para usar Git desde VS2010 he encontrado las Git Extensions que instalan, no sólo un plugin para VS2010, sinó también el propio Git y clientes adicionales que pueden necesitarse (SSH o PuTTY si queremos conexiones seguras con nuestros repositorios).

Una vez instaladas nos aparecerá un menú nuevo llamado “Git” en el VS2010.

Crear un repositorio local y rellenarlo

Para empezar a trabajar con Git, lo primero es crear un repositorio. Recordad que los repositorios son locales, por lo que un repositorio es simplemente un directorio de nuestro disco. En mi caso, tenía ya una solución de VS2010 en local que es la que quería empezar a compartir con Git. Por ello lo que hice fue crear un nuevo repositorio local. Hay otra opción, que es crear un repositorio local a partir de los datos de un repositorio remoto (git-clone) pero todavía no lo he hecho. Si todo va como está planeado, el viernes tocará hacerlo, y ya escribiré al respecto!

Usando las Git Extensions crearnos nuestro propio repositorio es tan sencillo como usar la opción “Initialize new repository” y nos saldrá una ventana como la siguiente:

image

La opción normal es “Personal repository”. Un Central repository es un repositorio sin Working directory, que sólo sirve para sincronizar datos.

Una vez entrada la carpeta (en este caso D:\Gittest) esa pasa a ser el directorio de trabajo (working directory) para ese repositorio. Si abrimos la carpeta con el explorador de windows veremos que hay una carpeta en su interior llamada .git: ese es el repositorio local.

Nota: Dado que el directorio donde inicializamos el repositorio local pasa a ser el directorio de tabajo, lo suyo es inicializar el repositorio local en el mismo directorio donde tenemos la solución de VS. Es decir, si la solución de VS la tenemos en el directorio C:\projects\source\myproject, ese sería el directorio que usariamos (recordad que el repositorio se crea en una subcarpeta llamada .git, por lo que no modificarà ni borrará nada de la carpeta que le digáis).

En mi caso en D:\Gittest ya tenía una solución, por lo que el árbol de directorios me ha quedado:

image

Ahora vamos a hacer commit de la solución al repositorio local. Antes que nada, pero debemos tener presente de que no queremos que todos los archivos que cuelgan del working directory (D:\gittest) se guarden en el repositorio local. Git no entiende de tipos de archivo, no sabe que es código fuente y que no. Existe un fichero en el working directory llamado .gitignore que sirve para indicar que ficheros no deben guardarse en el repositorio local al hacer un commit.

Por suerte editar este fichero con las Git Exensions, es trivial. Nos vamos al menú Git y seleccionamos “Edit .gitignore”. Nos aparecerá una ventana parecida a:

image

A la izquierda el contenido del .gitignore (en este caso vacío, normal ya que ni existe el fichero). A la derecha un ejemplo de .gitignore adaptado a VS. Si pulsais el botón “add default” os copiará las entradas de la derecha a la izquierda:

image

Fijaos que el .gitignore se ha confiugrado para evitar archivos como *.exe, *.pdb, etc, pero también directorios como TestResult* (que usa VS para guardar los resultados de las pruebas unitarias) o _Resharper* (que usa resharper para guardar sus configuraciones). Nosotros podríamos añadir más entradas y finalmente darle a “Save”. Eso nos creará el archivo .gitignore en nuestro working directory (D:\gittest).

Ahora estamos listos para hacer el commit y rellenar por primera vez el repositorio local. Para ello, de nuevo nos vamos al menú Git y seleccionamos “Commit”. Nos aparecerá una ventana parecida a:

image

Se divide en cuatro secciones:

  1. Izquierda superior: listado de operaciones pendientes de realizar (cambios entre el working directory y el repositorio local)
  2. Izquierda inferior: listado de operaciones que se van a realizar (es decir cuales de los cambios del working directory queremos propagar al repositorio local).
  3. Derecha superior: preview del archivo seleccionado en la parte (1).
  4. Derecha inferior: comandos de Git.

En este caso vamos a propagar todos los cambios, para ello pulsamos el botón “Staged Files” y en el menú desplegable marcamos “Stage all”. Con eso todos los ficheros de la izquierda superior pasarán a la izquiera inferior. Ahora vamos a realizar el commit (si quisieramos podríamos hacer también un push a un repositorio remoto pero todavía no hemos configurado ninguno).  Así que entramos un mensaje de commit en la parte derecha inferior (p.ej. commit inicial) y le damos al botón “Commit”. Git Extensions nos mostrará al final un diálogo con el resumen de lo hecho:

image

Añadir cambios

Bien, una vez hemos hecho el commit podemos seguir trabajando con VS2010 sin necesidad de hacer nada especial. Olvidaros de conceptos como “proteger” o “desproteger” de TFS o VSS. Simplemente trabajáis y modificáis el contenido del directorio de trabajo.

Cuando decidáis propagar los cambios del directorio de trabajo al repositorio local, repetís la operación de antes: Git –> Commit.

Repositorio remoto

Bien, ha llegado la hora de configurar un repositorio remoto. Para ello, lo primero que necesitamos es tener acceso a un repositorio remoto. Hay varios proveedores de repositorios Git en internet, entre los que destaca GitHub. GitHub pero está limitado a proyectos open source. Si neceistáis un hosting de Git para proyectos no open source hay varios de pago (con algunas versiones gratuitas). En esta consulta de StackOverflow hablan de ello: Best git repository hosting for commercial project.

Nota: A título informativo os diré que yo me dí de alta en el plan gratuito de Assembla, que te da 2GB de espacio Git.

La configuración de un repositorio remoto dependerá de donde lo tengamos, pero básicamente son dos pasos muy simples:

  1. Generar el par clave pública-privada para ssh o PuTTY y subir la clave pública al repositorio (si queremos acceso seguro).
  2. Ponerle a Git Extensions la URL del repositorio remoto.

Vamos a ver una demostración. Para independizarnos de cualquier proveedor, vamos a crear otro repositorio de Git en nuestra máquina (podríamos hacerlo en una carpeta compartida p.ej.) y lo usaremos como repositorio remoto.

Para ello de nuevo nos vamos al menú Git y le damos a “Initialize new repository” y ahora marcamos la opción de Central Repository. Y ponemos cualquier path nuevo que creemos (en mi caso he creado una carpeta D:\Remote):

image

Si ahora vamos a D:\Remote veremos que allí tenemos el repositorio. En este caso el repositorio no está en un subdirectorio .git, porque hemos elegido la opción de Central repository que no tiene directorio de trabajo.

Bien, ahora vamos a hacer un push de nuestro repositorio local al repositorio remoto. Para ello, primero, debemos dar de alta este repositorio remoto en las Git Extensions. Para ello nos vamos al menú Git y seleccionamos la opción de “Manage Remotes”. Nos aparecerá una ventana y ponemos los datos:

image

El nombre es un nombre identificativo, mientras que la URL en este caso es la carpeta donde está el repositorio. Finalmente le damos a “Save” para guardarlo.

Ahora ya podemos hacer un push para pasar los datos del repositorio local al remoto. Para ello, de nuevo nos vamos al menú Git y marcamos la opción Push. En la ventana que nos aparece marcamos la opción “Remote” y de la combo seleccionamos el repositorio remoto que dimos antes de alta:

image

Luego pulsamos el botón Push. Como antes nos mostrará un diálogo cuando haya finalizado:

image

Navegar por repositorios

Podemos navegar por un repositorio, usando la opción “Browse” del menú Git. Seleccionamos el repositorio y podemos navegar, ver los commits/push que se han realizado y ver los archivos y cambios contenidos en cada commit/push. De todos modos sólo he visto como hacer esto en repositorios (locales o remotos) que estén en disco. No sé como navegar por mi repositorio en Assembla p.ej.

image

Y aquí lo dejamos por el momento… A medida que trabaje y que vaya aprendiendo como funciona Git iré poniendo más información al respecto!

Espero que este post os haya sido útil y que en todo caso haya servido para que veáis que es Git y las diferencias con otros sistemas de control de código fuente como TFS.

Un saludo!

con 20 comment(s)
Archivado en:

EEhhhmm… bueno, no se me ocurre un título mejor. Este post nace gracias a un tweet de Lluis Franco. En el tweet Lluís preguntaba dónde guardar la cultura de una aplicación MVC si no se podía poner en la URL. Después de varios tweets comentando algunas cosillas yo he respondido diciendo que veía dos opciones: o en una cookie o en la base de datos. Una de las cosas que más me gustan de HTTP es que es simple: no hay muchas maneras de pasar estado entre cliente y servidor ;-)

En este post vamos a ver como podemos solucionar fácilmente el problema asumiendo que se guarda la cultura del usuario en una cookie.

De mi tweet, la parte importante es la segunda: independizar a los controladores de donde esta la cultura de la aplicación. Ya lo he comentado en varios posts: evitad acceder desde los controladores a objetos que dependen de HTTP: sesión, aplicación, cache y… cookies.

En un post anterior ya comenté como usar un value provider para hacer binding de datos de una cookie a un controlador. Esa es una buena solución si los datos de la cookie se usan en algunas pocas acciones de un controlador. Pero ese no es nuestro caso ahora: ahora queremos que la cultura se establezca siempre, para todas las acciones de todos los controladores.

La solución en este caso pasa por un Route Handler nuevo. Los route handlers son los objetos que se encargan de procesar las rutas (crear los controladores y cederles el control). Son pues objetos de bajo nivel. Cuando la tabla de rutas enruta una URL para ser procesada por una ruta concreta, se usa el RouteHandler asociado a dicha ruta para crear toda la infrastructura que MVC necesita para procesar la petición.

Recordad que la tabla de rutas se define en Global.asax y que por defecto tiene el siguiente código:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

Aquí no estamos especificando ningún route handler, por lo que se usará el que tiene MVC por defecto… Pero como (casi) todo en MVC lo podemos cambiar :)

En lugar de usar el método MapRoute (que por si alguien no lo sabe es un método de extensión) podemos crear un objeto Route y añadirlo directamente a la tabla de rutas. El constructor de Route tiene un parámetro que es de tipo IRouteHandler y que es el route handler para esta ruta. Así que puedo transformar la tabla de rutas anterior en esta:

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

routes.Add("Default", new Route("{controller}/{action}/{id}",
new CultureRouteHandler())
{
Defaults = new RouteValueDictionary(
new
{
controller = "Home", action = "Index", id = UrlParameter.Optional
})
});
}

Ambas son equivalentes, salvo que esta usará un objeto de tipo CultureRouteHandler para procesar las peticiones.

Ahora vamos a ver como es el CultureRouteHandler:

public class CultureRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
var cultureCookieVal = GetCultureFromCookie(requestContext.HttpContext.Request.Cookies);
var culture = new CultureInfo(cultureCookieVal);
requestContext.RouteData.Values.Add("culture", cultureCookieVal);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
return base.GetHttpHandler(requestContext);
}

private string GetCultureFromCookie(HttpCookieCollection cookies)
{
var retValue = "ca-ES";
if (cookies.AllKeys.Contains("userculture"))
{
retValue = cookies["userculture"].Value;
}
return retValue;
}
}

En este caso derivo de MvcRouteHandler (como casi siempre en MVC es mucho más sencillo derivar de alguna clase base que implementar la interfaz entera), y en el método GetHttpHandler lo que hago es llamar al método de la clase base pero antes:

  1. Recupero el valor de la cookie de cultura
  2. Guardo este valor en el route data con el nombre culture (por si alguien quiere consultarlo)
  3. Creo un CultureInfo a partir de los datos de la cookie y establezco la cultura del thread actual a este valor: así cualquier mecanismo que tenga de “localización” debería funcionar igualmente.

Finalmente para probar el tema me he creado un pequeño controlador:

public class HomeController : Controller
{
[OutputCache(NoStore = true, Location = OutputCacheLocation.None)]
public ActionResult Index()
{
return View();
}

public ActionResult SetCookie(string id)
{
if (!string.IsNullOrEmpty(id))
{
this.ControllerContext.HttpContext.Response.Cookies.Add(new HttpCookie("userculture", id));
}
return RedirectToAction("Index");
}
}

La acción /Home/Index simplemente retorna una vista. La acción /Home/SetCookie/id establece la cookie de cultura (se supone que el id es válido, algo así como /Home/SetCookie/es-ES p.ej.).

La vista que devuelve /Home/Index simplemente muestra la cultura actual:

<p>
Cultura actual: @System.Threading.Thread.CurrentThread.CurrentUICulture.ToString();
</p>

Bonus track: Y si quiero que algún controlador reciba la cultura actual como parámetro de alguna de sus acciones?

Bien, recordad que hemos hecho que el route handler guardase el valor de cultura en los route values. MVC tiene un value provider que permite realizar bindings desde los route values hacia los controladores. Guardábamos el valor con el nombre “culture” así que nos basta con:

public ActionResult Foo(string culture)
{
// Código...
}

El parámetro culture tiene el valor de la cultura.

Si quieres saber exactamente cómo reciben los datos los controladores, hace algún tiempecillo escribí un post al respecto.

De esa manera conseguimos lo que yo decía en mi tweet: agnostizar los controladores de dónde se guarda la cultura!

Un saludo!

con 12 comment(s)
Archivado en:

Muy buenas!

Una de las novedades que nos trae ASP.NET MVC3, con respecto a MVC2 es poder usar fácilmente la validación remota: eso es, desde cliente llamar a un método del servidor que nos diga si un dato (entrado p.ej. en un campo de texto es válido o no). Y cuando digo fácilmente me refiero a fácilmente, muy fácilmente.

Vamos a ver un ejemplo: para ello vamos a modificar la aplicación de ejemplo que crea MVC3 para que al darnos de alta, consulte si el usuario ya existe y si es el caso no nos deje. Os recuerdo que esa validación es Ajax, eso significa: el campo de texto pierda el foco se realizará la petición (en background) al servidor que comprobará si el usuario entrado ya existe y si es así mostrará un error en el campo de texto asociado.

Vamos pues, a verlo paso a paso ;-)

1. Viendo que nos genera MVC3

Para empezar creamos un nuevo proyecto ASP.NET MVC3. Cuando os pida el template, usad el de Internet Application (no uséis el Empty). De esa manera MVC3 nos crea el esqueleto de la aplicación inicial:

image

El fichero AccountModels.cs tiene distintos Viewmodels que usan las acciones del controlador Account. La acción que da de alta un usuario es Register que está definida en el controlador Account:

[HttpPost]
public ActionResult Register(RegisterModel model)
{
// Código...
}

La acción usa la clase RegisterModel que es uno de los viewmodels definidos en AccountModels.cs:

public class RegisterModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }

[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email address")]
public string Email { get; set; }

[Required]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}

2. Habilitando la validación remota

La validación remota en MVC3 se habilita, como el resto de validaciones, usando DataAnnotations, concretamente con el atributo Remote. En nuestro caso queremos habilitar la validación remota sobre la propiedad UserName. Para ello añado el atributo Remote:

[Required]
[Display(Name = "User name")]
[Remote("CheckUserAvailability", "Account", ErrorMessage = "This user name is not allowed.")]
public string UserName { get; set; }

El primer parámetro es la acción que va a validar el campo, la segunda es el controlador. Finalmente ponemos el mensaje de error (igual que las otras validaciones).

3. Creando el código de validación

Finalmente tenemos que crear la acción del controlador:

[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserAvailability(string username)
{
var validUserName = Membership.FindUsersByName(username).Count == 0;
return Json(validUserName, JsonRequestBehavior.AllowGet);
}

Fijaos lo simple que es: Usamos el membership provider para ver si existe algún otro usuario con el mismo nombre, y devolvemos un booleano (codificado en json). El uso de [OutputCache] es para evitar que MVC me cachee los resultados de las peticiones.

Y listos! Con eso ya hemos terminado.

Podemos ejecutar la aplicación, registrar un usuario y luego intentar darnos de alta de nuevo y veremos el error:

image

4. Y que pasa “en la trastienda”?

Si inspeccionamos las peticiones veremos que cada vez que cambia el texto se realiza una petición Ajax. Esta vez en lugar de firebug he usado las Developer Tools de IE9 (pulsar F12 para que os aparezcan):

image

Estas son las peticiones que se realizan si entro “eiximenis” en el campo de nombre de usuario. Si las contáis veréis que faltan peticiones. Eso es simplemente he pulsado dos teclas  muy rápido y las peticiones supongo que no se encolan

Si miramos los detalles de una petición vemos lo siguiente:

image

Fijaos en el primer campo (Request) me indica la URL de la petición. En mi caso es /Account/CheckUserAvailability?UserName=eiximenis

Es decir se pasa el valor por querystring (eso no es problema alguno para MVC que soporta el binding de datos en el querystring desde siempre).

5. Y qué código me genera eso en el navegador?

Recordáis que MVC3 apuesta fuertamente por el unobstrusive javascript? Pues eso es lo que nos genera:

<input data-val="true" 
data-val-remote="This user name is not allowed."
data-val-remote-additionalfields="*.UserName"
data-val-remote-url="/Account/CheckUserAvailability"
data-val-required="The User name field is required."
id="UserName" name="UserName" type="text" value="" />

Precioso no? Nada de código javascript mezclado por ahí que “ensucie” el html: sólo atributos.

Los atributos data-val-remote-* son los que controlan la validación remota. Para que eso funcione, es necesario que los scripts siguientes estén referenciados:

  1. jquery-1.4.4.min.js
  2. jquery.validate.min.js
  3. jquery.validate.unobtrusive.min.js

Yo los tengo siempre colocados en mi página Master principal (Views/Shared/_Layout.cshtml si usas Razor).

6. Controlando algunos aspectos “avanzados”

Vamos a ver como controlar algunos aspectos más de la validación remota. P.ej. te puede interesar que la llamada a la acción de validación (CheckUserAvailability) se realice via POST y no via GET. Para ello simplemente usa la propiedad HttpMethod del atributo Remote:

[Remote("CheckUserAvailability", "Account",
HttpMethod = "POST", ErrorMessage = "This user name is not allowed.")]

Eso añade el atributo data-val-remote-type="POST" al campo HTML generado lo que fuerza que la petición sea usando POST.

Otra cosa interesante es que podemos enviar más de un parámetro a la acción quen realiza la “validación remota”. P.ej. imaginad que a CheckUserAvailability le queremos también pasar el password que ha introducido el usuario (vale, no tiene lógica en ese ejemplo, pero imaginadlo igual :p).

Para ello podemos usar la propiedad AdditionalFields del atributo Remote:

[Remote("CheckUserAvailability", "Account", HttpMethod = "POST",
AdditionalFields = "Password",
ErrorMessage = "This user name is not allowed.")]
public string UserName { get; set; }

Ahora si miramos de nuevo lo que nos manda la petición veremos que junto al campo UserName nos manda el valor del campo Password:

image

Tened presente que el valor de Password puede ser vacío (el usuario puede no haber introducido nada allí todavía). Pero lo importante de esto es que las validaciones remotas no están limitadas a validar UN SOLO campo.

Y con eso terminamos el post de las validaciones remotas en ASP.NET MVC3… Como podeis ver, es un método sumamente potente y sumamente fácil!

Un saludo! ;)

con 2 comment(s)
Archivado en:

Muy buenas! Que tal el fin de año? Empachados con turrones, polvorones y demás? En fin, vamos a inaugurar el 2011 y que mejor manera que hacerlo que con un post! ;-)

En realidad hubiese querido que este post fuese el último del año anterior, pero no puede publicarlo antes por problemas logísticos. La idea del post surge de un tweet que publicó Luis Ruiz Pavón. Su pregunta era que tal acceder a la sesión desde un Model Binder para poner datos a disposición de los controladores. Mi respuesta fue que yo usaría un value provider, y así llegamos a este post.

Para conseguir binding de los datos de la sesión a los parámetros de un controlador no es necesario crear ningún Model Binder. En MVC2 se introdujo un concepto nuevo (del que ya he hablado varias veces por aquí) que se llama ValueProvider y que es el encargado de acceder donde están los datos y ponerlos a disposición de los Model Binders. Si ignoramos los value providers y hacemos un model binder que acceda a la sesión, entonces realmente nuestro model binder hace dos cosas:

  1. Ir donde están los datos (la sesión) y recogerlos
  2. Enlazar los datos con los parámetros de los controladores

Según la arquitectura de ASP.NET MVC los model binders sólo se encargan de lo segundo, y son los value providers quienes se encargan de lo primero. Así, pues, tened presente la regla:

  1. Si lo que queréis canviar es cómo se enlazan los datos (vengan de donde vengan) a los controladores: cread un model binder
  2. Si lo que queréis es modificar de dónde se obtienen los datos que se enlazan a los controladores: usad un value provider.

En nuestro caso, tal y como se enlazan los valores a los controladores ya nos va bien (el DefaultModelBinder es realmente bueno en su tarea), sólo que queremos que si un dato está en la sesión se coja de allí: necesitamos un value provider nuevo.

Factoría de value providers

En ASP.NET MVC los value providers se crean siempre mediante una factoría y lo que realmente registramos en el runtime son esas factorías. En cada petición ASP.NET MVC le pide a las distintas factorías que creen los value providers necesarios.

Así pues lo primero va a ser crear nuestra factoría, que devolverá objetos de nuestro value provider vinculado a sesión:

class SessionValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new SessionValueProvider(controllerContext.HttpContext.Session);
}
}

Simplemente debemos derivar de ValueProviderFactory y en el método GetValueProvider devolver una instancia de nuestro value provider. En mi caso devuelvo una instancia del SessionValueProvider y le paso la sesión en el constructor.

Debemos registrar esa factoría de value providers en el runtime de ASP.NET MVC. Para ello en el Global.asax basta con meter la siguiente línea (usualmente en el Application_Start):

ValueProviderFactories.Factories.Add(new SessionValueProviderFactory());

El value provider

Crear un value provider es “tan sencillo” como implementar la interfaz IValueProvider. De todos modos debemos conocer un poco como funcionan los model binders a los que debemos proporcionar los valores.

Los value providers vienen a ser como un “diccionario enorme” que los model binders consultan cuando quieren obtener un dato. Debemos saber cómo (“con que clave”) nos va a pedir el model binder los datos y como debemos dárselos. En un post mío de hace tiempo ya comenté como funciona el DefaultModelBinder, y allí cuento también la interacción entre el DefaultModelBiner y los value providers.

En fin, que todo ese rollete es para comentaros que muchas veces en lugar de implementar IValueProvider desde cero, es mucho mejor derivar de alguna de las clases que ya hay hechas, y hay una en concreto que nos viene al pelo: DictionaryValueProvider<TValue>. Esta clase implementa un value provider cuya fuente de datos es un diccionario cuyos valores son de tipo TValue. Y que es la sesión en el fondo sinó un gran diccionario cuyos valores son de tipo object?

Así pues creamos nuestra clase que derive de DictionaryValueProvider y lo único que tenemos que hacer es pasarle a nuestra clase base el diccionario que debe usar, que construimos a partir de la sesión:

class SessionValueProvider : DictionaryValueProvider<object>
{
public SessionValueProvider(HttpSessionStateBase session)
: base(CreateDictionary(session), CultureInfo.CurrentCulture)
{

}

private static IDictionary<string, object> CreateDictionary(HttpSessionStateBase session)
{
var entries = session.Keys.Cast<string>().ToDictionary(key => key, key => session[key]);
return entries;
}
}

Trivial no? El método CreateDictionary simplemente crea un IDictionary a partir de los datos de la sesión.

Y listos! Hemos terminado, No necesitamos hacer nada más para que el binding de objetos de sesión funcione. El requisito para que el binding se efectúe es el de siempre: que el nombre del parámetro en la acción del controlador tenga el mismo nombre que la clave de sesión:

public class SessionController : Controller
{
public ActionResult Put()
{
Session["ufo"] = "String en sessión";
Session["complex"] = new Foo()
{
Cadena = "Una cadena en objeto complejo",
Entero = 100,
Lista = new List<int>() {1, 1, 3, 5, 8, 13}
};
return View();
}
public ActionResult Get(string ufo)
{
ViewData["data"] = ufo;
return View();
}

public ActionResult GetClass(Foo complex)
{
return View(complex);
}
}

En este controlador la acción Put coloca dos datos en la sesión: una cadena con clave “ufo” y un objeto de una clase llamada Foo, con clave “complex”. Las dos acciones restantes (Get y GetClass) usan el binding y obtienen los datos de la sesión.

Ventajas de usar el binding para obtener los valores

La ventaja principal de usar el binding para obtener los valores en lugar de acceder a la sesión directamente es que desacopla el código de las acciones de la sesión de HTTP. En definitiva, si quiero probar si la acción Get funciona correctamente, p.ej. usando un unit test, me basta con pasarle una cadena cualquiera. Si en el código de la acción accediese a la sesión debería tener acceso a la sesión (desde la prueba) y rellenarla.

Espero que os sea útil!

Un saludo!

PD: Teneis un proyecto en VS2010 con MVC2 con el código de dicho post en mi skydrive: http://cid-6521c259e9b1bec6.office.live.com/self.aspx/BurbujasNet/ZipsPosts/MvcSessionBinding.zip

con no comments
Archivado en: