Compartir a través de


Puntos de datos

Paginación por parte del servidor con Entity Framework y ASP.NET MVC 3

Julie Lerman

Descargar el ejemplo de código

image: Julie LermanEn mi columna Puntos de datos de febrero, mostré el complemento DataTables de jQuery y sus capacidades para controlar sin problemas enormes cantidades de datos en el lado del cliente. Esto funciona bien con las aplicaciones web, donde quiere segmentar grandes cantidades de datos. Este mes, me concentraré en el uso de consultas que recuperan menores cargas para permitir diferentes tipos de interacción con los datos. Esto es de especial importancia cuando tiene como objetivo aplicaciones móviles.

Le sacaré provecho a las características presentadas en ASP.NET MVC 3 y demostraré la forma de usar dichas características junto con la paginación eficiente por parte del servidor en Entity Framework. Existen dos desafíos con esta tarea. La primera es proporcionar una consulta de Entity Framework con los parámetros de paginación correctos. La segunda es imitar una característica de la paginación por parte del cliente al proporcionar pistas visuales para indicar que existen más datos que se deben recuperar, así como también, vínculos para desencadenar la recuperación.

ASP.NET MVC 3 tiene muchas nuevas características, como el nuevo motor de vista Razor, mejoras de validación y muchísimas más características de JavaScript. La página de inicio de MVC está en asp.net/mvc, donde puede descargar ASP.NET MVC 3 y encontrar vínculos a publicaciones de blog y vídeos de entrenamiento para ayudarle a familiarizarse rápidamente. Una de las nuevas características que usaré es ViewBag. Si anteriormente usó ASP.NET MVC, ViewBag es una mejora a la clase ViewData y le permite usar propiedades creadas de manera dinámica.

Otro elemento nuevo que ASP.NET MVC 3 presenta es el System.Web.Helpers.WebGrid especializado. Aunque una de las características de la cuadrícula es la paginación, usaré la nueva cuadrícula pero no su paginación en este ejemplo, ya que la paginación la realiza el cliente, en otras palabras, revisa un conjunto de datos que se le proporciona, de manera similar al complemento DataTables. En su lugar, usaré la paginación por parte del servidor.

Para esta pequeña aplicación, necesitará trabajar en un Entity Data Model. Estoy usando uno creado desde la base de datos de muestra de Microsoft AdventureWorksLT, pero sólo necesito traer Customer y SalesOrderHeaders al modelo. Moví las propiedades Customer rowguid, PasswordHash y PasswordSalt en una entidad separada, de modo que no me tengo que preocupar por ellos al editar. Aparte de este pequeño cambio, no modifique el modelo desde su estado predeterminado.

Creé un proyecto usando la plantilla de proyecto predeterminada ASP.NET MVC 3. Esto rellena previamente un número de controladores y vistas, y dejaré que el HomeController predeterminado presente los clientes.

Usaré una sola clase DataAccess para proporcionar una interacción con el modelo, el contexto y posteriormente, la base de datos. Es esta clase, mi método GetPagedCustomers proporciona la paginación por parte del servidor. Si el objetivo de la aplicación ASP.NET MVC era permitir que el usuario interactuara con todos los clientes, serían muchos clientes recuperados en una sola consulta y administrados en el explorador. En su lugar, dejaremos que la aplicación presente 10 filas a la vez y GetPagedCustomers proporcionará este filtro. La consulta que finalmente tendré que ejecutar luce así:

context.Customers.Where(c => 
c.SalesOrderHeaders.Any()).Skip(skip).Take(take).ToList()

La vista sabrá qué página debe solicitar y suministrar dicha información al controlador. El controlador estará a cargo de saber cuántas filas deberá suministrar por página. El controlador calculará el valor de “omisión” mediante el número de página y las filas por página. Cuando el controlador llama al método GetPagedCustomers, pasará el valor de omisión calculado, así como también, las filas por página, que son el valor de “toma”. De modo que si estamos en la página cuatro y presentamos 10 filas por página, la omisión será 40 y la toma será 10.

La consulta de paginación crea primero un filtro que solicita solamente aquellos clientes que tienen alguna SalesOrders. Entonces, mediante los métodos Skip y Take de LINQ, los datos resultantes serán un subconjunto de aquellos clientes. La consulta completa, incluida en la paginación, se ejecuta en la base de datos. La base de datos recupera solamente la cantidad de filas especificadas por el método Take.

La consulta consta de unas cuantas partes para permitir algunos trucos que agregaré más adelante. Aquí hay una primera pasada en el método GetPagedCustomers que se llamará desde HomeController:

public static List<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        return query.Skip(skip).Take(take).ToList();
      }
    }

El método Index del controlador que llama a este método determinará la cantidad de filas que debe recuperar mediante una variable que llamaré pageSize, que se vuelve el valor de Take. El método Index también especifica dónde comenzar según el número de una página, que pasará como un parámetro, como se muestra aquí:

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      return View(customers);
    }

Esto nos permite avanzar bastante. La paginación por parte del servidor está completamente en su lugar. Con WebGrid en el marcado de vista Index, podemos mostrar los clientes recuperados desde el método GetPagedCustomers. En el marcado, necesita declarar y crear una instancia de la cuadrícula, pasando Model, que representa la List<Customer> que se proporcionó cuando el controlador creó la vista. Después, con el método WebGrid GetHtml, puede dar formato a la cuadrícula y especificar qué columnas desea mostrar. Sólo mostraré tres de las propiedades Customer: CompanyName, FirstName y LastName. Le dará gusto encontrar una compatibilidad completa de IntelliSense a medida que escribe este marcado, ya sea que use sintaxis asociada con vistas ASPX o con la sintaxis del nuevo motor de vista Razor MVC 3 (como en el siguiente ejemplo). En la primera columna, proporcionaré un ActionLink Editar, de modo que el usuario pueda editar cualquiera de los clientes que se muestran:

@{
  var grid = new WebGrid(Model); 
}
<div id="customergrid">
  @grid.GetHtml(columns: grid.Columns(
    grid.Column(format: (item) => Html.ActionLink
      ("Edit", "Edit", new { customerId = item.CustomerID })),
  grid.Column("CompanyName", "Company"), 
  grid.Column("FirstName", "First Name"),
  grid.Column("LastName", "Last Name")
   ))
</div>

El resultado se muestra en la Figura 1.

image: Providing Edit ActionLinks in the WebGrid

Figura 1 Proporcionando ActionLinks Editar en WebGrid

Hasta ahora, todo perfecto. Pero esto no ofrece una forma para que el usuario explore otra página de datos. Hay algunas maneras para lograr esto. Una forma es especificar el número de página en URI, por ejemplo, http://adventureworksmvc.com/Page/3. Seguramente no quiere pedirle al usuario final que haga esto. Un mecanismo de detección es contar con controles de paginación, como vínculos de números de página “1 2 3 4 5 …” o vínculos que indiquen hacia delante o atrás, por ejemplo “<< >>.”

El obstáculo que ahora impide habilitar los vínculos de paginación es que la página de vista Index no sabe que existen más clientes que se deben adquirir. Solamente sabe que el universo de clientes son los 10 que está mostrando. Al agregar lógicas adicionales a la capa de acceso a los datos y pasarla a la vista mediante el controlador, puede solucionar el problema. Comencemos con la lógica de acceso a los datos.

Para saber si hay más registros más allá del conjunto actual de clientes, necesitará tener un conteo de todos los clientes posibles que la consulta pueda recuperar sin paginar en grupos de 10. En este punto es donde la composición de la consulta en GetPagedCustomers tiene resultados. Observe que la primera consulta se devuelve a _customerQuery, una variable que se declara en el nivel de clase, como se muestra aquí:

_customerQuery = context.Customers.Where(c => c.SalesOrderHeaders.Any());

Puede anexar el método Count al final de esa consulta a fin de obtener el conteo de todos los clientes que coincidan con la consulta antes de que se aplique la paginación. El método Count forzará la ejecución de una consulta relativamente simple de inmediato. Aquí está la consulta ejecutada en SQL Server, desde el cual la respuesta devuelve un solo valor:

    SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
           COUNT(1) AS [A1]
           FROM [SalesLT].[Customer] AS [Extent1]
           WHERE  EXISTS (SELECT 
                  1 AS [C1]
                  FROM [SalesLT].[SalesOrderHeader] AS [Extent2]
                  WHERE [Extent1].[CustomerID] = [Extent2].[CustomerID]
           )
    )  AS [GroupBy1]

Luego que tenga el conteo, puede determinar si la página actual del cliente es la primera página, la última página o alguna página en medio. Entonces puede usar esa lógica para decidir qué vínculo desea mostrar. Por ejemplo, si está más allá de la primera página de clientes, es lógico mostrar un vínculo para tener acceso a las primeras páginas de datos de clientes con un vínculo para la página anterior, como “<<.”

Podemos calcular los valores para representar esta lógica en la clase de acceso a los datos y luego exponerla en una clase contenedora junto con los clientes. Aquí presento la nueva clase que usaré:

public class PagedList<T>
  {
    public bool HasNext { get; set; }
    public bool HasPrevious { get; set; }
    public List<T> Entities { get; set; }
  }

El método GetPagedCustomers devolverá ahora una clase PagedList en lugar de una List. La Figura 2 muestra la nueva versión de GetPagedCustomers.

Figura 2 La nueva versión de GetPagedCustomers

public static PagedList<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        var customerCount = query.Count();

        var customers = query.Skip(skip).Take(take).ToList();
      
        return new PagedList<Customer>
        {
          Entities = customers,
          HasNext = (skip + 10 < customerCount),
          HasPrevious = (skip > 0)
        };
      }
    }

Con la nueva variable rellena, observemos cómo el método Index en HomeController puede devolverlos a la vista. Aquí es donde puede usar el nuevo ViewBag. Aún devolvemos los resultados de la consulta de clientes en una vista, pero además puede llenar los valores a fin de determinar cómo lucirá el marcado para los vínculos siguiente y anterior en ViewBag. Entonces estarán disponibles en la vista en el tiempo de ejecución:

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      ViewBag.HasPrevious = DataAccess.HasPreviousCustomers;
      ViewBag.HasMore = DataAccess.HasMoreCustomers;
      ViewBag.CurrentPage = (page ?? 0);
      return View(customers);
    }

Es importante comprender que ViewBag es dinámico, no está fuertemente tipado. ViewBag no viene realmente con HasPrevious y HasMore. Simplemente los invente a medida que escribía el código. Así que no se alarme si IntelliSense no le sugiere esto. Puede crear cualquier propiedad dinámica que desee.

Si ha usado el diccionario ViewPage.ViewData y tiene curiosidad de cómo es esto diferente, ViewBag hace el mismo trabajo. Pero además de hacer que su código se vea mejor, las propiedades están tipificadas. Por ejemplo, HasNext es un dynamic{bool} y CurrentPage es un dynamic{int}. No tendrá que convertir los valores cuando los recupere más tarde.

En el marcado, todavía tengo la lista de clientes en la variable Model, pero también hay una variable ViewBag disponible. Todo dependerá de usted a medida que escribe las propiedades dinámicas en el marcado. Una información sobre herramientas le recuerda que las propiedades son dinámicas, como se muestra en la Figura 3.

image: ViewBag Properties Aren’t Available Through IntelliSense Because They’re Dynamic

Figura 3 Las propiedades de ViewBag no están disponibles mediante IntelliSense ya que son dinámicas

Aquí está el marcado que usa las variables ViewBag para determinar si desea o no mostrar los vínculos de exploración:

@{ if (ViewBag.HasPrevious)
  {
    @Html.ActionLink("<<", "Index", new { page = (ViewBag.CurrentPage - 1) })
  }
}

@{ if (ViewBag.HasMore)
   { @Html.ActionLink(">>", "Index", new { page = (ViewBag.CurrentPage + 1) }) 
  }
}

Esta lógica es un giro en el marcado usado en el tutorial de la aplicación NerdDinner, que puede encontrar en nerddinnerbook.s3.amazonaws.com/Intro.htm.

Ahora cuando ejecuto la aplicación, puedo explorar desde una página de clientes a la siguiente.

Cuando estoy en la primera página, tengo un vínculo para explorar la siguiente página, pero nada para ir a la página anterior ya que no hay ninguna (consulte la Figura 4).

image: The First Page of Customer Has Only a Link to Navigate to the Next Page

Figura 4 La primera página de clientes tiene solamente un vínculo para explorar la siguiente página

Cuando hago clic en el vínculo y exploro la siguiente página, puede ver que ahora hay vínculos para ir a la página anterior o a la siguiente (consulte la Figura 5).

image: A Single Page of Customers with Navigation Links to Go to Previous or Next Page of Customers

Figura 5 Una sola página de clientes con vínculos de exploración para ir a la página anterior o siguiente de los clientes

 Por supuesto, el siguiente paso será trabajar con un diseñador para que esta paginación sea más atractiva.

Pieza fundamental de su cuadro de herramientas

Para resumir, si bien existen muchas herramientas para simplificar la paginación por parte del cliente, como la extensión DataTables de jQuery y la nueva WebGrid ASP.NET MVC 3, puede que las necesidades de la aplicación no se beneficien siempre de traer grandes cantidades de datos. Ser capaz de realizar una paginación eficiente por parte del servidor es una pieza fundamental del cuadro de herramientas. Entity Framework y ASP.NET MVC funcionan juntos para proporcionar una muy buena experiencia para el usuario y, al mismo tiempo, simplifican su tarea de desarrollo para realizar esto.

Julie Lerman es MVP de Microsoft, profesora de .NET y consultora que vive en las colinas de Vermont. Puede encontrar su presentación sobre acceso a datos y otros temas de Microsoft .NET en grupos de usuarios y congresos en todo el mundo. Lerman mantiene un blog en thedatafarm.com/blog y es la autora del célebre libro, “Programming Entity Framework” (O’Reilly Media, 2009). Puede seguir a Julie en Twitter.com/julielerman.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Vishal Joshi