ASP.NET MVC – Tratando con enums.

En un proyecto ASP.NET MVC en el que estoy colaborando, surgió la necesidad de tratar con viewmodels que tenían propiedades cuyo tipo era un enum. Algo así como:

[Flags]

public enum TestEnum

{

    None = 0,

    One = 1,

    Two =2,

    Four =4

}

 

public class FooModel

{

    public TestEnum TestData { get; set; }

}

Los valores de TestEnum son combinables a nivel de bits (de ahí que esté decorado con [Flags], es decir el valor de la propiedad TestData puede ser cualquiera de los cuatro o bien una combinación (p. ej. One y Two).

ASP.NET MVC no gestiona, por defecto, bien estos casos. Imaginemos que tenemos el siguiente código en el controlador:

public ActionResult Index()

{

    var model = new FooModel();

    model.TestData = TestEnum.One | TestEnum.Two;

    return View(model);

}

Y el código correspondiente en la vista:

@using (Html.BeginForm())

{

    @Html.EditorFor(x => x.TestData)  

    <input type="submit" value="enviar" />

}

Lo que nos generará será un TextBox con los valores del enum (en este caso One y Two) separados por comas:

image

Es obvio que no es una buena manera que el usuario entre esos datos. Por su parte el enlazado funcionará correctamente (es decir en el método que recibe el POST se recibirá que la propiedad TestData vale TestEnum.One | TestEnum.Two, ya que el DefaultModelBinder sí que es capaz de tratar este caso

En el proyecto lo que se quería era que en lugar de mostrar este texto se mostraran tantas checkboxes como valores tiene el enum y que se pudiesen marcar una o varias.

Eso se consigue con relativa facilidad usando un EditorTemplate. En ASP.NET MVC los EditorTemplates (y sus equivalentes de visualización los DisplayTemplates) son vistas parciales que son renderizadas cuando se debe de editar o visualizar un valor de un tipo o propiedad en concreto. Los EditorTemplates se colocan por lo general en la carpeta Views/Shared/EditorTemplates (los Display Templates en la Views/Shared/DisplayTemplates).

Así pues creamos un Editor Template que nos cree tantas checkboxes como valores tiene el enum:

@model Enum

@{

    var modelType = @Model.GetType();

}

 

@foreach (var name in Enum.GetNames(modelType))

{

    var value = Convert.ToInt32(Enum.Parse(modelType, name));

    if (value != 0)

    {

        var isChecked = ((Convert.ToInt32(Model) & value) == value) ? "checked" : null;

        <input type="checkbox" name="@ViewData.TemplateInfo.HtmlFieldPrefix" value="@name" checked="@isChecked" />  @name<br />

    }

}

El código es relativamente sencillo y lo que hace es crear una checkbox por cada clave (valor) del enum y marcarla si es necesario (es decir, si un and a nivel a de bits entre el valor del enum y el valor de cada clave da distinto de cero). Hay una comprobación addicional para no renderizar una checkbox si el valor de la clave es 0 (en nuestro caso sería el None). Para usar este EditorTemplate (que yo he llamado FlagEnum.cshtml) una posibilidad es indicarle al viewmodel que lo use, decorando la propiedad con [UIHint]:

public class FooModel

{

    [UIHint("FlagEnum")]

    public TestEnum TestData { get; set; }

}

Dado que en nuestra vista ya usábamos EditorFor para generar el editor de la propiedad TestData, no es necesario ningún cambio más. El resultado es ahora más interesante:

image

Parece que hemos terminado, eh? Pues no… Aparece un problema. Si le damos a enviar tal cual, lo que ahora recibimos en el método que recibe los datos POST es:

image

¡Ahora no se nos enlaza bien el campo! Eso es debido a como funciona el Model Binder de ASP.NET MVC. La diferencia con antes (cuando había el textbox) es que antes teníamos un solo campo en la petición y ahora tenemos N (tantos como checkboxes marcadas). Si comparamos las peticiones enviadas, lo veremos mejor.

Esta es la petición que el navegador envía si NO usamos nuestro EditorTemplate:

POST http://localhost:19515/ HTTP/1.1
Accept: text/html
Referer: http://localhost:19515/
Origin: http://localhost:19515
Content-Type: application/x-www-form-urlencoded

TestData=One%2C+Two

Y esta otra la petición que se envía si usamos el EditorTemplate:

POST http://localhost:19515/ HTTP/1.1
Accept: text/html
Referer: http://localhost:19515/
Origin: http://localhost:19515
Content-Type: application/x-www-form-urlencoded

TestData=One&TestData=Two

Fijaos en la diferencia (marcada en negrita). El Model Binder por defecto de ASP.NET MVC es capaz de gestionar el primer caso, pero no el segundo, de ahí que ahora no funcione y nos enlace tan solo el primer TestData que se encuentra (y que vale One).

¿Y la solución? Pues hacernos un Model Binder propio que sea capaz de tratar estos casos. Por suerte el código no es excesivamente complejo:

public class EnumFlagModelBinder : DefaultModelBinder

    {

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

        {

            if (!(bindingContext.Model  is Enum))

            {

                return base.BindModel(controllerContext, bindingContext);   

            }

 

            var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (value.RawValue is string[])

            {

                var tokens = (string[]) value.RawValue;

                int intValue = 0;

                foreach (var token in tokens)

                {

                    var currentIntValue = 0;

                    var isIntValue = Int32.TryParse(token, out currentIntValue);

                    if (isIntValue)

                    {

                        intValue |= currentIntValue;

                    }

                    else

                    {

                        var modelType = bindingContext.Model.GetType();

                        intValue |= Convert.ToInt32(Enum.Parse(modelType, token));

                    }

                }

 

                return Enum.Parse(bindingContext.Model.GetType(), intValue.ToString());

            }

 

            return base.BindModel(controllerContext, bindingContext);

 

 

        }

    }

El código es muy sencillo:

Si el tipo del modelo no es un Enum derivamos hacia el enlace por defecto de ASP.NET MVC. En caso contrario obtenemos el valor de la propiedad que se está enlazando, consultando a los Value Providers.

Este valor será un string[] con los diversos valores que el usuario ha entrado (p. ej. un string[] con dos elementos “One” y “Two”). Iteramos sobre este array de valores y por cada valor:

  1. Miramos si es un entero. Si lo es, hacemos un or a nivel de bits entre una variable (intValue inicialmente a cero) y este valor.
  2. Si NO es un entero (p.ej. es “One”) obtenemos el valor entero con Enum.Parse y realizamos el mismo or a nivel de bits.

Finalmente devolvemos el valor asociado al valor de entero que hemos obtenido.

P. ej. si el usuario ha marcado las checks “One” y “Two” el array (tokens) tendrá esos dos valores. Al final IntValue valdrá 3 (el resultado de hacer un or de bits entre 1 y 2) y devolveremos el resultado de hacer Enum.Parse de este 3 (que es precisamente One | Two).

Nota: El hecho de mirar primero si el valor de token es un entero, es un pequeño refinamiento para que nuestro EditorTemplate funcione también en aquellos casos en que nos envíen los valores numéricos asociados (que nos envien p. ej. TestData=1&TestData=2 en la petición. Usando nuestro EditorTemplate este caso no se da).

Ahora tan solo debemos indicarle a ASP.NET MC que use nuestro Model Binder para enlazar las propiedades de tipo TestEnum. Aunque nuestro Model Binder podría enlazar cualquier Enum (es genérico) ASP.NET MVC no nos permite asociar un Model Binder a “cualquier Enum”. Para indicarle a ASP.NET MVC que use nuestro Model Binder para las propiedades de tipo TestEnum basta con añadir en el Application_Start:

ModelBinders.Binders.Add(typeof(TestEnum), new EnumFlagModelBinder());

Y ¡listos! Con esto hemos terminado.

Espero que os haya sido interesante.

2 comentarios sobre “ASP.NET MVC – Tratando con enums.”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *