ASP.NET MVC: WebGrid con filtro, paginación y ordenación

ASP.NET MVCHace unos días, el amigo Cadavid realizaba una consulta en los comentarios de la serie de posts que escribí hace unos meses sobre el helper Webgrid, sobre cómo podíamos implementar filtros personalizados sobre los datos mostrados, y dado que no es algo que se pueda explicar en un simple comentario, me comprometí a escribir un post sobre el tema, así que ahí va.

El problema fundamental es que WebGrid no incorpora ningún tipo de ayuda para realizar esta tarea tan frecuente; simplemente se encarga de mostrar los datos que le suministremos, por lo que si queremos filtrarlos debemos hacerlo de forma manual. En pura teoría bastaría con hacer llegar a Controlador los filtros a aplicar, y que éste consultara la información desde el Modelo teniendo en cuenta los criterios establecidos.

Sin embargo, algo que puede parecer relativamente simple se complica un poco si queremos, además, mantener intacta la capacidad de ordenación y paginación de datos, puesto que debemos encargarnos también del mantenimiento del estado entre peticiones. Pero vaya, no es nada que no podamos solucionar en unos minutos 😉


Partiendo del proyecto que desarrollamos en el último post de la serie vamos a ver cómo podemos añadir al grid un par de criterios de búsqueda, de forma que el resultado luzca tal que así:

Criterios de filtro con Webgrid
El campo Texto nos permitirá buscar subcadenas en el nombre y apellidos de las personas almacenadas en la base de datos, y los otros dos campos permitirán filtrar por rango (mín-máx) los hijos que tienen. Como es habitual, estas condiciones las combinaremos con un Y lógico.

1. La vista

En primer lugar introducimos en la vista, justo antes de generar el grid, el formulario que utilizaremos para solicitar al usuario los criterios de búsqueda:

Cuadro de criterios de búsqueda
El código es el siguiente:

@using(Html.BeginForm(null, null, FormMethod.Get))
{
    <fieldset>
        <legend>Criterios de búsqueda</legend>
        @Html.Label("buscar", "Texto:")
        @Html.TextBox("buscar") 
        @Html.Label("hijosMin", "Hijos mín:")
        @Html.TextBox("minHijos") 
        @Html.Label("maxHijos", "Hijos máx:")
        @Html.TextBox("maxHijos" )
 
        <input type="submit" value="Buscar" />
    </fieldset>
}
Observad la simplicidad del formulario. Por no usar, no usa ni siquiera la sintaxis lambda en los helpers de edición. Los controles vamos generarlos partiendo de campos cuyos valores existirán en el query string, y no desde el Modelo, que es lo habitual, y por esta razón fijaos que el formulario está configurado para ser enviado utilizando el método HTTP GET.

De esta forma podemos propagar muy fácilmente el valor de los controles (textbox) entre las llamadas:

  • si el usuario introduce criterios y pulsa el botón enviar, la URL a la que se realizará la petición será, por ejemplo, /personas/index?buscar=aguilar&hijosMin=0&hijosMax=8.
  • si el usuario utiliza los botones de navegación propios del grid (avance/retroceso de página, salto a página directa, o reordenación), estos parámetros serán añadidos a los anteriores, de forma que seguirán manteniendo sus valores entre las distintas llamadas. Esto es así gracias a que Webgrid genera los enlaces de estas acciones respetando los parámetros actuales del query string; es decir, si estamos filtrando y saltamos a la página 4, accederemos a una dirección que incluirá tanto la información de los criterios de búsqueda como la de paginación, algo como: /personas/index?buscar=aguilar&minHijos=0&maxHijos=8&page=4.
Y con esto, dejamos completada la capa vista.

2. El Controlador

Si recordáis, el método de acción encargado de obtener los datos de grid y enviar la vista con los datos al usuario recibía tres parámetros: la página actual, el campo de ordenación y el sentido de ésta (ascendente/descendente).

Dado que ahora necesitará también obtener los criterios de ordenación, debemos ampliar su definición añadiendo parámetros para estos valores:

public ActionResult Index(int page = 1, string sort = "Apellidos", string sortDir = "ASC", 
                          string buscar = null, int? minHijos = null, int? maxHijos = null)
Observad que son todos parámetros opcionales, y los establecemos a nulo para detectar fácilmente cuándo nos vienen rellenos.

¿Y en qué puntos necesitamos utilizar estos nuevos parámetros? Pues sólo en dos:

  • en la llamada que hacemos al Modelo para contar el total de filas del grid, a la que tendremos que informar sobre los criterios de filtrado para que el conteo se realice correctamente.
  • en la llamada que hacemos al Modelo para obtener las filas a mostrar en la página actual, donde obviamente también deberemos tener en cuenta los filtros.
La acción quedaría más o menos así:

public ActionResult Index(int page = 1, string sort = "Apellidos", string sortDir = "ASC",
                          string buscar = null, int? minHijos = null, int? maxHijos = null)
{
    var numPersonas = _services.ContarPersonas(buscar, minHijos, maxHijos);
    var personas = _services.ObtenerPaginaDePersonasFiltrada(page, PERSONAS_POR_PAGINA, 
                                   sort, sortDir, buscar, minHijos, maxHijos);
 
    var datos = new PaginaDePersonasViewModel()
                    {
                        NumeroDePersonas = numPersonas,
                        PersonasPorPagina = PERSONAS_POR_PAGINA,
                        Personas = personas
                    };
 
    return View(datos);
}
Y esto es todo en el controlador.

3. El Modelo

Y por último, ya en el Modelo, debemos hacer que los métodos utilizados desde el controlador (ContarPersonas y ObtenerPaginaDePersonasFiltrada) tengan en cuenta los parámetros en los que indicamos las condiciones de búsqueda.

En el primero de ellos, simplemente retornamos el número de personas que cumplan los criterios que nos llegan como parámetros:

public int ContarPersonas(string textoBuscar = null, int? minHijos = null, int? 
                          maxHijos = null)
{
    IQueryable<Persona> query = _datos.Personas;
    query = queryPersonasFiltrada(textoBuscar, minHijos, maxHijos, query);
    return query.Count();
}
El método de utilidad queryPersonasFiltrada() que estamos utilizando únicamente se encarga de añadir a la query las cláusulas where que necesitamos para tener en cuenta las condiciones especificadas:

private static IQueryable<Persona> queryPersonasFiltrada(string textoBuscar, int? minHijos, 
                                         int? maxHijos, IQueryable<Persona> query)
{
    if (!string.IsNullOrWhiteSpace(textoBuscar))
        query = query.Where(p => p.Nombre.Contains(textoBuscar) || p.Apellidos.Contains(textoBuscar));
    if (maxHijos != null)
        query = query.Where(p => p.NumeroDeHijos <= maxHijos);
    if (minHijos != null)
        query = query.Where(p => p.NumeroDeHijos >= minHijos);
    return query;
}
Por último, implementamos el método que obtiene los datos a mostrar en la página actual:

public IEnumerable<Persona> ObtenerPaginaDePersonasFiltrada(int paginaActual, int personasPorPagina, 
              string columnaOrdenacion,  string sentidoOrdenacion, 
              string textoBuscar, int? minHijos, int? maxHijos)
{
    // Comprobamos los datos de entrada
    sentidoOrdenacion = sentidoOrdenacion.Equals("desc", StringComparison.CurrentCultureIgnoreCase) ? 
                        sentidoOrdenacion : "asc";
 
    var validColumns = new[] { "apellidos", "fechanacimiento", "email", "numerodehijos" };
    if (!validColumns.Contains(columnaOrdenacion.ToLower()))
        columnaOrdenacion = "apellidos";
 
    if (paginaActual < 1) paginaActual = 1;
    if (personasPorPagina < 1) personasPorPagina = 10;
 
    // Generamos la consulta
    var query = (IQueryable<Persona>) _datos.Personas
                     .OrderBy("it." + columnaOrdenacion + " " + sentidoOrdenacion);
 
    query = queryPersonasFiltrada(textoBuscar, minHijos, maxHijos, query);
 
    return query
            .Skip((paginaActual - 1) * personasPorPagina)
            .Take(personasPorPagina)
            .ToList();
}

Hay poco que comentar sobre este código. En primer lugar se realiza una comprobación básica de los parámetros de entrada, para a continuación generar la consulta que se realizará sobre la base de datos. Como podéis observar, también se utiliza el método queryPersonasFiltrada() para aplicar los criterios de consulta.

En resumen…

Como hemos podido ver, la implementación de criterios de búsqueda en un WebGrid no difiere mucho de la que habíamos descrito en su momento. Sólo hay que tener en cuenta los siguientes puntos:
  • primero, incluir en la Vista un formulario donde se recojan los criterios de la consulta para enviarlos al controlador.
  • segundo, preparar el Controlador para que reciba estos criterios y los haga llegar al Modelo.
  • tercero, en el Modelo, simplemente aplicar esos criterios en el momento de contar las filas totales, y en la obtención de los datos a mostrar en la página del grid.
En SkyDrive he colgado un proyecto que incluye un ejemplo completo donde podéis ver todo esto en funcionamiento: WebGridFiltradoDemo (MVC 3 + SQL Express).

Publicación original (5-oct-2011): http://www.variablenotfound.com/2011/10/aspnet-mvc-webgrid-con-filtro.html

Este artículo y muchos más en: Variable not found.

Deja un comentario

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