miércoles, 12 de mayo de 2010

ASP.NET MVC: Custom Model Binders

Seguimos esa serie donde intentamos bucear un poco por algunas interioridades de ASP.NET MVC, intentando ver como funcionan por dentro algunas de las características de ese framework tan apasionante como és ASP.NET MVC. Si en el primer post de la serie vimos lo que eran los value providers y en el segundo post vimos como funcionaba el DefaultModelBinder en el post de hoy veremos como podemos crear Model Binders propios (lo que a su vez, nos ayudará a entender todavía más como funciona el DefaultModelBinder).

Bueno, para empezar dejemos claro un punto fundamental:

  • En ASP.NET MVC no estamos limitados a un solo Model Binder, podemos tener muchos model binders, de hecho uno por cada tipo de modelo.

La clase estática ModelBinders es la que mantiene el conjunto de model binders que estén registrados en el sistema. Para registrar un Model Binder propio simplemente llamamos al método Add() de la colección Binders expuesta por dicha clase:

ModelBinders.Binders.Add(typeof(FooClass), new FooBinder());





El método Add() espera el tipo de modelo y el binder a usar para objetos de dicho tipo. Si para un tipo de modelo no existe model binder definido, se usará el model binder predeterminado, cuyo valor por defecto es el DefaultModelBinder pero que también podemos cambiar:




ModelBinders.Binders.DefaultBinder = new CustomModelBinder();





1. Un ejemplo sencillo… un Model Binder para objetos simples



Vamos a crear un Model Binder propio tan sencillo como (probablemente) inútil: un model binder propio para objetos de tipo string, que simplementa convierta los valores en mayúsculas.



El trato del framework de ASP.NET MVC con los model binders es muy simple: la interfaz IModelBinder que deben implementar todos los model binders define un sólo método: BindModel. Como se las apañe el model binder internamente le da igual al framework. Por suerte para nosotros la clase DefaultModelBinder es muy extensible, de forma que cuando implementamos un model binder propio, lo más normal (aunque no es obligatorio) es derivar de dicha clase. Así que antes vamos a ver que es lo que hace el DefaultModelBinder cuando debe enlazar un modelo:



 Elace de un modelo con el DefaultModelBinder



Nota: No estan todas las funciones que usa el CustomModelBinder (faltan las relacionadas con la validación del modelo, pero no quiero hablar hoy de validaciones).



Todas estas funciones son virtuales y por lo tanto pueden ser redefinidas en clases derivadas. Aquí tienes una breve descripción de cada método:




  • BindModel: El único método de IModelBinder, su responsabilidad es devolver el modelo creado y enlazado.


  • CreateModel: Crea una instancia del modelo


  • GetTypeDescriptor: Obtiene la información del tipo del modelo


  • GetModelProperties: Obtiene información sobre las propiedades del modelo


  • BindProperty: Método que enlaza una propiedad concreta del modelo


  • GetPropertyValue: Vista en el post anterior, obtiene el valor de una propiedad. Para ello usará el model binder asociado al tipo de la propiedad y llamará a su BindModel.


  • SetProperty: Vista en el post anterior, establece el valor de una propiedad



Volviendo a nuestro caso (un model binder que convierta las cadenas en mayúsculas) vamos a derivar de DefaultModelBinder y vamos a redefinir el método BindModel. Nos basta con este, puesto que un objeto string no tiene propiedades que se puedan enlazar, así que no se llamaría nunca a BindProperty de nuestro model binder: en BindModel vamos a hacer todo el trabajo:




public class StringBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object o = base.BindModel(controllerContext, bindingContext);
return o as string != null ? ((string)o).ToUpper() : o;
}
}





El código es trivial: llamamos a la implementación de BindModel del DefaultModelBinder (de esa manera todo el trabajo va a realizarlo el CustomModelBinder) y luego simplemente convertimos el resultado a mayúsculas.



Sólo nos queda registrar nuestro model binder, usando la clase ModelBinders. Esto suele hacerse en Global.asax en el Application_Start:




ModelBinders.Binders.Add(typeof(string), new StringBinder());





Y listos… ahora cualquier cadena que se enlace será convertida a mayúsculas, pero atención! No es necesario que el controlador reciba una cadena: si la acción del controlador recibe cualquier clase que tenga una propieda de tipo string, dicha propiedad se enlazará usando nuestro model binder, por lo que dicha propiedad será convertida a mayúsculas.



2. Otro ejemplo, colecciones a partir de una sola cadena



Vamos ahora a crear un model binder para una clase de modelo específica. La clase es tal como sigue:




public class Pedido
{
public string Nombre {get; set;}
public IEnumerable<string> Bebidas {get; set;}
}





Como ya sabemos, podemos enlazar colecciones con N campos en la petición que tengan el mismo nombre, o con N campos que tengan nombre[0], nombre[1],…, nombre[N-1]. Pero en este caso, vamos a crear una vista que tenga un solo campo que se llame Bebidas:




<form action="/Pedido/Nuevo" method="post">
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<label for="Nombre">Nombre</label>
</div>
<div class="editor-field">
<input id="Nombre" name="Nombre" type="text" value="" />
</div>
<div class="editor-label">
<label for="Bebidas">Bebidas</label>
</div>
<div class="editor-field">
<input id="Bebidas" name="Bebidas" type="text" value="" />
</div>
</fieldset>
<input type="submit" value="Pedir" />
</form>





Fijaos que sólo tenemos un <input type=”text” name=”Bebidas”>. Cuando hacemos submit del formulario los datos de la petición POST quedan de la siguiente forma:



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

Content-Length: 51

Nombre=eiximenis&Bebidas=coca-cola%2C+fanta%2C+agua



Fijaos que existe un solo campo POST llamado Bebidas, cuyo valor es la cadena “coca-cola, fanta, agua”. Si usáis del DefaultModelBinder para enlazar este modelo, el controlador recibirá un objeto Pedido cuyo nombre será correcto, y la propiedad Bebidas será un IEnumerable<string> con un solo elemento (con valor coca-cola, fanta, agua):



image



Nota: Fíjate como el nombre se ha convertido a mayúsculas, ya que el DefaultModelBinder ha usado el StringBinder que hicimos antes para enlazar la propiedad Nombre que es de tipo String!



Bueno… esperar que el DefaultModelBinder entienda que un campo separado por comas es realmente una lista de cadenas es esperar demasiado, así que vamos a hacer un CustomBinder que haga esto:




public class PedidoBinder : DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
object value = base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
object retVal = value;
if (propertyDescriptor.Name == "Bebidas" && value as IEnumerable<string> != null)
{
retVal = ((IEnumerable<string>)value).First().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
return retVal;
}
}





El código es muy simple… A diferencia del caso anterior, ahora redefinimos el método GetPropertyValue (dado que la clase Pedido si que tiene propiedades). Simplemente miramos si estamos obteniendo la propiedad “Bebidas” y si es el caso:




  1. Cogemos el valor que ha obtenido el DefaultModelBinder


  2. Lo convertimos a IEnumerable<string>, ya que sabemos que se trata de un IEnumerable<string> con una sola cadena


  3. Sobre esa cadena, devolvemos el resultado de hacer el Split (lo que devuelve un array, que a su vez es otro IEnumerable<string>).



Y listos, ahora sí que el enlace se realiza correctamente:



image



Bien, vamos a ver ahora otra manera en como podríamos codificar ese mismo model binder: vamos a implementar directamente la interfaz IModelBinder (no es lo mejor en este caso, pero vamos a aprender algunas cosillas más haciendolo).



3. Implementando IModelBinder



Insisto: En muchas ocasiones es mejor derivar de DefaultModelBinder en lugar de implementar directamente la interfaz IModelBinder (para aprovechar parte de lo que el DefaultModelBinder ya hace por nosotros).



Aquí tenemos una posible implementación:




public class PedidoBinderInterfaz : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
Pedido pedido = new Pedido();

pedido.Nombre = bindingContext.ValueProvider.GetValue("Nombre").AttemptedValue as string;
IEnumerable<string> bebidas = bindingContext.ValueProvider.GetValue("Bebidas").RawValue as IEnumerable<string>;
if (bebidas != null)
{
pedido.Bebidas = ((IEnumerable<string>)bebidas).First().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}

return pedido;
}
}





Fijate que dado que sabemos que el modelo es de tipo Pedido (puesto que este model binder sólo se registra para objetos de tipo Pedido) podemos crear directamente un Pedido y enlazar sus dos propiedades (Nombre y Bebidas).



Y ahora lo que quería que vierais: Para preguntar a los value providers por el valor de un campo de la petición, se usa el método GetValue de la propiedad ValueProvider del bindingContext. La propiedad ValueProvider es un objeto de la clase ValueProviderCollection pero nosotros lo recibimos como un IValueProvider. Eso es muy interesante: realmente el objeto es una colección de value providers, pero nosotros la vemos como un solo value provider y simplemente llamamos al método GetValue(). La propia clase se encarga de iterar por los value providers que contiene y encontrar el primero que nos pueda devolver el valor indicado.



Igual viendo el código pensáis que tampoco hay para tanto, que implementar la interfaz IModelBinder tampoco es tan complicado… bueno, fijaos en que lo que no hace este Model Binder y que si que hace el DefaultModelBinder:




  1. No estamos validando el modelo… Siempre podemos meter el código de validación dentro de BindModel y tampoco seria muy costoso hacerlo, cierto… pero perdemos la capacidad de usar DataAnnotations p.ej. (Si queremos soportar DataAnnotations entonces si que debemos empezar a tirar código)


  2. No estamos usando los model binders concretos para las propiedades… me explico: si enlazáis un modelo Pedido con este model binder, la propiedad Nombre la estamos enlazando nosotros directamente, sin usar el StringBinder que teníamos registrado… efectivamente, el valor aparece en minúsculas:



image



Bueno… espero que os esté gustando esta serie de posts sobre temas “internos” de ASP.NET MVC… aún nos quedan varias cosas por destripar!!!



Un saludo!!



PD: El código de ejemplo lo podéis descargar aquí (link a mi skydrive).



PD2: Como siempre… esto es un crosspost desde mi blog de geeks.ms… Pásate mejor por allí!!!

No hay comentarios: