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, 16 de octubre de 2012
ASP.NET MVCSi hay algo que me gusta de ASP.NET MVC es la cantidad de fórmulas que ofrece para aumentar nuestra productividad. Prácticamente cualquier código que estemos hartos de repetir una y otra vez puede ser encapsulado y reutilizado usando los puntos de extensión que nos proporciona el framework.

Hoy vamos a ver una solución a un problema al que seguro nos hemos enfrentado cientos de veces: la edición en formularios de propiedades de tipo enum.

1. El escenario

Imaginad que tenemos dos enumeraciones como las siguientes:
public enum Color
{
    Black, Blue, Red, Green
}

public enum Language
{
    English, Spanish, Italian, Portuguese
}

Usadas una clase del Modelo tal que:
public class Contact
{
    public string Name { get; set; }       
    public Language MainLanguage { get; set; }
    public Color? FavouriteColor { get; set; }
}
Y, a continuación, queremos crear un formulario de edición para la misma, con el código:
<h2>Edit contact</h2>
@using(Html.BeginForm())
{
    @Html.EditorForModel()
    <input type="submit" value="Send" />
}
imageComo era de esperar, el resultado una vez en ejecución es algo similar a lo que podemos ver en la captura de pantalla de la derecha.

Las propiedades MailLanguage y FavouriteColor, a pesar de ser enumeraciones, muestran como control de edición un simple cuadro de texto en el que podemos introducir cualquier cosa.

Y dado que son enum y podemos conocer sus valores de antemano, ¿no sería más lógico que la edición de estas propiedades se realizaran utilizando desplegables? 

2. Editor template para enums

Sin duda, la fórmula más sencilla y reutilizable para conseguir el objetivo que pretendemos es crear una plantilla de edición personalizada para las enumeraciones.

Como seguro sabréis, los editor templates proporcionan un mecanismo para generar controles de edición específicos para tipos de datos; podemos tener un editor para propiedades de tipo string (p.e., un cuadro de texto), de tipo DateTime (p.e., un cuadro de texto con un date picker), etc. Lo único que hay que hacer es crear en la carpeta /Views/Shared/EditorTemplates vistas parciales con el nombre del tipo de datos para el que se desea crear el editor (string.cshtml, datetime.cshtml, etc.).

Por tanto, probemos introduciendo el siguiente código en /Views/Shared/EditorTemplates/Enum.cshtml:
@model Enum
@{
    var isRequired = this.ViewData.ModelMetadata.IsRequired;

    var type = this.ViewData.ModelMetadata.ModelType;
    if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
    {
        type = type.GenericTypeArguments[0];
    }

    var values = Enum.GetValues(type);
    var items = (from object value in values
                 select new SelectListItem
                            {
                                Text = Enum.GetName(type, value),
                                Value = value.ToString()
                            });
}
@Html.DropDownListFor(m=>m, items, isRequired ? "Select...": "(None)")
Como se puede observar, prácticamente sólo hacemos tres cosas:
  • En primer lugar, determinamos la obligatoriedad de la propiedad a editar observando los metadatos del modelo.  
  • A continuación, creamos una lista de elementos de tipo SelectListItem recorriendo los valores disponibles en la enumeración. Observad que es necesario comprobar si el tipo que nos está llegando es anulable (Nullable<TEnum>)  y obtener el tipo de la enumeración de su parámetro genérico. 
  • Por último, usamos DropDownListFor() para generar el desplegable. En caso de tratarse de una propiedad obligatoria, añadimos un primer elemento con el texto “Select…”, y en caso contrario usamos el texto “(None)” para identificar el elemento nulo.
Y con esto ya tenemos hecho la mayor parte del trabajo, aunque aún nos falta un detalle. Por alguna razón que no llego a entender, ASP.NET MVC utiliza la plantilla string.cshtml (o el editor por defecto para el tipo string)  para editar las propiedades de tipo enum, lo cual nos obliga a indicar en las propiedades la plantilla a utilizar (las dos fórmulas usadas a continuación son equivalentes):
public class Contact
{
    public string Name { get; set; }  
    [UIHint("Enum")]
    public Language MainLanguage { get; set; }
    [DataType("Enum")]
    public Color? FavouriteColor { get; set; }
}
imageYa en ejecución, observamos que el resultado ha mejorado notablemente, como se puede apreciar en la captura de pantalla :-)

En este punto podríamos considerar que hemos alcanzado el objetivo que nos proponíamos, pero… ¿no os parece aún demasiado trabajo tener que decorar cada propiedad de tipo enumeración con estos atributos? ¿No podríamos dejar al framework que se encargue de estas minucias?

3. Proporcionando metadatos con metadata providers

Ya hemos hablado por aquí en varias ocasiones de los proveedores de metadatos, o metadata providers. De hecho, si no tienes claro lo que son, te recomendaría que leyeras el post “Mamá, ¿de dónde vienen los metadatos” que publiqué haces unos meses.

Muy resumidamente, se trata del mecanismo que obtiene los metadatos de las clases del Modelo desde donde se encuentren definidos. El proveedor por defecto los obtiene examinando las clases y sus propiedades mediante reflexión y obteniendo los atributos (anotaciones) que definen restricciones y otras características, pero podemos crear proveedores personalizados que las extraigan desde otras fuentes.

Y vamos a utilizar esta característica para evitar tener que especificar el atributo [UIHint] a las propiedades de tipo enum. El proveedor cuyo código veremos a continuación hereda del usado por defecto en ASP.NET MVC 4 (CachedDataAnnotationsModelMetadataProvider) y sobrescribe el procedimiento de obtención de metadatos:
public class EnumMetadataProvider: CachedDataAnnotationsModelMetadataProvider
{
    protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(
        CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
    {
        var metadata = base.CreateMetadataFromPrototype(prototype, modelAccessor);
        var type = metadata.ModelType;
        if (type.IsEnum ||
            (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && 
            type.GetGenericArguments().Length == 1 && type.GetGenericArguments()[0].IsEnum))
        {
            metadata.TemplateHint = "Enum";
        }
        return metadata;
    }
}
Observad que lo único que estamos haciendo es invocar el método de la clase base y, posteriormente, comprobar si el tipo de la propiedad es un enumerado o un tipo anulable de enumerado, en cuyo caso introduce en la propiedad de metadatos TemplateHint el nombre de la plantilla a utilizar (“Enum”).

Por último, ya sólo queda establecer un objeto de este tipo como proveedor de metadatos por defecto para la aplicación:
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        // ...

        ModelMetadataProviders.Current = new EnumMetadataProvider();
    }
}
De esta forma, por fin podemos eliminar de nuestra clase del Modelo los molestos atributos y todas las enumeraciones de nuestra aplicación se mostrarán en el formulario con la plantilla de edición que hemos creado anteriormente :-)

Publicado en: Variable Not Found.

7 Comentarios:

Juan Francisco dijo...

Hola, muy bueno el post, como siempre. ¿Y cómo se haría eso mismo en ASP.NET MVC3?

josé M. Aguilar dijo...

Hola!

En MVC3 sería igual, salvo en la implementación del proveedor de metadatos.

En este caso, habría que heredar de DataAnnotationsModelMetadataProvider e implementar el método CreateMetadata, tal y como se describe en el post http://www.variablenotfound.com/2011/11/mama-de-donde-vienen-los-metadatos.html.

La implementación del método sería muy parecida a la que vemos aquí: dejamos que se ejecute la lógica por defecto (la de la clase base) y, tras ello, comprobamos si el tipo de la propiedad es un enum para ajustarle el TemplateHint.

Inténtalo y si no lo consigues lo vemos en mayor detalle.

Saludos & gracias por comentar.

CachedDataAnnotationsModelMetadataProvider

Juan Francisco dijo...

Hola de nuevo. Al final conseguí crear el provider en MVC3, pero me da un error en el código de la vista, ya que en el framework 4 no existe la propiedad GenericTypeArguments en la clase Type.

¿Cómo se haría en este caso?

Muchas gracias.

josé M. Aguilar dijo...

Hola!

En .NET 4 puedes usar type.GetGenericArguments()[0] para obtener el tipo del primer parámetro genérico, en este caso, el tipo envuelto por el nullable.

Un saludo!

Jose dijo...

Muy buen post !
Una pregunta, y en BBDD qué sería el enum, un entero ?
Gracias

José María Aguilar dijo...

Hola!

El mapeo más natural es hacia campos de tipo entero en base de datos. Aunque si usas un ORM como Entity Framework o NHibernate, podrás persistir directamente propiedades de tipo Enum.

Saludos!

leviatanMX dijo...

Hola pues esta interesante... pero creo que comprometes la vista al tener que meter codigo dentro de ella, cuando lo que se busca es no hacerlo... no seria mejor crear un helper para reutilizarlo