viernes, 7 de mayo de 2010

ASP.NET MVC: ValueProviders

Hola! Hoy quiero comentar un aspecto de ASP.NET MVC2 que no sé hasta que punto es conocido, y son los llamados Value Providers.

Disclaimer: Este post será largo y puede ser un poco denso y asumo conocimientos básicos de ASP.NET MVC. Tampoco tengas reparos en leerte este post en más de un dia si quieres… había pensado dividirlo en dos posts, pero al final he preferido meterlo todo en uno.

Si habéis trabajado un poco con ASP.NET MVC, sabréis que si tenéis un controlador con esta acción:

[HttpPost()]
public ActionResult Create(Producto p)
{
// Hacer algo con producto
return View();
}





ASP.NET MVC es capaz de hacer binding entre los parámetros de la Request y las propiedades de la clase Producto para instanciar un objeto Producto para tí y pasarlo al controlador.



Es decir, si la clase producto está definida como:




public class Producto
{
public int Codigo { get; set; }
public string Nombre { get; set; }
public int Precio { get; set; }
}





Y la Request tiene estos campos, ASP.NET MVC hace el binding por nosotros… P.ej. si usamos un formulario como el siguiente para enviar los datos:




<form action="/Productos/Create" method="post">
<fieldset>
<input id="Codigo" name="Codigo" type="text" value="" />
<input id="Nombre" name="Nombre" type="text" value="" />
<input id="Precio" name="Precio" type="text" value="" />
<input type="submit" value="Create" />
</fieldset>
</form>






La Request tiene los campos “Codigo”, “Nombre” y “Precios” (los name de los input). Al enviarse el formulario genera una Request con los siguientes datos POST (se pueden ver fácilmente usando firebug):



Content-Type: application/x-www-form-urlencoded

Content-Length: 31

Codigo=1&Nombre=Silla&Precio=12



La última línea es la que tiene los campos con los valores, que son usados por ASP.NET MVC para realizar el binding con la clase Producto.



El responsable de recoger los valores y realizar el binding con el modelo es el ModelBinder pero no es reponsabilidad del ModelBinder saber donde están los valores. P.ej. haced una prueba… Si al formulario anterior le quitáis uno de los campos (p.ej. el campo Precio) y modificáis el action del <form> para que quede <form action=”/Productos/Create?Precio=10” method=”post> y realizáis la petición veréis que sigue funcionando.



En este caso si miramos con firebug la petición, no tiene el parámetro post “Precio”:



Content-Type: application/x-www-form-urlencoded

Content-Length: 21

Codigo=1&Nombre=Silla



Pero si que dicho parámetro está en la URL, que ahora es /Productos/Create?Precio=10. Pero esto no afecta al ModelBinder que es capaz de recoger el parámeteo con independencia de dónde se encuentre dentro de la Request. ¿Y cómo es posible? Pues gracias a los Value Providers.



Pero antes sigamos jugando un poco, para tener todas las piezas encima de la mesa… que ocurre si sin modificar el atributo action del tag <form> volvemos a añadir el <input name=”Precio”> al formulario? Es decir tenemos un formulario con los tres campos (Codigo, Nombre y Precio) pero además tenemos el campo Precio otra vez en la url (?Precio=100). Pueden ocurrir tres cosas:




  1. Que ASP.NET MVC de error, porque el campo “Precio” está repetido (una vez en la URL y otra en los parámetros POST) de la petición.


  2. Que tenga prioridad el valor del parámetro en la URL


  3. Que tenga prioriodad el valor del parámetro POST



Bien, lo que ocurre es que tiene prioridad el valor del parámetro POST. Es decir el valor de la propiedad Precio del objeto Producto que reciba el controlador será el que haya entrado el usuario y no el que se indica en la URL. Pero… ¿por que?



Cuando una petición es tratada por el framework, primero se pasa por una serie de value providers que inspeccionan cada uno dicha petición y guardan los valores que cada uno de ellos entiende. P.ej. existe un value provider para inspeccionar los valores POST y otro distinto para inspeccionar los valores de la URL. ASP.NET MVC mantiene una colección de value providers y pasa la request por todos ellos.



P.ej. asumamos en nuestro caso que tenemos una colección con dos value providers (luego veremos que esto no es exactamente así, pero me vale esa simplificación por ahora). Llamemosle PostValueProvider al primero y UrlValueProvider al segundo. Supongamos que nos llega la petición que hemos comentado:




  • Parámetros en la URL: ?Precio=100


  • Parámetros POST: Codigo=1&Nombre=Silla&Precio=12



La request pasa por el primer value provider (supongamos que es el PostValueProvider) que la inspecciona, ve que hay tres parámetros llamados Código, Nombre y Precio y se los guarda con sus respectivos valores. Luego la request pasa por el siguiente value provider, el UrlValueProvider que inspecciona la request y ve que hay un parámetro llamado Precio y se lo guarda junto con su valor.



Ahora es el turno del ModelBinder: el ModelBinder detecta que debe crear un objeto Producto que tiene tres propiedades: Código, Nombre y Precio, así que pregunta a los value providers por los valores de estos campos. Esta frase es la clave: pregunta a los value providers, no a un value provider en particular. Pongamos que cuando el ModelBinder necesita el valor del campo “Precio” se limita a preguntar simplemente el valor de dicho campo, y el ModelBinder espera que haya un sólo valor para dicho campo. Si como es el caso dos value providers han guardado el valor para dicho campo, sólo uno responde… cual? Pues el que esté primero en la lista de value providers que ASP.NET MVC mantiene. Ni más ni menos :)



Creación de un Value Provider propio



Como ya sabréis ASP.NET MVC es muy extensible, así que obviamente uno se puede crear sus propios Value Providers, para permitir tratar peticiones que tengan datos codificados de forma extraña o en otros sitios donde pueden estar los datos (p.ej. en cookies). Crear un value provider es muy simple, basta crear una clase que implemente la interfaz IValueProvider. Este interfaz define dos métodos:




  1. ContainsPrefix –> No se como explicar fácilmente lo que significa exactamente sin entrar en demasiados detalles sobre como funciona el ModelBinder, así que permitidme una simplificación. Este método devuelve si el value provider tiene valor para un campo en concreto. Es decir si el value provider tiene un valor para el campo “Precio” entonces ContainsPrefix(“Precio”) debe devolver true. Insisto: No es tan simple, pero para entender el concepto del post nos basta así.


  2. GetValue –> Devuelve el valor que se le pide. Es decir GetValue(“Precio”) devuelve el valor correspondiente al campo “Precio” que tenga este Value Provider. Este método sólo se llama si ContainsPrefix devuelve true.



Vamos a ver como podemos implementar un ValueProvider propio… P.ej. vamos a implementar un Value Provider que lea valores de los <appSettings> del web.config (de acuerdo, quizá no es brutalmente útil, pero como ejemplo servirá).



El código podría ser el siguiente:




public class AppSettingsValueProvider : IValueProvider
{
private Dictionary<string, ValueProviderResult> values;


public AppSettingsValueProvider()
{
values = new Dictionary<string, ValueProviderResult>();
foreach (string key in ConfigurationManager.AppSettings.AllKeys)
{
string appSetting = ConfigurationManager.AppSettings[key];
values.Add(key, new ValueProviderResult(appSetting, appSetting, CultureInfo.InvariantCulture));
}
}

public bool ContainsPrefix(string prefix)
{
return values.ContainsKey(prefix);
}

public ValueProviderResult GetValue(string key)
{
ValueProviderResult value;
values.TryGetValue(key, out value);
return value;
}
}





En el constructor se inicializa el diccionario values con los valores leídos de los <appSettings> del web.config. El método GetValue de IValueProvider, no devuelve un object con el valor directo del campo pedido, sinó que devuelve un ValueProviderResult una clase que tiene tres campos (rawValue, attemptedValue y culture).  El campo rawValue es lo que realmente se ha leído de la petición, mientras que el campo attemptedValue es el valor de rawValue convertido a una string. En mi caso dado que <appSettings> contiene ya string, tanto rawValue como attemptedValue tienen el mismo valor.



Bien! Ya tenemos un value provider… ahora ha llegado el momento de decirle a ASP.NET MVC que lo use… y aquí es donde debemos explicar la simplificación que hice antes :)



Factorías de Value Providers



Os acordáis cuando dije que ASP.NET MVC mantenía una colección de value providers y que pasaba la request a cada uno de ellos para que pudiesen procesarla y guardarse los campos necesarios? Dije que esto no era exactamente así, sinó una simplificación… Pues bien, la verdad es que ASP.NET MVC no mantiene una colección de value providers, sinó una colección de factorías de value providers.



Si te preguntas porque una factoría en lugar de guardar directamente los value providers… es para darte más control sobre como se crean los value providers: un value provider actúa sobre los datos de la petición actúal, por lo que por cada petición deben crearse todos los value providers de nuevo… La interfaz IValueProvider no define ningún mecanismo para pasarle la Request al value provider. Tampoco tenemos acceso a ningún tipo de contexto ni nada parecido… Piensa, si ASP.NET MVC debe crear los value providers a cada petición, cómo le pasa los datos de la request?



La solución pasa por usar factorías de value providers es decir, clases cuya única responsabilidad es crear los value providers a cada petición.



Veamos como sería nuestra factoría que cree objetos de nuestro AppSettingsValueProvider:




public class AppSettingsValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{

return new AppSettingsValueProvider();
}
}





Basta con derivar de ValueProviderFactory y redefinir el método GetValueProvider. En este méotdo tenemos acceso al ControllerContext que a su vez nos da acceso a la request http. Ahora si queréis pasáis la request http (o lo que queráis) al value provider.



Una vez tenemos la factoría debemos decirle a ASP.NET MVC que la use. Eso se hace registrando nuestra factoría usando la clase estática ValueProviderFactories:




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





Usualmente esto se coloca en el Application_Start de Global.asax.



Fijaos en un detalle: La factoría se crea sólo una vez (y la creamos nosotros, no el framework) y luego para cada petición se llama al método GetValueProvider de la factoría… método que también hemos hecho nosotros y donde se devuelve el value provider. De esta manera controlamos la creación de los value providers (lo que nos permitiría usar, si quisiéramos, mecanismos de inyección de dependencias).



Y listos! Ya estamos. Si modificamos la vista de datos para que no tenga el campo “Precio” (ni como POST ni como GET) y añadimos en el web.config:




<appSettings>
<add key="Precio" value="999"/>
</appSettings>





Veréis que el controlador recibe un Producto con el nombre y código que hayáis indicado y un Precio de 999. Dado que nuestra factoría se ha añadido la última de la lista, si volvéis a poner el Precio en el formulario, tendrá prioridad el que haya entrado el usuario (ya que la factoría que crea el value provider con los datos POST está antes de nuestra factoría).



Os dejo un proyecto de demo. La vista inicial muestra tres enlaces: Formulario con los tres campos, formulario con dos campos pero precio en la url y formulario con dos campos. Al hacer submit del formulario se nos muestran los detalles del producto entrado. Veréis como en el último caso (formulario con dos campos) aparece el valor de precio 999 que es el que se ha sacado del web.config (en el primer caso tiene preferencia el valor entrado por el usuario y en el segundo el valor de la url).



Os podeis descargar el proyecto desde aquí (enlace a mi skydrive).



Espero que te haya quedado más o menos clara la idea de los value providers… en un post próximo hablaré de los ModelBinders para que podamos tener la foto completa!



Saludos!



PD: Esto es un crosspost desde mi blog de geeks.ms!

No hay comentarios: