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, 12 de julio de 2011
ASP.NET MVCTratar con tipos enumerados (enum en C#), a pesar de su comodidad e idoneidad en multitud de ocasiones, suele siempre una tarea pesada debido a la falta de soporte directo existente en algunas tecnologías.

Por ejemplo, hasta la fecha, Entity Framework los ha ignorado por completo (aunque que esto va a cambiar y la próxima versión de EF sí soportará enums :-)), lo cual resulta muy incómodo en el momento de crear componentes de acceso a datos, viéndonos obligados a realizar demasiadas conversiones, o incluso a veces el plantearnos otras soluciones, como el uso de constantes numéricas en su lugar para simplificar las operaciones.
public enum Color
{
    Rojo = 1,
    Verde = 2,
    Azul = 3
}
Ya centrados en ASP.NET MVC, un aspecto muy interesante del mecanismo de binding es su capacidad para trabajar de forma muy natural con estos tipos de datos, aunque esto pueda acarrear algunos problemillas. Veámoslo con un ejemplo partiendo del siguiente controlador:
public class EnumController : Controller
{
    public ActionResult Test(Color color)
    {
        return Content("Color: " + color);
    }
}
Suponiendo que utilizamos la ruta por defecto, una petición dirigida a la dirección URL /enum/test?color=1 retornará por pantalla el texto “Color: Rojo”. Durante el binding, el framework ha recuperado el parámetro “color” presente en la query string (un “1”) y lo ha transformado en el elemento de la enumeración correspondiente con este valor.

Y no sólo eso, también podemos hacer referencia al identificador asignado en la enumeración; así, una petición como /enum/test?color=Verde será capaz de interpretarlo de forma correcta, asignando al parámetro color el valor apropiado.

Pero en realidad no se trata de un mérito del binder, ni tan siquiera del framework MVC. Internamente, el proceso de conversión de los tipos enumerados se delega a la clase EnumConverter (presente en System.ComponentModel), que a su vez llamará a Enum.Parse() para obtener el valor apropiado desde el string obtenido por el value provider desde la query string. Este método examina la cadena y en función de su contenido buscará de una u otra forma el miembro de la enumeración a retornar:
  • Si la cadena comienza por un dígito o los signos “-“ o “+”, interpreta que el contenido del string es el valor asignado al elemento, por lo que intentará obtenerlo partiendo de éste. Por eso el ejemplo anterior, cuando en la petición asignábamos a color el valor “1” funcionaba correctamente.
  • En caso contrario, se asume que la cadena contiene directamente el identificador (“Rojo”, “Verde”…), y se intenta localizar en la enumeración el elemento que coincida con el suministrado. Por esta razón el segundo ejemplo, cuando en la petición asignábamos a color el valor “verde” también funcionaba correctamente.
Asimismo, este mecanismo funciona perfectamente cuando es una enumeración de flags. Como sabréis, estos tipos enumerados permiten definir “switches” o banderas combinables entre sí utilizando operadores lógicos binarios. Así, una petición como /enum/test/?color=Rojo,Verde será también interpretada de forma correcta, introduciendo en el parámetro color la combinación (un "or" binario) de ambos valores, de la misma forma que lo sería /enum/test/?color=1,2.

En resumen, esta flexibilidad a la hora de interpretar los valores y obtener el elemento correspondiente del enumerado es realmente potente y da mucho juego a la hora de crear en nuestras aplicaciones acciones muy expresivas y respetuosas con el protocolo HTTP sobre el que trabajamos. Sin embargo, esta potencia tiene también unas contraindicaciones que debemos conocer.

¿Y qué ocurre cuando los valores que se envían a una acción no son correctos?

Pues aquí llegan los problemas, claro ;-). Si a la acción anterior enviamos una petición incorrecta introduciendo en el navegador una URL como /enum/test?color=Amarillo veremos que se produce una excepción:

Error al suministrar un valor incorrecto

Observad que, a diferencia de lo que podríamos esperar, la excepción no hace referencia al hecho de que el miembro Amarillo no pertenece al enum, sino a que el parámetro color no puede contener un nulo. Es decir, el framework utiliza los componentes descritos anteriormente para intentar la conversión, pero éstos retornan un nulo y, dada la firma de la acción, éste es un valor que no puede aceptarse.

Para confirmarlo, sólo tenemos que utilizar un tipo anulable en el parámetro de la acción, así:
    public ActionResult Test(Color? color)
    ...

Simplemente con esa línea ya aseguramos que la aplicación no se parará en ejecución cuando llegue un nombre incorrecto del miembro de la enumeración, simplemente deberemos controlar lo que queremos hacer en caso de que color sea nulo.

Pero un problema aún más grave tenemos cuando el valor incorrecto viene expresado en forma numérica, indicando un valor inexistente en el tipo enumerado, por ejemplo en /enum/test?color=24. Fijaos que el valor está fuera de los permitidos, sería equivalente a hacer lo siguiente desde código:
    Color color = (Color) 24;
