lunes, 10 de mayo de 2010

ASP.NET MVC: El DefaultModelBinder

En el post anterior vimos que eran los Value Providers de ASP.NET MVC. En éste, lo que vamos a ver es el DefaultModelBinder y algunas de sus “interioridades”…

Disclaimer: Al igual que el post anterior, este asume conocimientos básicos de ASP.NET MVC, así como de http en general.

Antes que nada el repaso rápido: Cuando un controlador recibe en una acción un objeto del modelo, ASP.NET MVC es capaz de realizar el binding entre los datos contenidos en la request y el objeto que espera el controlador. Por un lado los value providers se encargan de leer los datos de la request y guardarlos “en una estructura común” y por otro los model binders crean el objeto del modelo a partir de dicha estructura común.

Hoy vamos a diseccionar el DefaultModelBinder, el model binder que trae por defecto ASP.NET MVC.

Colecciones

Vamos a suponer la siguiente clase del modelo:

public class Persona
{
public IEnumerable<string> Telefonos { get; set; }
public string Nombre { get; set; }
public int Edad { get; set; }
}





Asumid que tenemos un controlador Personas con una acción Crear que recibe un objeto Persona. Vamos a ver como el DefaultModelBinder es capaz de mapear las propiedades, incluída la colección Telefonos. Para ello tengo simplemente la siguiente vista:




<form action="/Personas/Crear" method="post" >
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<label for="Nombre">Nombre</label>
<input type="text" name="Nombre" />
</div>
<div class="editor-label">
<label for="Edad">Edad</label>
<input type="text" name="Edad" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 1</label>
<input type="text" name="Telefonos" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 2</label>
<input type="text" name="Telefonos" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 3</label>
<input type="text" name="Telefonos" />
</div>
<input type="submit" />
</form>





Fijaos que hay 3 <input type=”text” name=”Telefono”>. Esto no es incorrecto, el parámetro name de un <input> puede estar repetido para indicar campos con más de un valor… Si introduzco datos y envío el formulario los campos POST de la request son:



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

Content-Length: 90

Nombre=Nombre&Edad=12&Telefonos=%2B34+555222&Telefonos=%2B34+666112&Telefonos=%2B34+777114



Honestamente… el navegador no es que haga gran cosa: simplemente tenemos 5 campos POST: Nombre, Edad y 3 campos Telefonos. Veamos ahora lo que ocurre en el lado del DefaultModelBinder.



El método dentro del DefaultModelBinder que obtiene el valor de una propiedad se llama GetPropertyValue y tiene la siguiente firma:




protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)





De todos los parámetros que recibe este método nos interesan básicamente dos:




  1. bindingContext: Un objeto de la clase ModelBindingContext que contiene la información necesaria para poder realizar el binding: Proporciona acceso a los value providers, al ModelState (donde se guarda información relativa a la validación del modelo) y a los metadatos del modelo.


  2. propertyDescriptor: Un objeto de la clase PropertyDescriptor con información sobre la propiedad del modelo de la cual queremos obtener el valor (básicamente el tipo de la propiedad).



A partir de la información del bindingContext el DefaultModelBinder puede preguntar a los value providers que le den el valor del campo que contiene el valor de la propiedad (por defecto si estamos rellenando la propiedad Telefonos el DefaultModelBinder preguntará a los value providers por el valor de Telefonos). El DefaultModelBinder usará la información del propertyDescriptor para convertir el valor devuelto por los value providers al tipo del que sea la propiedad.



Luego el framework llama al método SetPropertyValue del propio model binder, que tiene esta firma:




protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)





Los parámetros son más o menos los mismos que GetPropertyValue, con la salvedad de que recibimos también el objeto devuelto por GetPropertyValue. En este método “simplemente” asignamos el valor a la propiedad (podemos acceder al modelo a través del la propiedad Model del parámetro bindingContext).



Bueno, en el caso de la petición POST que nos ocupa esto es lo que devuelve el método GetPropertyValue de la propiedad Teléfonos:



image



El DefaultModelBinder, a partir de la información de los value providers, crea un array de cadenas con el valor de los tres teléfonos (recordad que la propiedad estava declarada como IEnumerable<string> en el modelo).



Hemos visto como a partir de una petición POST con varios campos “Telefonos” el DefaultModelBinder era capaz de enlazar esto a una variable IEnumerable<string> del modelo… Sigamos jugando un poco a ver que pasa… :)



Imaginad que hago la siguiente modificación en la vista:




<div class="editor-label">
<label for="Telefonos">Telf 1</label>
<input type="text" name="Telefonos[0]" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 2</label>
<input type="text" name="Telefonos[1]" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 3</label>
<input type="text" name="Telefonos[2]" />
</div>





Es decir, en lugar de tener tres campos cuyo name es Telefonos, tengo tres campos cuyo name es Telefonos[0], Telefonos[1] y Telefonos[2]. Si relleno los datos y envio la petición, ahora los datos POST tienen la forma:



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

Content-Length: 105

Nombre=eiximenis&Edad=20&Telefonos%5B0%5D=%2B34+111&Telefonos%5B1%5D=%2B34+222&Telefonos%5B2%5D=%2B34+333



Ahora en los datos POST tenemos (además de Nombre y Edad) tres campos más, llamados Telefonos%5B0%5D, Telefonos%5B1%5D, y Telefonos%5B2%5D (%5B es el código para [ y %5D es el código para ]).



Pues bien: eso funciona correctamente… el DefaultModelBinder en el GetPropertyValue para la propiedad Telefonos hace lo siguiente (es una simplificación de lo que ocurre realmente, pero es suficiente para entender el concepto):




  1. Pregunta a los value providers para el valor de Telefonos y el resultado es null (no hay ningún campo “Telefonos”)


  2. Como el DefaultModelBinder sabe que está enlazando una colección, pregunta a los value providers por el valor de Telefonos[0] y si existe lo añade a la colección de la propiedad Telefonos y pregunta por Telefonos[1]… y así sucesivamente.



No me puteéis el pobre DefaultModelBinder, que es bastante inteligente pero tampoco es dios… eso no funciona:




<div class="editor-label">
<label for="Telefonos">Telf 1</label>
<input type="text" name="Telefonos[1]" />
</div>
<div class="editor-label">
<label for="Telefonos">Telf 2</label>
<input type="text" name="Telefonos[2]" />
</div>





En este caso NO tenemos ni campo “Telefonos”, ni campo “Telefonos[0]" en la request, así que el DefaultModelBinder entenderá que la propiedad Telefonos no ha sido informado y la pondrá a null.



Objetos compuestos



Imaginad ahora que tenemos nuestro modelo definido del siguiente modo:




public class Persona
{
public IEnumerable<string> Telefonos { get; set; }
public string Nombre { get; set; }
public int Edad { get; set; }

public Direccion Direccion { get; set; }
}

public class Direccion
{
public string Calle { get; set; }
public string Ciudad { get; set; }
}





Como se las apaña el DefaultModelBinder para enlazar la propiedad Direccion que es un objeto completo??



Para ver como se las puede apañar el DefaultModelBinder, vamos a dejar que el propio framework nos ayude :) Par ello añado las siguientes líneas dentro del <form> de la vista:




<div class="editor-label">
<%: Html.LabelFor(x=>x.Direccion) %>
<%: Html.EditorFor(x=>x.Direccion) %>
</div>





Recordad que Html.LabelFor lo que hace es crear una <label> vinculada a la propiedad del modelo que se le pasa vía la expresión lambda. Por otro lado Html.EditorFor lo que hace es crear un “editor” para la propiedad del modelo que se le indica… El framework usa los metadatos del modelo para saber que tipo de editor es mejor (además de que nosotros podemos definir el tipo editor que queramos, pero eso es otra historia). Cuál creeis que es el “editor por defecto” para la propiedad Direccion del modelo, que es un objeto de la clase Direccion?



Si habéis respondido “dos textboxes, uno para la propiedad Calle y el otro para la propiedad Ciudad” habeis dado en el clavo… este es el código HTML que me genera la llamada a Html.EditorFor:




<div class="editor-field">
<input class="text-box single-line" id="Direccion_Calle" name="Direccion.Calle" type="text" value="" />
</div>
<div class="editor-label">
<label for="Direccion_Ciudad">Ciudad</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Direccion_Ciudad" name="Direccion.Ciudad" type="text" value="" />
</div>





Exacto, dos <input type=”text”> uno para la propiedad Calle y el otro para la propiedad Ciudad… pero os habéis fijado en los name?




  • Direccion.Calle


  • Direccion.Ciudad



No vamos a entrar dentro del ciclo de vida entero del DefaultModelBinder (porque daría para otro post entero) y pienso que nos basta con esta idea: El DefaultModelBinder es capaz de procesar modelos complejos siempre y cuando los value providers tengan valores cuyo nombre sea propiedad.propiedad.propiedad…



Validación del modelo



El DefaultModelBinder no sólo crea objetos del modelo y los enlaza… también es el responsable de validar que el modelo cumpla con las restricciones que tiene. Las restricciones forman parte de los metadatos del modelo y por lo tanto están accesibles a través del parámetro bindingContext (propiedad ModelMetaData). El DefaultModelBinder comprueba que los valores de las propiedades satisfacen las restricciones que indican los metadatos. Si no es el caso, utiliza el ModelState (accesible a través del bindingContext) y añade un error (llamando al método AddModelError) para indicar que la propiedad determinada no cumple la restricción indicada.



El proveedor de metadatos para el modelo que usa ASP.NET MVC por defecto es DataAnnotations, pero nosotros podríamos crearnos nuestros propios proveedores de metadatos (sí, sí… ASP.NET MVC es sumamente extensible). Pero eso también ya es una historia para otro post… :)



Resumiendo…



El Model Binder es el encargado de crear un objeto del tipo correspondiente al que espera la acción de destino del controlador y de rellenarlo con los valores que provienen de la petición, aunque para ello no consulta los datos de la petición, sinó que consulta los datos de los value providers (que son quienes previamente han consultado la petición). Eso independiza al Model Binder de la forma en cómo los datos estén codificados en la petición. Finalmente el Model Binder también valida que el modelo cumpla las restricciones indicadas en los metadatos asociados.



Hemos visto por encima como funciona el DefaultModelBinder, el Model Binder que viene por defecto en ASP.NET MVC… Como (casi) todo en ASP.NET MVC podemos cambiar el DefaultModelBinder por otro a nuestro antojo, o lo que suele ser más normal, asociar un Model Binder específico a un tipo de modelo… Pero eso lo veremos en otro post… :)



Un saludo!



PD: Esto es un crosspost desde mi blog en geeks.ms… Pásate por allí mejor! :)

No hay comentarios: