Autor en Google+
Saltar al contenido

Variable not found. Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo de software, internet, u otros temas relacionados con la tecnología. C#, ASP.NET, ASP.NET MVC, HTML, Javascript, CSS, jQuery, Ajax, VB.NET, componentes, herramientas...

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo
de software, internet, u otros temas relacionados con la tecnología

¡Microsoft MVP!
martes, 1 de febrero de 2011
ASP.NET MVC Una de las preguntas que recibo con más frecuencia vía formulario de contacto en el blog, en sus comentarios, y en los cursos de MVC es cómo implementar grids, las habituales rejillas de datos que se suelen utilizar en interfaces de gestión de datos, con capacidades de paginación y ordenación.

Dado que hasta MVC 3 no había un soporte oficial para implementar esta funcionalidad desde la propia plataforma, nos veíamos obligados a buscarnos un poco la vida, y o bien usar el andamiaje generado por Visual Studio y desarrollar a mano las paginaciones y ordenaciones (¡uf!), o utilizar componentes externos (como el de MVCContrib, Telerik, jqGrid u otros).

La última versión de ASP.NET MVC, de la mano de la tecnología WebPages, nos trae un nuevo conjunto de helpers de productividad bastante interesantes en el espacio de nombres System.Web.Helpers, entre los cuales encontramos WebGrid, que por fin ofrece una solución “de serie” potente y flexible para ayudarnos a implementar esta funcionalidad de uso tan frecuente en nuestras aplicaciones.

Vamos a ver, paso a paso y de forma totalmente práctica, cómo utilizar WebGrid. Al final de post, además, encontraréis un enlace para descargar el proyecto completo para VS2010+SQL Express.

1. Lo primero: el Modelo

Estructura de la tabla en la base de datosLo primero que necesitamos antes de empezar a profundizar en el helper WebGrid es un Modelo, las entidades de datos de nuestra aplicación, así como los mecanismos que nos permitan hacer persistente y recuperar la información desde el sistema de almacenamiento que estemos utilizando.

En este caso, vamos a utilizar como almacén una base de datos SQL Express con una única tabla, en la que hemos creado una tabla para guardar datos de personas; esta colección es la que pretendemos mostrar en forma de rejilla de datos, por lo que necesitamos poblarla con algunos datos para probar más adelante.

Entity Data Model Para acceder a la base de datos vamos a utilizar Entity Framework, por lo que  necesitaremos también un Entity Data Model, un modelo conceptual de entidades, que podemos agregar a la carpeta Models del proyecto utilizando la opción “Agregar nuevo elemento” del menú contextual. En el asistente que aparece, sólo debemos indicarle que vamos a generar el modelo desde una base de datos existente, seleccionamos la tabla Personas, y listo.

Con esto tenemos ya la infraestructura básica de datos de nuestra aplicación. El siguiente paso será crear una clase de servicios, que será la que provea la lógica de negocio y acceso a datos para nuestro sistema.

Obviamente esto no tiene por qué ser siempre así, depende de nuestras necesidades y de la arquitectura del software, y en nuestro ejemplo van a ser ambos aspectos bastante simples.

El código inicial de nuestra clase de servicios es el siguiente:
public class ModelServices: IDisposable
{
    private readonly DatosEntities _datos = new DatosEntities();
 
    public IEnumerable<Persona> ObtenerPersonas()
    {
        return _datos.Personas.ToList();
    }
 
    public void Dispose()
    {
        _datos.Dispose();
    }
}
Como se puede observar en el código anterior, tenemos un único método, llamado ObtenerPersonas(), que retorna el conjunto completo de personas almacenadas en la base de datos.

Y de momento, he aquí todo el Modelo que necesitamos de momento.

2. El controlador

Nuestra clase del controlador, que llamaremos PersonasController, en este primer acercamiento va a ser realmente sencilla. Una única acción, Index(), que retornará una vista a la que suministraremos la colección de datos obtenida desde el Modelo:
public class PersonasController : Controller
{
    public ActionResult Index()
    {
        var datos = new ModelServices().ObtenerPersonas();
        return View(datos);
    }
}
Y esto es todo: una acción con dos líneas. Vale, podría haber sido una única línea, pero así el código me parecía más legible ;-)

3. La vista, toma primera: WebGrid entra en escena

Ahora es cuando vamos a empezar a notar las ventajas de utilizar WebGrid respecto a las opciones disponibles hasta MVC y que ya hemos citado al principio del post.

Observad el siguiente código, una vista Razor que recibe una enumeración de objetos Persona desde el controlador y genera un grid completo:
@model IEnumerable<WebGridDemo.Models.Persona>
@{
    ViewBag.Title = "Personas";
    WebGrid grid = new WebGrid(Model);
}
<h2>Personas</h2>
@grid.GetHtml()
Impresionante, ¿eh? Aunque pueda parecer increíble, el código anterior es todo lo que necesitamos para montar un grid funcionalmente completo, con paginación y ordenación por columnas: ¡dos líneas! En la primera de ellas instanciamos el WebGrid suministrándole la colección de datos sobre la que debe iterar, y en la segunda (ya al final) generamos el marcado HTML que enviaremos al cliente.

El resultado en ejecución lo podemos ver en la siguiente captura de pantalla:

WebGrid en ejecución


Aunque todavía queda algo lejos de la perfección, el resultado es espectacular.

El helper, en su comportamiento por defecto, muestra una columna por cada propiedad que encuentra en la clase sobre la que itera, eso sí, ordenándolas alfabéticamente. Además, ha utilizado el nombre de las propiedades como encabezado de columna, y las muestra como enlaces para forzar la ordenación por cada una de ellas, e incluso ha introducido en el pie un sistema completo de navegación por las páginas de datos.

Y lo mejor que todo esto funciona directamente, sin necesidad de añadir una línea de código más :-)

Sin embargo, como siempre, estos automatismos tienen su precio. Por un lado, no estamos controlando las columnas a mostrar, ni el formato en que sus valores son presentados (observad, por ejemplo, la fecha de nacimiento), ni sus encabezados… Normalmente, necesitaremos esforzarnos algo más (aunque no demasiado) para dejarlo todo perfecto.

Además, existe un serio problema de rendimiento cuando el número de elementos del grid sea importante: tanto la ordenación como la paginación se realizan en memoria con el número total de elementos. Exagerando, si tenemos un millón de filas en la base de datos, se materializarán en memoria un millón de objetos, serán ordenados según el criterio actual y, finalmente, sólo serán mostrados al cliente los diez objetos que contiene una página de datos. Veremos más adelante que hay fórmulas para gestionar de forma eficiente estos escenarios.

4. La vista, toma segunda: las columnas que yo quiera, por favor

Existen distintas fórmulas para especificar las columnas a mostrar en un WebGrid. La primera de ellas, es mediante exclusión sobre el conjunto total de propiedades. Entre muchos otros aspectos, en el método GetHtml() podemos especificar un array de nombres de propiedad que no deben mostrarse como columnas. Por ejemplo, sobre el ejemplo anterior, si no nos interesa mostrar la propiedad IdPersona, podríamos haber sustituido la última línea de la vista por:
@grid.GetHtml(exclusions: new[] {"IdPersona"})
Sin embargo, el enfoque anterior no es demasiado útil, puesto que normalmente querremos indicar el orden de aparición de las columnas, especificar sus encabezados, determinar si las columnas pueden ser utilizadas como criterios de ordenación, etc. Toda esta información se define en objetos WebGridColumn.

Aunque hay otras formas de hacerlo, habitualmente encontraremos en el parámetro columns de la llamada a GetHtml() un array con el detalle de las columnas del grid, como en el siguiente ejemplo:
@grid.GetHtml(columns: new [] {
    grid.Column("Nombre"),
    grid.Column("Apellidos"),
    grid.Column("EMail"),
    grid.Column("FechaNacimiento"),
    grid.Column("NumeroDeHijos"),
})
Como se puede observar, estamos pasando en el parámetro columns un array en el que cada elemento lo estamos generando mediante una llamada al método Column() de WebGrid, en cuyo primer parámetro indicamos el nombre de la propiedad a la que corresponde la columna.

El resultado de la ejecución del código utilizando este último código sería:

Web grid en ejecución, con mis columnas

Algo ha mejorado la cosa, aunque todavía tenemos que afinar algo más en cuanto a la presentación.

5. La vista, toma tercera: las columnas como yo quiero, por favor

Todavía nos quedan varios detalles por apuntalar para que el grid, al menos a efectos visuales, cumpla un mínimo razonable. Para personalizar cada columna, podemos utilizar los parámetros del método generador de columnas Column() que ya hemos visto anteriormente:
  • header, que permite indicar el texto mostrado en el encabezado,
  • canSort, que indica si la columna puede ser utilizada como criterio de ordenación,
  • format, que permite indicar un formato personalizado para el contenido de la columna,
  • style, que indica la clase CSS que se aplicará a todas las celdas de la columna.
De todas ellas, sólo merece una especial mención la propiedad format. En ella podemos indicar, bien mediante un bloque de marcado Razor, bien mediante una función lambda, cómo debe formatearse el contenido de la propiedad vinculada a la columna.

En el primer caso, debemos comenzar el bloque de marcado con el carácter de escape de Razor (@) y seguirlo del código que queremos enviar al cliente. Desde su interior podemos hacer referencia al objeto que está siendo evaluado utilizando @item, como en el siguiente ejemplo, donde se muestra cómo formatear la columna EMail para que sea mostrada como un hiperenlace de tipo mailto:
    grid.Column("EMail", 
                 format: @<a href="mailto:@item.Email">@item.Email</a>
    )

Para profundizar en la "magia" que hace posible la utilización de bloques de marcado donde debería haber código, no os perdáis este magnífico post de Eduard Tomás sobre Razor Templates.

También podemos utilizar una función lambda, que recibe como parámetro el objeto actual y retorna una cadena (o un IHtmlString si no debe ser codificada). Por ejemplo, a continuación vemos cómo utilizar esta posibilidad para dar formato a la columna FechaNacimiento:
    grid.Column("FechaNacimiento", 
                format: p=>p.FechaNacimiento.ToShortDateString()
    )

Por tanto, teniendo en cuenta todo lo anterior, podemos tunear un poco el grid utilizando el siguiente código. Como recordatorio, mostraré de nuevo el código completo de la vista, para que podáis observar en su conjunto cómo va quedando:
@model IEnumerable<WebGridDemo.Models.Persona>
@{
    ViewBag.Title = "Personas";
    WebGrid grid = new WebGrid(Model);
}
<h2>Personas</h2>
@grid.GetHtml(columns: new [] {
    grid.Column("Nombre", canSort: false),
    grid.Column("Apellidos"),
    grid.Column("EMail", 
                 format: @<a href="mailto:@item.Email">@item.Email</a>
    ),
    grid.Column("FechaNacimiento", 
                header: "Fecha de nacimiento",
                format: p=>p.FechaNacimiento.ToShortDateString()
    ),
    grid.Column("NumeroDeHijos", 
                header: "Número de hijos",
                style: "a-la-derecha"
    )
})

En ejecución ya sí que podemos ver algo más terminado:

WebGrid, con las columnas con formato

6. La vista, toma cuarta: ¿y no puedo añadir columnas personalizadas?

¡Pues claro!

De hecho, basta con añadir columnas exactamente igual que las anteriores, excepto en que no las vincularemos a ninguna propiedad de la clase del Modelo. Esto, combinado con la flexibilidad del formateo personalizado (parámetro format), nos ofrece ya todo lo que necesitamos para crear columnas totalmente a nuestro antojo.

El siguiente código muestra cómo añadir una columna adicional con enlaces hacia las acciones que permitirían, por ejemplo, editar o eliminar una Persona:
@grid.GetHtml(columns: new [] {

    ... // Resto de columnas del grid, vistas anteriormente
    grid.Column(
          "", 
          header: "Acciones",
          format: @<text>
                    @Html.ActionLink("Editar",   "Edit",   new { id=item.IdPersona} )
                    |
                    @Html.ActionLink("Eliminar", "Delete", new { id=item.IdPersona} )
                  </text>
    )
})
Observad que esta vez, para incrementar la legibilidad del código, estamos utilizando el tag especial <text> de Razor, que nos permite crear bloques de marcado de varias líneas.

Y el resultado, como el siguiente:

WebGrid, con columnas personalizadas

7. La vista, toma quinta: mejor casi que lo quiero todo a mi gusto

El helper WebGrid ofrece multitud de opciones de personalización adicionales que podemos establecer tanto al llamar a sus distintos métodos como de forma directa. Por ejemplo, GetHtml() permite indicar los siguientes parámetros, además de los ya vistos:
  • headerStyle, footerStyle, rowStyle, alternatingRowStyle, y selectedRowStyle permite indicar las clases CSS a aplicar a las filas de encabezado, pie, filas de datos alternativas, y fila seleccionada, respectivamente.
  • caption, para especificar un título para la tabla, que será incluido en una etiqueta <caption> en el encabezado.
  • fillEmptyRows, establecido a true hace que cada página tenga siempre el mismo número de filas, creando filas en blanco si fuera necesario.
  • emptyRowCellValue indica el valor a mostrar en las celdas de filas vacías.
  • mode, permite especificar el tipo de paginador a generar, eligiéndolo mediante una combinación de elementos de la enumeración WebGridPagerModes:
    • WebGridPagerModes.Numeric: el paginador mostrará enlaces directos a páginas cercanas a la actual.
    • WebGridPagerModes.FirsLast: se mostrarán enlaces para ir a la primera y última página de datos.
    • WebGridPagerModes.NextPrevious: aparecerán enlaces para desplazarse a la página anterior y siguiente.
    • WebGridPagerModes.All: todos los anteriores al mismo tiempo.
  • numericLinksCount: indica el número de páginas que aparecerán, siempre que mode contenga el valor WebGridPagerModes.Numeric.
  • firstText, previousText, nextText, lastText, permite sustituir los textos que aparecen por defecto en los enlaces de ir a la primera, anterior, siguiente y última página respectivamente. Inicialmente son los habituales “<<”, “<”, “>”, y “>>”.
Por ejemplo, observad el siguiente código, y su resultado en ejecución una vez hemos creado un par de reglas en la hoja de estilos del sitio web:
...
<h2>Personas</h2>
@grid.GetHtml(
    fillEmptyRows: true,
    alternatingRowStyle: "fila-alternativa",
    headerStyle: "encabezado-grid",
    footerStyle: "pie-grid",
    mode: WebGridPagerModes.All,
    firstText: "<< Primera",
    previousText: "< Anterior",
    nextText: "Siguiente >",
    lastText: "Última >>",
    columns: new [] {
     ... // Definición de columnas vista anteriormente
})


WebGrid, con algo de estilo

Asimismo, el propio constructor de WebGrid permite modificar también numerosos aspectos funcionales del grid mediante los siguientes parámetros:
  • defaultSort, que indica la columna que actuará como ordenación por defecto mientras no se especifique otra.
  • rowsPerPage (por defecto, 10), define el número de filas que aparecerán en cada página.
  • canPage, canSort, indican respectivamente si el grid va a permitir paginación y ordenación. Por defecto, true en ambos casos.
    fieldNamePrefix, que permite indicar el prefijo que será utilizado en los parámetros del query string utilizados por el grid. Esto, por ejemplo, permitiría mostrar varios grids simultáneamente sobre la misma página, sin interferir en su funcionamiento.
  • selectionFieldName, sortFieldName, sortDirectionFieldName permiten indicar el nombre de los parámetros usados para mantener el estado de, respectivamente, la fila seleccionada, el campo de ordenación y el sentido de la ordenación.

WebGrid permite incluso el funcionamiento en modo Ajax, es decir, es capaz de mostrar las distintas páginas de datos sin necesidad de recargar la página completa. En este caso, podemos utilizar los parámetros ajaxUpdateContainerId y ajaxUpdateCallback, que permiten indicar respectivamente el elemento de la página donde serán mostrados los datos y una función callback que será llamada tras actualizar el elemento. Esto, si no se me olvida, lo veremos en otro post más adelante.

Y recapitulando…

A lo largo de este artículo hemos ido profundizando en el uso de WebGrid de forma progresiva, partiendo de un ejemplo realmente simple hasta abordar escenarios de personalización más avanzados. Espero que sirva para mostrar el uso y principales características de este útil y potente helper, que con toda seguridad nos ahorrará bastante trabajo en los proyectos MVC 3.

Sin embargo, ya he comentado anteriormente que no es oro todo lo que reluce… la paginación implementada por defecto por WebGrid es bastante ineficiente, puesto que necesita tener en memoria el conjunto de datos completo para ordenarlo y extraer únicamente la página solicitada. No es nada complicado implementar de forma correcta esta paginación, pero dado que este post ya ha quedado lo suficientemente extenso, dejaremos su explicación para un artículo posterior.


Descargar proyectoAh, podéis descargar el proyecto de demostración desde mi Skydrive (requiere VS2010, MVC 3 y SQL Express).



Publicado en: Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

20 Comentarios:

Victor dijo...

Muy bueno, muy completo.

Espero que el post de paginación sea pronto!


Saludos.

José M. Aguilar dijo...

Hola, Victor!

Ante todo, gracias por comentar :-)

En cuanto al siguiente post, en breve lo tendremos por aquí.

Un saludo.

sebaslarrea dijo...

Excelente solo que hay un detalle. Si tengo un millon de registros y quiero buscar (o que solo liste) los apellidos que comiencen con "PAEZ", existe forma de hacerlo?

José M. Aguilar dijo...

Hola, Sebas.

Bueno, si queremos filtrar el resultado, el ideal es enviarle al Modelo los criterios (que los apellidos comiencen por XX) a la hora de obtener los datos, y hacer que WebGrid opere sólo con los datos obtenidos (parecido a lo que se hace en la segunda parte de este post para paginar).

Otra cosa es cómo mantener el valor del filtro entre peticiones (imaginando que este sea variable) si, por ejemplo, el usuario cambia de página de datos o cambia la columna de ordenación. Para esto, tendrías que utilizar algún mecanismo (por ejemplo rutas, cookies, o ingeniártelas en cliente con jquery para modificar los enlaces creados por webgrid).

En resumen: algo más complicado, pero poderse se podría.

Saludos & gracias por comentar!

Naúfrago del Asfalto dijo...

Muy buen post, como siempre.
Gracias.

Juan Fletes dijo...

Gracias estoy empezando en MVC3 y esa era una gran interrogante que tenia.

Excelente aporte :D

tony dijo...

excelente post gracias por tus conocimientos bastante util para agergar en los proyectos de desarrollo

ALEJANDRO dijo...

Saludos Amigo gracias por el aporte mi pregunta seria si podemos modificar los datos dentro del WebGrid saludos.

ALEJANDRO dijo...

y ah me puedes pasar tu correo seria bacan. para cualquier consulta ya q recien estoy entrado a este nuevo mundo!

testblog12345 dijo...

Es muy util este componente pero nos ha traido bastante dolores de cabeza... Paso a explicar el problema:

Tenemos configurado WebGrid para que funcione via ajax. Esto anda de maravillas.

El problema es cuando en esa misma pagina efectuamos alguna otra invocacion ajax, como por ejemplo borrar un componente de la lista. Resulta ser que luego la lista utiliza la ultima url para hacer la invocacion a ajax a sus metodos de Ordenacion/Paginacion...Esto por supuesto rompe nuestra logica...Porque por ejemplo esta serir de pasos falla:
1) Renderizamos el grid.En /ListItems
2) Hacemos Click en Delete. Lo cual por ajax llama al metodo /Delete del controlador
3) Se vuelve a renderizar el grid(todo por ajax)
4) Hacemos click en el header de una columna para ordenar la grilla
5) Error! El request la grilla lo esta haciendo a /Delete?[OrderByParameter]... en vez de a /ListItems?[OrderByParameters]


Les ha sucedido esto alguna vez? Como lo han solucionado?

José M. Aguilar dijo...

Hola, @testblog1235

Pues la verdad es que no me ha ocurrido nunca. De hecho, tampco suelo usar mucho el modo Ajax de WebGrid, no me convence demasiado.

En cualquier caso, efectivamente, WebGrid se apoya mucho en el querystring para mantener el estado entre llamadas y hay que tener algo de cuidad con eso, pero nunca se me ha dado el problema que comentas.

¿Podrías enviarme algún proyecto de prueba con un ejemplo de este problema que pudiera reproducir para echarle un vistazo? ¿O colgarlo en algún sitio para que lo descargue?

Un saludo & gracias.

Ezequiel Scrivano dijo...

Excelente post!! me ha sido de gran utilidad.. Gracias!!

Anónimo dijo...

Pensar con PHP utilizando el flamante framework Yii esto ya te lo hace automaticamente dejandote una vista impecable. Acá hay que hacerlo todo a manopla

Diego P. dijo...

Mas alla de lo que webgrid sea. El analisis y el post es impecable.
Muy bueno gracias

Anónimo dijo...

Muy bueno y comprensible tu Post... me ayudo un monton, Muchas gracias por compartir tus conocimientos!!!

juan dijo...

Gracias me sirvio un montón

mike`s dijo...

Buenas tardes!!

Excelente post.

Para darle un poco mas de diseño necesito alinear el dato de una columna. Que utilizaría para centrar el dato de una columna?

José M. Aguilar dijo...

Hola!

Puedes usar el parámetro "style" de las columnas para asignarle una clase CSS, y desde esta clase indicar que el contenido debe alinearse.

Saludos!

Johan Salazar dijo...

Muy buen post, tengo una duda, en caso de que quisiera enviar los datos seleccionados en el grid a un controlador o a otra vista es posible haciendolo desde la columna edit que creó?

José M. Aguilar dijo...

Hola!

Sí, claro que puedes enviar datos a acciones (no a vistas). Por ejemplo en este post generamos un link ("edit") hacia una acción del controlador, pasándole un parámetro "id" con el identificador de la fila mostrada.

Pero podrías enviar más datos añadiendo las propiedades que te interesen al objeto anónimo:

@Html.ActionLink("Editar", "Edit", new { id=item.IdPersona, name=item.Nombre } )


Saludos.