Seguro que ya sabéis lo que ocurre en este caso: nada. A la hora de asignar valores a una variable de tipo enum no se realiza de forma automática ningún tipo de comprobación que permita detectar que el valor no forma parte de los permitidos, por lo que la variable color contendrá un bonito 24 que en el contexto de nuestro sistema no significa absolutamente nada :-(

Y precisamente por eso decía el problema es más grave que antes, que al menos la excepción o el valor nulo en el parámetro de entrada indicaba que el valor recibido no es correcto. Si lo que nos llega es un número fuera de rango no nos enteraremos y “se colará” silenciosamente en nuestra lógica, lo que podría liar un desaguisado tremendo.

A continuación se muestra cómo podríamos gestionar en la acción la entrada de un dato incorrecto, bien sea por tratarse de un identificador inválido (que entraría un nulo) o bien un valor numérico fuera de los permitidos en la enumeración. Observad que utilizamos el método IsDefined() de la clase Enum para comprobar si el valor suministrado es válido:
    public ActionResult Test(Color? color)
    {
        if (!color.HasValue || !Enum.IsDefined(typeof(Color), color))
            throw new  ArgumentOutOfRangeException("color");
        

        return Content("Color: " + color);
    }
Supongo que estaréis pensando, y con razón, que sería demasiado trabajo repetirlo estas comprobaciones en todas las acciones que acepten parámetros de tipo enum, ¿verdad?

Controlando los valores de entrada mediante un model binder personalizado

Sin duda, sería mejor idea aprovechar las posibilidades de extensión del framework y crear rápidamente un binder específico que sea capaz de liberar a nuestro controlador de realizar las tareas de comprobación de los parámetros de entrada de tipo enum, como el siguiente:
public class EnumBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = base.BindModel(controllerContext, bindingContext);
        var type = bindingContext.ModelType;
 
        if (value!=null && Enum.IsDefined(type, value))
            return value;
            
        throw new ArgumentOutOfRangeException(bindingContext.ModelName);
    }
}
Como se observa en el código, se comprueba que el valor no sea nulo y que se encuentre entre los definidos en la enumeración, lanzando una excepción en caso contrario. Os dejo como deberes la implementación del binder que acepte enumeraciones de tipo flag, y parámetros de tipo anulable ;-)

A continuación tenemos que indicar al framework que debe utilizar este model binder para las enumeraciones. Una posibilidad sería registrar en la colección ModelBinders una asociación para cada tipo de enumeración, algo así:
    ModelBinders.Binders.Add(typeof(Color), new EnumBinder());
Desafortunadamente, esta fórmula de registro de binders no funciona para jerarquías de clases; es decir, no podemos registrar un binder para la clase Enum y que se aplique automáticamente para todas sus descendientes, por lo que si usamos muchas enumeraciones sería bastante tedioso registrar uno por uno los binders.

Otra posibilidad sería utilizar los nuevos ModelBinderProviders, una característica introducida en ASP.NET MVC 3, que permiten introducir lógica en el momento de obtención de los binders y, por tanto, decidir qué binder queremos asignar a cada clase. El siguiente código podría ser un proveedor simple que conseguiría el efecto deseado:
class MyModelBinderProvider: IModelBinderProvider
{
    public IModelBinder GetBinder(Type modelType)
    {
        if (modelType.IsEnum)
            return new EnumBinder();
        return null;
    }
}
Y su registro en el global.asax.cs sería así:
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        ModelBinderProviders.BinderProviders.Add(new MyModelBinderProvider());
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);
    }

En cualquier caso, sea cual sea la vía utilizada, una vez asociado el binder en la inicialización de la aplicación, ya controlará por nosotros la entrada de valores incorrectos para la enumeración:
    public ActionResult Test(Color color)
    {
        // Podemos asegurar que "color" es un color válido
        return Content("Color: " + color);
    }

Generación de rutas hacia acciones en cuyos parámetros hay enums

Ya hemos visto cómo funciona el mecanismo de binding en acciones entre cuyos parámetros se encuentran enums, pero, ¿cómo tenemos en cuenta esta particularidad a la hora de generar enlaces o rutas hacia estas acciones?

Pues la respuesta es bien simple: como si se tratara de cualquier otro tipo de datos. El siguiente código muestra la generación de enlaces hacia la acción vista anteriormente utilizando el helper Html.ActionLink(), primero de forma directa y a continuación utilizando T4MVC:
     @Html.ActionLink("Enlace a azul", "Test", "Enum", new {color = Color.Azul}, null)
     @Html.ActionLink("Enlace a verde", MVC.Enum.Test(Color.Verde))
Como se puede observar, no hay que realizar conversiones ni ningún otro tipo de malabarismos, simplemente podemos utilizar elementos de la enumeración de forma directa cuando sean necesarios :-)

Publicado en: Variable not found.

Aún no hay comentarios, ¡sé el primero!