martes, 29 de noviembre de 2011
En ASP.NET MVC normalmente utilizamos atributos para aportar información adicional a las propiedades del Modelo, incluyendo detalles como su descripción textual, formato de presentación, tipo de datos, etc. Esta información puede ser utilizada desde la capa vista para generar etiquetas, editores y, en algunos casos, incluso lógica de edición o presentación en la página.
Sin embargo, los atributos en el propio código de la clase no son la única vía para especificar metadatos en el framework. En este post veremos cómo extender el framework para crear nuevas vías para especificar esta información.
Sea cual sea su origen, los metadatos siempre se representan como objetos del tipo
¿Y cómo determina el framework MVC qué proveedor de metadatos utilizar?
En primer lugar, intenta utilizar el dependency resolver para obtener una instancia de un tipo que implemente
Si no ha sido posible obtener un proveedor desde el dependency resolver, el framework utilizará el establecido en la propiedad
Por tanto, para crear un proveedor personalizado de metadatos, basta con:
Como sabéis, al mostrar en una vista las etiquetas (labels) asociadas a una propiedad del Modelo, el texto que aparece es obtenido desde los metadatos de la entidad, desde el atributo
Pues bien, nuestro objetivo es conseguir generar de forma automática estas las descripciones (
Para ello, dado que queremos conservar el comportamiento del proveedor por defecto
Cuando el framework intenta obtener el proveedor de metadatos actual, antes de nada intenta localizarlo utilizando el dependency resolver. Si estamos utilizando un contenedor IoC como Unity o StructureMap, podemos utilizarlos para gestionar esta dependencia, asociando la clase abstracta
Por ejemplo, en el caso de Unity, lo más sencillo es instalar el paquete Unity.MVC3 desde Nuget, y ya simplemente tendríamos que incluir la siguiente línea en el registro de tipos (en el archivo bootstrapper.cs):
Puedes descargar el proyecto de ejemplo (VS2010 + MVC 3) desde mi Skydrive.
En definitiva, en este post hemos visto que en ASP.NET MVC los metadatos no proceden de una ubicación fija, sino que son obtenidos a través de un proveedor configurable. Esto nos permite modificar el comportamiento por defecto del framework y adaptarlo a nuestras necesidades, generarlos de forma dinámica, como en el ejemplo que hemos visto, u obtenerlo desde algún almacén de persistencia (como podría ser un archivo de configuración o una bases de datos), y siempre de forma muy sencilla aprovechando la magnífica extensibilidad del marco de trabajo :-)
Publicado en: Variable not found.
Sin embargo, los atributos en el propio código de la clase no son la única vía para especificar metadatos en el framework. En este post veremos cómo extender el framework para crear nuevas vías para especificar esta información.
1. Proveedores de metadatos
Como decíamos, ASP.NET MVC viene de fábrica con componentes que nos permiten introducir anotaciones directamente sobre las propiedades de las clases que manejamos. En el siguiente ejemplo podemos ver una entidad en la que se están introduciendo metadatos según este mecanismo:public class Friend
{
[Required]
[Display(Name="Full name")]
public string FullName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email address")]
public string EmailAddress { get; set; }
[Required]
[DataType(DataType.Url)]
[Display(Name = "Blog url")]
public string BlogUrl { get; set; }
[Display(Name="Birth year")]
public int BirthYear { get; set; }
}
Cuando el framework necesita obtener los metadatos relativos a una clase, utiliza un metadata provider, un componente que se encarga de obtener los metadatos desde donde se encuentren definidos. El proveedor usado por defecto se llama DataAnnotationsModelMetadataProvider
, y es el responsable de leer los atributos desde la definición de la clase, pero podemos sustituirlo fácilmente por otro proveedor que obtenga los metadatos desde otros orígenes, como archivos de configuración, bases de datos, o simplemente introducir lógica durante la generación de los mismos.Sea cual sea su origen, los metadatos siempre se representan como objetos del tipo
ModelMetadata
, cuya estructura podéis ver a la derecha. Los providers deben incluir la lógica para obtener los metadatos desde donde corresponda, pero siempre retornarán objetos ModelMetadata
, en los que, entre otra información, podemos encontrar los siguientes datos:- descripción del elemento,
- cadena de formato cuando es visualizado,
- tipo de datos que contiene (emails, urls, fechas, horas, etc.),
- orden en el que debe aparecer la propiedad si se generan interfaces de edición o visualización de la entidad,
- representación textual para los nulos,
- si el dato es obligatorio, o de sólo lectura,
- si debe ser mostrado en edición, o en visualización,
- la plantilla de edición o visualización que debe utilizarse,
- … y muchos más. Podéis consultar la referencia completa en MSDN.
¿Y cómo determina el framework MVC qué proveedor de metadatos utilizar?
En primer lugar, intenta utilizar el dependency resolver para obtener una instancia de un tipo que implemente
ModelMetadataProvider
, que es la clase base para todos los proveedores. Este punto de extensión permite definir un proveedor personalizado muy fácilmente, y sobre todo si utilizamos contenedores IoC.Si no ha sido posible obtener un proveedor desde el dependency resolver, el framework utilizará el establecido en la propiedad
ModelMetadataProviders.Current
. Por defecto, esa propiedad contiene una un objeto de la clase DataAnnotationsModelMetadataProvider
, pero podemos sustituirlo por cualquier otro descendiente de ModelMetadataProvider
.Por tanto, para crear un proveedor personalizado de metadatos, basta con:
- crear una clase descendiente de
ModelMetadataProvider
. Por comodidad, usaremos normalmente como base algunos de los siguientes tipos, más concretos:DataAnnotationsModelMetadataProvider
, si lo que pretendemos es extender el sistema por defecto, basado en la captura de metadatos desde las anotaciones (atributos) de la clase.AssociatedMetadataProvider
, si lo que queremos es saltarnos por completo la obtención de metadatos desde la propia clase e implementar otros mecanismos.
- indicar al framework que debe utilizar nuestro nuevo proveedor, que podemos hacerlo:
- o bien usando el dependency resolver,
- o bien estableciendo una instancia del nuevo provider en
ModelMetadataProviders.Current.
2. Creando un metadata provider
Vamos a implementar un proveedor de metadatos sencillo con objeto de que podáis entender su funcionamiento.Como sabéis, al mostrar en una vista las etiquetas (labels) asociadas a una propiedad del Modelo, el texto que aparece es obtenido desde los metadatos de la entidad, desde el atributo
Display
o DisplayName
(en ese orden); en caso de no existir, se asume como descripción el nombre de la propiedad.Pues bien, nuestro objetivo es conseguir generar de forma automática estas las descripciones (
DisplayName
) de cada campo partiendo del nombre de la propiedad, y teniendo en cuenta el “camel casing”, de forma que podremos ahorrarnos el teclear esta descripción en muchos casos. Por ejemplo, a una propiedad que se llame FullName
se le asociará automáticamente la descripción “Full Name”, o EmailAddress
se describirá como “Email Address”. De esta forma la entidad anterior podremos simplificarla un poco:public class Friend
{
[Required]
public string FullName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string EmailAddress { get; set; }
[Required]
[DataType(DataType.Url)]
public string BlogUrl { get; set; }
public int BirthYear { get; set; }
}
Fijaos que hemos eliminado los atributos [Display]
, y el resultado en ejecución de un editor de esta entidad se mostrará como en la captura de pantalla lateral, donde las etiquetas de cada campo han sido generadas automáticamente.Para ello, dado que queremos conservar el comportamiento del proveedor por defecto
DataAnnotationsModelMetadataProvider,
lo extenderemos y añadiremos la lógica deseada a su método CreateMetadata()
, que es el invocado para obtener los metadatos de la entidad y cada una de las propiedades:public class DisplayNameModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes, Type containerType,
Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType,
modelAccessor, modelType, propertyName);
if (metadata.PropertyName != null && metadata.DisplayName == null)
{
metadata.DisplayName = splitCamelCase(metadata.PropertyName);
}
return metadata;
}
static string splitCamelCase(string input)
{
return Regex.Replace(input, "([A-Z])", " $1",
System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
}
}
La implementación del método CreateMetadata()
no es nada del otro mundo:- En primer lugar, llamamos a la clase antecesora para obtener los metadatos a partir de las anotaciones del modelo.
- A continuación, si no se ha obtenido ningún valor de metadatos para el
DisplayName
, y siempre que se esté evaluando una propiedad, la establecemos, generándola mediante el métodosplitCamelCase()
. - El método
splitCamelCase()
no tiene mucho misterio (bueno, sí, usa expresiones regulares ;-)), y lo único que hace es buscar las letras mayúsculas e insertar delante de ellas un espacio. Es algo tosco, pero me vale para no desviar la atención del post a este detalle sin importancia (y por cierto, el mérito de este método no es mío, sino de Jon Galloway).
2.1. Registro del proveedor directamente
La fórmula más rápida y sencilla consiste en establecer directamente una instancia del nuevo proveedor en la propiedadModelMetadataProviders.Current
, durante la inicialización de la aplicación, por ejemplo así: protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelMetadataProviders.Current = new DisplayNameModelMetadataProvider();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Esto es todo lo que necesitamos hacer para que nuestro flamante proveedor entre en funcionamiento.2.2. Registro del proveedor usando el dependency resolver
Si estáis utilizando un contenedor de IoC, quizás sea más coherente introducir la resolución de este proveedor en el mismo.Cuando el framework intenta obtener el proveedor de metadatos actual, antes de nada intenta localizarlo utilizando el dependency resolver. Si estamos utilizando un contenedor IoC como Unity o StructureMap, podemos utilizarlos para gestionar esta dependencia, asociando la clase abstracta
ModelMetadataProvider
al tipo concreto DisplayNameModelMetadataProvider
.Por ejemplo, en el caso de Unity, lo más sencillo es instalar el paquete Unity.MVC3 desde Nuget, y ya simplemente tendríamos que incluir la siguiente línea en el registro de tipos (en el archivo bootstrapper.cs):
private static IUnityContainer BuildUnityContainer()
{
var container = new UnityContainer();
container.RegisterType<ModelMetadataProvider, DisplayNameModelMetadataProvider>();
container.RegisterControllers();
return container;
}
Una vez registrado el provider, sea directamente o mediante este último mecanismo, el sistema obtendrá los metadatos desde éste. Así, tendríamos el siguiente resultado:Clase del Modelo | Vista | Resultado |
public class Friend
{
[Required]
public string FullName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string EmailAddress
{ get; set; }
[Required]
[DataType(DataType.Url)]
public string BlogUrl { get; set; }
public int BirthYear { get; set; }
}
| @using (Html.BeginForm())
{
@Html.EditorForModel()
<div>
<input type="submit" />
</div>
}
|
En definitiva, en este post hemos visto que en ASP.NET MVC los metadatos no proceden de una ubicación fija, sino que son obtenidos a través de un proveedor configurable. Esto nos permite modificar el comportamiento por defecto del framework y adaptarlo a nuestras necesidades, generarlos de forma dinámica, como en el ejemplo que hemos visto, u obtenerlo desde algún almacén de persistencia (como podría ser un archivo de configuración o una bases de datos), y siempre de forma muy sencilla aprovechando la magnífica extensibilidad del marco de trabajo :-)
Publicado en: Variable not found.
2 Comentarios:
Perfecto tras el webcast que me zampe ayer! :D
Estupendo :-)
Gracias por comentar!
Enviar un nuevo comentario