Extendiendo ASP.NET MVC 3 – Model Binders
Como vimos en el post anterior Model Binding es el proceso de transformar los datos que se obtienen al momento del Request (como por ejemplo valores de Forms, Query Strings, etc.) en Modelos.
Por cada valor que se quiera obtener para crear un Modelo, se deben seguir mínimamente los siguientes pasos: obtener el valor buscado utilizando los Value Providers, mantener el estado de ese valor para poder utilizarlo en el caso de necesitarlo (por ejemplo para mostrar un error), y aplicar las validaciones necesarias para asegurarnos que el valor obtenido cumple con los requisitos para pertenecer a nuestro Modelo.
En nuestro siguiente ejemplo vamos a ver una implementación de un Model Binder que se utiliza para crear una instancia de la clase Frase, la cual tiene una referencia a la clase Autor. Este Model Binder se encarga de obtener la instancia de Autor correspondiente al código que se envía en el Request y termina generando una instancia de la clase Frase con la referencia al Autor obtenido.
Veamos el código de las clases Frase y Autor:
{
public Frase()
{
Comentarios = new List<Comentario>();
Votos = new List<Voto>();
}
public int FraseId { get; set; }
[Required(ErrorMessage="Si no vas a decir nada no molestes che!!!")]
public string Texto { get; set; }
public List<Voto> Votos { get; set; }
public List<Comentario> Comentarios { get; set; }
public int AutorId { get; set; }
public Autor Autor { get; set; }
public int CantidadVotos
{
get { return CalcularVotos(); }
}
private int CalcularVotos()
{
var meGusta = Votos.Where(x => x.MeGusta).Count();
var noMeGusta = Votos.Where(x => !x.MeGusta).Count();
return meGusta - noMeGusta;
}
}
{
public int AutorId { get; set; }
public string Nombre { get; set; }
public List<Frase> Frases {get; set;}
public Autor()
{
Frases = new List<Frase>();
}
}
Nuestro Model Binder se va a encargar de generar una instancia de la clase Frase, con la propiedad Texto y la instancia de Autor completas. Para ello va a obtener del Request, mediante el Value Provider, los valores para la clave “Texto”, y para la clave “Autor”. Con el valor de Autor, obtendremos la instancia correspondiente para asignarla a la Frase. Si se fijan en la definición de la clase Frase, van a ver que el campo “Texto” es un campo requerido el cual posee un mensaje de error especial. Esta Metadata de validación va a ser utilizada por el Model Binder para validar la completitud del valor.
Dicho esto, veamos el código del Model Binder:
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var frase = new Frase();
frase.Texto = Get<string>(controllerContext, bindingContext, "Texto");
var nombreAutor = Get<string>(controllerContext, bindingContext, "Autor");
var autor = FrasesService.ObtenerAutor(nombreAutor);
frase.Autor = autor;
return frase;
}
private TModel Get<TModel>(ControllerContext controllerContext,
ModelBindingContext bindingContext,
string name)
{
// Obtengo el Valor del ValueProvider
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(name);
// Genero el Model State con el valor. Esto me permite devolver el valor para nuevos bindings en caso de que
// sea invalido, (Ej: Valores string en propiedades int).
ModelState modelState = new ModelState { Value = valueProviderResult };
bindingContext.ModelState.Add(name, modelState);
// Convierto al tipo especifico
var model = (TModel)valueProviderResult.ConvertTo(typeof(TModel));
// Obtengo la metadata de la propiedad, necesaria para la validacion
var metadata = bindingContext.PropertyMetadata[name];
metadata.Model = model;
// Usando la Metadada y el valor corro las validaciones
IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(metadata, controllerContext);
foreach (var validator in validators)
foreach (var validatorResult in validator.Validate(bindingContext.Model))
modelState.Errors.Add(validatorResult.Message);
return model;
}
}
Como primera observación podemos ver que los Model Binders deben implementar la Interfaz IModelBinder, la cual posee el método BindModel que recibe como parámetros la información de contexto del controller dentro de una instancia de la clase ControllerContext y también la información de contexto del proceso de Binding dentro de una instancia de la clase ModelBindingContext.
Con estos dos parámetros se puede acceder a todo lo necesario para realizar Model Binding: Value Providers, Model State, Metadata de Modelos, etc.
Nuestro Model Binder posee un método privado Get, el cual se encarga de obtener un valor que se corresponda al nombre de una clave.
Este método posee cinco partes interesantes, la primera donde se usa el Value Provider para obtener el valor el cual viene wrapeado en una instancia de la clase ValueProviderResult, la segunda donde se genera el Model State para mantener el valor que se obtuvo para poder ser utilizado según necesidad (por ejemplo si esperamos un entero y recibimos un string, vamos a querer que ese string se mantenga para poder presentarse en la pagina nuevamente en el mismo input donde fue ingresado junto con el mensaje de error correspondiente), la tercera donde se toma el valor obtenido y se transforma al tipo esperado, la cuarta donde se obtiene la Metadata del Modelo para la propiedad que corresponde la clave, y la quinta donde se utiliza la información de validación obtenida en la metadata para realizar las validaciones y en el caso de que no pasen almacenar los errores en el ModelState instanciado para la clave que estamos buscando.
Una vez completada la obtención de valores y construcción del Modelo, el método BindModel devuelve la instancia de Frase con el Texto y el Autor completos.
Con esto, tenemos completo nuestro Custom Model Binder para ahora, como hicimos con el Custom Value Provider, el siguiente paso es “decirle” a MVC que existe un nuevo Model Binder y que tiene que ser considerado para ser utilizado al momento de realizar el Binding para la clase Frase. Esto se realiza dentro del Global.Axasx, como se muestra en el siguiente snippet:
{
ModelBinders.Binders.Add(typeof(Frase), new FraseModelBinder());
}
Con este último punto, nuestra aplicación esta lista para utilizar nuestro nuevo ModelBinder!
En la próxima semana continuaremos con el siguiente punto de extensibilidad: View Engines.
Nos leemos!!