Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 29 de noviembre de 2011
ASPNETMVCEn 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.

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.

ModelMetadataSea 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.
Es importante tener en cuenta que el proveedor es invocado una vez para obtener los metadatos de la clase en sí, y otra vez por cada una de sus propiedades.

¿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.
Pero mejor veámoslo con un ejemplo…

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:Resultado de EditorForModel() con la entidad Friend
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étodo splitCamelCase().
  • 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).
Y esto es todo. Ahora vamos a registrar el proveedor para el framework pueda utilizarlo, y veremos cómo hacerlo de las dos formas que he comentado antes: de forma directa, y usando el dependency resolver.

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 propiedad ModelMetadataProviders.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 ModeloVistaResultado
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>
}
image
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.

2 Comentarios:

Sergi dijo...

Perfecto tras el webcast que me zampe ayer! :D

josé M. Aguilar dijo...

Estupendo :-)

Gracias por comentar!