viernes, 6 de marzo de 2009

Paginación y Ordenamiento del lado servidor con GridView y ADO.NET Entity Framework

Esta vez quiero mostrarles como actualmente estoy implementando paginación y ordenamiento del lado servidor, usando GridView, ObjectDataSource y ADO.NET EF. Aunque de una forma muy similar también es aplicable a LINQ2SQL.

 

Creando los métodos en la capa de negocio o servicios

El primer paso consiste en crear en nuestra capa de negocios o servicios un par de métodos que luego serán invocados por el control ObjectDataSource.

Básicamente necesito 2 métodos para lograr mi objetivo:

  1. Un método que me devuelva la cantidad total de registros
  2. Y otro método que me devuelva los registros paginados

Para el ejemplo voy a crear en una clase CatalogService, un método FindProducts() y otro método FindProductsCount().

public IList<Product> FindProducts(int? code, string description, int? categoryID, string sortExpression, int startRows, int maxRows)
       {
           using (var db = new StoreEntities())
           {
               var query = from p in db.Products select p;

               query = FindProductsFilter(query, code, description, categoryID);
               query = FindProductsSort(query, sortExpression);

               return query.Skip(startRows).Take(maxRows).ToList();
           }
       }

Aquí vemos que FindProducts() llamá a 2 métodos FindProductsFilter() y FindProductsSort() que se encargan de aplicar los filtros necesarios y ordenar los registros, y luego usa los métodos Skip y Take para paginar los resultados.

private static IQueryable<Product> FindProductsFilter(IQueryable<Product> query, int? code, string description, int? categoryID)
{
    if (code.HasValue)
        query = query.Where(p => p.Code== code);

    if (!string.IsNullOrEmpty(description))
        query = query.Where(p => p.Description== description);

    if (categoryID.HasValue)
        query = query.Where(p => p.Category.ID== categoryID);

    return query;
}

private IQueryable<Product> FindProductsSort(IQueryable<Product> query, string sortExpression)
{
    if (string.IsNullOrEmpty(sortExpression))
        return query.OrderBy(p => p.Description);

    return query.OrderBy(sortExpression);
}

Es importante saber que el método OrderBy() no acepta un parámetro string para realizar el ordenamiento. Sin embargo, aquí estoy usando unas extensiones de LINQ conocidas como Dynamic Query para lograr este objetivo.

Pueden leer un artículo de Scot Guthrie traducido al español sobre Dynamic Query y descargar el código fuente.

Sin Dynamic Query, tendría que parsear el parámetro sortExpression y escribir un switch o multiples ifs para aplicar el OrderBy correspondiente según el campo. En pocas palabras, tendría que escribir muuucho más código, entonces prefiero Dynamic Query.

Y ahora el método FindProductsCount() que no hace otra cosa más que contabilizar el total de productos. Este método lo utiliza el ObjectDataSource y la grilla para poder calcular el total de páginas necesarias, sin tener que traer todos los registros para luego paginarlos.

public int FindProductsCount(int? code, string description, int? categoryID)
{
    using (var db = new StoreEntities())
    {
        var query = from p in db.Products select p;

        query = FindProductsFilter(query, code, description, categoryID);

        return query.Count();
    }
}

 

Consumiendo los métodos desde la UI

Ahora creo una página aspx con unos campos para filtrar, un botón y una grilla asociada a un ObjectDataSource para los resultados. El formulario se vería mas o menos así:

image

No deseo explicar cómo enlazar la grilla con un ObjectDataSource, solo bastaría con decir que debemos asociar al método FindProducts() creado anteriormente y asociar los parámetros del método con los controles de la página. Y luego hay que habilitar Paging y Sorting en la grilla.

Las propiedades establecidas para el ObjectDataSource aparecen en negrita:

image 

Las propiedades sortExpression, startRows y maxRows serán pobladas y pasadas al método FindProducts y/o FindProductsCount automáticamente.

La última pieza es escribir código para el botón Buscar:

protected void FindButton_Click(object sender, EventArgs e)
{
    if (!Page.IsValid) return;

    try
    {

    ProductsGridView.DataSourceID = ProductsDataSource.ID;
    ProductsGridView.DataBind();
}
catch (Exception ex)
{
    ShowError(ex.Message);
}

}

Debe observarse que solo en runtime estoy enlazando la grilla con el control ObjectDataSource. Es decir, después de configurar la grilla y el ObjectDataSource, en tiempo de diseño, quito la referencia al ObjectDataSource en la propiedad DataSourceID de la grilla. De esta forma, cuando la página se carga no se hace una búsqueda automática, sino solo cuando se completan los filtros de búsqueda y se presiona el botón Buscar.

Pero esto es una particularidad que quise implementar y no tiene ningún efecto sobre la paginación y ordenamiento, motivo de este post.

Bueno creo que ya está todo, ahora hay que probar.

image

A mí me funcionó de maravillas ;-).

Mirando con el SQL Profiler veo que solo se consultan 10 registros por página. Las consultas al SQL Server devuelven solo los registros que el usuario quiere ver. Esto permite que nuestras aplicaciones sean mucho más escalables, pues solo traemos los registros necesarios y se minimiza la carga de trabajo en la BD.

Bueno espero que sirva.
Comentarios son bienvenidos!

Saludos, Gus

8 comentarios:

Unknown dijo...

He intentado implementar la solución que propones, y la realidad es que no me trae ningún dato. Casi seguro porque no he sabido configurar correctamente el objectDataSource. Sería interesante que comentases que valores le asignas a los parámetros startRows y maxRows cuando eliges el método al configurar el oDS. Muchas gracias por el esfuerzo de escribir el artículo de todas formas, e intentaré hacerlo funcionar por mi cuenta. Un saludo Gustavo.

Unknown dijo...

Vale, simplemente quitando esos parámetros cuando el asistente nos pide que le demos un valor por defecto funciona sin problemas, aunque como mi consulta es distribuida y con un access de por medio el desempeño sigue siendo malísimo, así que me parece que necesitaré soluciones alternativas. Muchas gracias por el post de nuevo.

.NET en latino dijo...

Victor, Gracias por comentar!. Entiendo que si hay un Access en el fondo la paginación no es realmente a nivel de datos. Aunque Access puede hacer un papel digno en muchos escenarios. Saludos, Gus

Mariano R. Noguera dijo...
Este comentario ha sido eliminado por el autor.
Mariano R. Noguera dijo...

Excelente Gus! Lo que estaba buscando y muy clara tu explicación.

Muchas Gracias!

Mariano

GIANCARLO dijo...

Hola Gustabo, se puede implementar tu solucion sin el uso de Linq y con solo usar una clase con stored procedure osea solo con puro codigo.
Saludos

.NET en latino dijo...

Hola Giancarlo. Si, es posible. Es un poco más duro paginar registros mediante SPROCS, pero nada del otor mundo. Incluyo un ejemplo para SQL Serevr 2005 (y superior):

CREATE PROCEDURE dbo.GetLogs
@PageIndex INT,
@PageSize INT
AS

BEGIN

WITH LogEntries AS
(SELECT ROW_NUMBER() OVER (ORDER BY Date DESC)
AS Row, Date, Description
FROM LOG)

SELECT Date, Description
FROM LogEntries
WHERE Row between
(@PageIndex - 1) * @PageSize + 1 and @PageIndex*@PageSize

END

Anónimo dijo...

Muy buen tutotial, pero te faltó mencionar el agregar los parámetros al ObjectDataSource (SelectParameters).