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, 23 de mayo de 2017
ASP.NET MVC 5El amigo José Antonio M. A., alumno de mi curso de ASP.NET MVC 5 en CampusMVP, me envió hace unos días una pregunta que he creído interesante comentar por aquí porque probablemente pueda ayudar a alguien más.

La cuestión que planteaba era relativa al mal funcionamiento de WebGrid cuando mostramos un campo de tipo enum y queremos ordenar por el mismo. Poco más o menos, me lo comentaba de la siguiente forma:
He incluido un campo enum en la clase del modelo y lo he incluido como columna en WebGrid.
[…]
Todo funciona bien menos el WebGrid, que no me ordena por esa columna. Al hacer click en ella ordena de forma correcta ascendentemente pero al hacer click otra vez no te ordena de forma descendente, mirando la url se ve que sortOrder no varía sigue siendo ASC. si lo cambio a mano a desc me lo ordena bien. ¿Es que el webgrid no ordena enumeraciones? ¿Cuál sería la solución, si quiero ordenar?
Obviamente, el hecho de que al cambiar a mano el parámetro sortOrder todo funcionara bien, dejaba de manifiesto que el problema no estaba en el controlador ni en el modelo, dejando la vista como única sospechosa de provocar ese problema.

Mis primeras respuestas iban orientadas a buscar un pequeño fallo en la forma de usar WebGrid o similar, porque me parecía algo bastante extraño, y, sobre todo, me parecía raro que no me hubiera pasado antes. José Antonio comentó, además, que había encontrado referencias por la red de que WebGrid tenía este problema, y que podía solucionarse si se forzaba una conversión de la enumeración a string a la hora de pasar los datos a la vista (!).

Y tras hacer varias pruebas, efectivamente, parece que algo hay. No sé si categorizarlo como bug del componente, o simplemente un comportamiento algo oscuro y poco previsible del mismo, pero en cualquier caso es interesante conocer este problema para poder atajarlo con éxito.

El motivo

Resulta que WebGrid guarda internamente una lista con los nombres de las columnas que serán gestionadas por este componente. Las columnas que no sean incluidas en esta lista no podrán ser utilizadas para realizar operaciones como la ordenación.

Los nombres de columna se establecen durante la llamada al método Bind(), de la siguiente forma:
WebGrid grid = new WebGrid(rowsPerPage: Model.PageSize);
grid.Bind(
    Model.People, 
    columnNames: new[] {"name", "lastname", "email"}, 
    autoSortAndPage: false, 
    rowCount: Model.PeopleCount
);
Observad que esto es independiente de las columnas que definamos más adelante para visualizar en el grid, durante la llamada a grid.GetHtml(). Estamos en un momento anterior, cuando "bindeamos" el grid a la colección de datos que mostrará.

Bien, el caso es que normalmente no lo hacemos así. Lo habitual es no suministrar el parámetro columnNames al método Bind(), asumiendo que este binding lo realizará el sistema de forma automática por nosotros, tomando por defecto como columnas todas las propiedades de los objetos que vamos a mostrar en la rejilla.

Y ahí es donde viene el problema. El siguiente método del código fuente de WebGrid, decide qué tipos de datos son aceptados en ese binding automático:
private static bool IsBindableType(Type type)
{
    Debug.Assert(type != null);

    Type underlyingType = Nullable.GetUnderlyingType(type);
    if (underlyingType != null)
    {
        type = underlyingType;
    }
    return (type.IsPrimitive ||
            type.Equals(typeof(string)) ||
            type.Equals(typeof(DateTime)) ||
            type.Equals(typeof(Decimal)) ||
            type.Equals(typeof(Guid)) ||
            type.Equals(typeof(DateTimeOffset)) ||
            type.Equals(typeof(TimeSpan)));
}
Este método es invocado durante la inicialización de WebGrid para cada propiedad presente en los objetos que van a ser mostrados en la rejilla. Las columnas que superen las condiciones serán introducidas en la lista de columnas válidas, y ésta será utilizada en distintos puntos para comprobar si existen los campos con los que pretendemos realizar operaciones, como la ordenación.

Las soluciones

Si os encontráis con este problema, existen varias soluciones; podéis usar la que más os guste, menos pereza os dé o la que mejor encaje en vuestro proyecto, pues el resultado será el mismo.

Como hemos visto anteriormente, los enum no son incluidos por defecto simplemente porque no están en la lista de tipos admitidos. Lo único que habría que hacer para que WebGrid soportase enums por defecto sería añadir la condición type.IsEnum al retorno de este método:
private static bool IsBindableType(Type type)
{
    ...
    return (type.IsPrimitive || 

            type.IsEnum ||                    // <-- Allow enums
            type.Equals(typeof(string)) ||
            type.Equals(typeof(DateTime)) ||
            type.Equals(typeof(Decimal)) ||
            type.Equals(typeof(Guid)) ||
            type.Equals(typeof(DateTimeOffset)) ||
            type.Equals(typeof(TimeSpan)));
}
Pero bueno, como no siempre tenemos el código fuente de WebGrid a mano y no siempre estamos dispuestos a introducir una copia personalizada de este componente en nuestro proyecto, así que lo más conveniente es utilizar otro tipo de soluciones.

Una posibilidad consistiría simplemente en eliminar el tipo enum de los objetos a mostrar en la rejilla. Por ejemplo, podemos enviarlos a la vista cambiando los tipos del Modelo, transformándolos o proyectando previamente a tipos de datos soportados, como string o int. Es sencillo de aplicar, aunque puede resultar intrusiva y también algo trabajosa.

Otra posibilidad se puede inferir directamente de los motivos que hemos comentado más arriba. Si el problema se debe a que no hemos especificado las columnas concretas en el método Bind(), hagámoslo y tendremos el problema solucionado. La única contraindicación destacable es que es algo farragosa, y puede ser propensa a fallos, pues debemos mantener esta lista de nombres de columna sincronizada con los nombres de las propiedades de los objetos (aunque quizás podría aliviarse algo usando el operador nameof de C#):
WebGrid grid = new WebGrid(rowsPerPage: Model.PageSize);
grid.Bind(
    Model.People, 
    columnNames: new[] {"name", "lastname", "email", "persontype"}, 
    autoSortAndPage: false, 
    rowCount: Model.PeopleCount
);
Por último, también podemos utilizar un truquillo rápido (¿o más bien debería decir un hack?) que parece que ha funcionado en las pruebas que he hecho: establecer manualmente la propiedad SortColumn del objeto WebGrid al campo de ordenación actual, justo después de hacer el Bind():
WebGrid grid = new WebGrid(rowsPerPage: Model.PageSize);
grid.Bind(Model.People, autoSortAndPage: false, rowCount: Model.PeopleCount);
grid.SortColumn = Request["sort"];
De esta forma forzamos a que WebGrid reconozca el campo de ordenación actual aunque éste no se encuentre en la lista de columnas bindeadas :)

¡Espero que os sea de ayuda!

Publicado en Variable Not Found.

2 Comentarios:

frann96 dijo...

Hola ! Antes que nada excelente tus aportes!
Te hago una consulta, que componente recomendas actualmente para la realización de grillas en mvc ? WebGrid, JqGrid con algun helper, etc ??

Muchas gracias!

José María Aguilar dijo...

Hola!

Muchas gracias por tu comentario :)

Para escenarios simples, donde Ajax no es un requisito, me gusta Webgrid por lo simple que es echarlo a andar. Cuando sí tengo que hacer un planteamiento Ajax, el grid de Kendo está muy bien, y DataTables también es correcto y fácil de usar. Ya hace tiempo que no uso jqGrid, pero supongo que seguirá siendo una opción válida. En mi caso, prefiero usar directamente JS en lugar de helpers.

Pero después de haber visto muchos, la conclusión a la que he llegado es que todos son más o menos lo mismo. Por tanto, casi da igual el que elijas, siempre que sepas utilizarlo bien :)

Un saludo!