Pontos de dados

Paginação de servidor com o Entity Framework e o ASP.NET MVC 3

Julie Lerman

Baixar o código de exemplo

image: Julie LermanNa coluna Pontos de dados de fevereiro, mostrei o plug-in jQuery DataTables e sua capacidade de lidar com altos volumes de dados no cliente. Isso funciona bem com aplicativos Web, quando você quer organizar grandes volumes de dados. Neste mês, o foco será como usar consultas que retornam cargas menores para permitir outro tipo de interação com os dados. Isso é importante principalmente quando você visa aplicativos móveis.

Aproveitarei os recursos introduzidos no ASP.NET MVC 3 e demonstrarei como usá-los junto com paginação de servidor eficiente no Entity Framework. Essa tarefa implica dois desafios. O primeiro é fornecer uma consulta do Entity Framework com os parâmetros de paginação corretos. O segundo é imitar um recurso de paginação do cliente fornecendo pistas visuais para indicar que existem mais dados a serem recuperados, bem como links para disparar a recuperação.

O ASP.NET MVC 3 tem muitos recursos novos, como o novo mecanismo de exibição Razor, aprimoramentos de validação e uma infinidade de outros recursos JavaScript. A página de anúncio do lançamento do MVC está em asp.net/mvc, onde você pode baixar o ASP.NET MVC 3 e encontrar links para postagens de blog e vídeos de treinamento que o ajudarão a se familiarizar. Um dos novos recursos que usarei é ViewBag. Se você já usava o ASP.NET MVC, ViewBag é um aprimoramento da classe ViewData que permite usar propriedades criadas dinamicamente.

Outro novo elemento oferecido pelo ASP.NET MVC 3 é o System.Web.Helpers.WebGrid especializado. Embora um dos recursos da grade seja a paginação, usarei a nova grade, mas não sua paginação, neste exemplo porque ela é de cliente — ou seja, pagina um conjunto de dados fornecidos a ela, da mesma forma que o plug-in DataTables. Vou usar uma paginação de servidor em vez disso.

Para este pequeno aplicativo, precisarei de um Modelo de Dados de Entidade com o qual trabalhar. Estou usando um que foi criado com base no banco de dados de exemplo AdventureWorksLT da Microsoft, mas só preciso de Customer e SalesOrderHeaders no modelo. Coloquei as propriedades Customer rowguid, PasswordHash e PasswordSalt em uma entidade separada, assim não preciso me preocupar com elas na edição. Exceto por essa pequena alteração, não modifiquei o modelo tendo em vista o seu padrão.

Criei um projeto usando o modelo de projeto padrão do ASP.NET MVC 3. Ele preenche previamente uma série de controladores e exibições, e vou deixar o HomeController padrão apresentar Customers.

Usarei uma classe DataAccess simples para propiciar interação com o modelo, o contexto e, subsequentemente, o banco de dados. Nessa classe, o método GetPagedCustomers oferece paginação de servidor. Se o objetivo do aplicativo ASP.NET MVC era permitir que o usuário interagisse com todos os clientes, muitos clientes seriam retornados em uma única consulta e gerenciados no navegador. Em vez disso, vamos permitir que o aplicativo apresente 10 linhas de cada vez, e o método GetPagedCustomers fornecerá esse filtro. A consulta que terei de executar mais tarde é parecida com esta:

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

A exibição saberá qual página deve ser solicitada e dará essa informação ao controlador. O controlador estará encarregado de saber quantas linhas deverão ser fornecidas por página. Ele calculará o valor “skip” usando o número de página e as linhas por página. Quando o controlador chamar o método GetPagedCustomers, passará o valor skip calculado, bem como as linhas por página, que é o valor “take”. Assim, se estivermos na página quatro e forem apresentadas 10 linhas por página, o valor skip será 40 e take será 10.

A consulta de paginação primeiro cria um filtro que solicita apenas aqueles clientes que têm SalesOrders. Assim, usando os métodos Skip e Take de LINQ, os dados resultantes serão um subconjunto desses clientes. A consulta completa, incluindo a paginação, é executada no banco de dados. O banco de dados retorna somente o número de linhas especificado pelo método Take.

A consulta é composta por algumas partes para permitir alguns truques que adicionarei ao longo do processo. Este é um primeiro passo no método GetPagedCustomers que será chamado a partir de 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();
      }
    }

O método Index do controlador que chama esse método determinará o número de linhas a serem retornadas usando uma variável que chamarei de pageSize, que passa a ser o valor de Take. O método Index também especificará onde começar com base em um número de página que será passado como parâmetro, como ilustrado aqui:

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

Isso adianta bastante o nosso trabalho. A paginação do servidor está completamente implementada. Com um WebGrid na marcação de exibição de Index, podemos exibir os clientes retornados do método GetPagedCustomers. Na marcação, é preciso declarar e instanciar a grande, passando Model, que representa List<Customer> fornecido quando o controlador criou a exibição. Em seguida, usando o método WebGrid GetHtml, você pode formatar a grade especificando as colunas que devem ser exibidas. Mostrarei apenas três das propriedades de Customer: CompanyName, FirstName e LastName. Você ficará contente quando perceber que há suporte total para IntelliSense conforme for digitando essa marcação, independentemente de usar uma sintaxe associada a exibições ASPX ou a nova sintaxe do mecanismo de exibição Razor do MVC 3 (como é o caso do exemplo a seguir). Na primeira coluna, fornecerei um ActionLink de Edit para que o usuário possa editar quaisquer Customers que forem exibidos:

    @{
      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>

O resultado é mostrado na Figura 1.

image: Providing Edit ActionLinks in the WebGrid

Figura 1 Fornecendo ActionLinks de Edit no WebGrid

Até aqui tudo bem. Mas isso não permite que o usuário navegue para outra página de dados. Existem várias maneiras de se conseguir isso. Uma delas é especificar o número de página no URI — por exemplo, http://adventureworksmvc.com/Page/3. Certamente você não quer pedir para os seus usuários finais fazerem isso. Um mecanismo mais perceptível é ter controles de página, como links de número de página “1 2 3 4 5 …” ou links que indiquem avançar e voltar, por exemplo “<<      >>.”

O atual obstáculo para habilitar links de página é que a página de exibição Index não tem conhecimento de que existem mais Customers a serem adquiridos. Ela só sabe que o universo de clientes são os 10 que estão aparecendo. Você pode resolver esse problema adicionando um pouco mais de lógica à camada de acesso a dados e passá-la para a exibição por meio do controlador. Vamos começar com a lógica de acesso a dados.

Para saber se há mais registros além do conjunto de clientes atuais, você precisará ter a contagem de todos os clientes possíveis que a consulta retornaria sem paginação em grupos de 10. Isso vai compensar compor a consulta em GetPagedCustomers. Observe que a primeira consulta é retornada em _customerQuery, uma variável declarada no nível de classe, como ilustrado aqui:

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

Você pode acrescentar o método Count ao final dessa consulta para obter a contagem de todos os Customers que correspondem à consulta antes de a paginação ser aplicada. O método Count forçará a execução imediata de uma consulta relativamente simples. Esta é a consulta executada no SQL Server, cuja resposta retorna um único 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]

Depois que você tiver a contagem, poderá determinar se a página de clientes atual é a primeira página, a última página ou algo entre isso. Então você poderá usar essa lógica para decidir quais links serão exibidos. Por exemplo, se você estiver além da primeira página de clientes, será lógico exibir um link para acessar páginas anteriores de dados de cliente com um link para a página anterior, como “<<.”

Podemos calcular valores para representar essa lógica na classe de acesso a dados e a expor em uma classe wrapper junto com os clientes. Esta é a nova classe que vou usar:

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

Agora o método GetPagedCustomers retornará uma classe PagedList em vez de List. A Figura 2 mostra a nova versão de GetPagedCustomers.

Figura 2 A nova versão 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)
        };
      }
    }

Com as novas variáveis preenchidas, vejamos como o método Index em HomeController pode colocá-las de volta em View. É aqui que você pode usar a nova ViewBag. Ainda retornaremos os resultados da consulta de clientes em uma View, mas é possível preencher os valores para ajudar a determinar como será a marcação para os links anteriores e seguintes em ViewBag. Assim eles estarão disponíveis para View no tempo de execução:

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);
    }

É importante entender que ViewBag é dinâmica e não fortemente tipada. Na verdade, ViewBag não vem com HasPrevious e HasMore. Eu apenas as criei enquanto digito o código. Por isso não fique preocupado se o IntelliSense não fizer essa sugestão. Você pode criar as propriedades dinâmicas que quiser.

Se você esteve usando o dicionário ViewPage.ViewData e está curioso sobre as diferenças, ViewBag de fato faz o mesmo trabalho. Mas, além de tornar o código um pouco mais bonito, as propriedades são tipadas. Por exemplo, HasNext é um dynamic{bool} e CurrentPage é um dynamic{int}. Você não terá de calcular os valores quando recuperá-los posteriormente.

Na marcação, ainda tenho a lista de clientes na variável Model, mas também existe uma variável ViewBag disponível. Você fica por sua própria conta e risco quando insere as propriedades dinâmicas na marcação. Uma dica de ferramenta lembra que as propriedades são dinâmicas, como mostra a Figura 3.

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

Figura 3 As propriedades ViewBag não estão disponíveis via IntelliSense porque são dinâmicas

Aqui está a marcação que usa as variáveis ViewBag para determinar se os links de navegação devem ou não ser exibidos:

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

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

Essa lógica é uma variação da marcação usada no Tutorial do Aplicativo NerdDinner, disponível em nerddinnerbook.s3.amazonaws.com/Intro.htm.

Agora quando executo o aplicativo, posso navegar de uma página de clientes para a outra.

Quando estou na primeira página, tenho um link para acessar a próxima, mas nenhum link para ir para uma página anterior porque não há nenhuma (veja a Figura 4).

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

Figura 4 A primeira página de um cliente tem apenas um link que permite navegar até a próxima página

Quando clico no link e navego até a próxima página, observe que agora existem links para acessar a página anterior ou a seguinte (veja a Figura 5).

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

Figura 5 Uma única página de clientes com links de navegação para acessar a página anterior ou a próxima página

 A próxima etapa, claro, será trabalhar com um designer para tornar essa paginação mais interessante.

Peça crucial da sua caixa de ferramentas

Resumindo, embora existam inúmeras ferramentas para simplificar a paginação de cliente, como a extensão jQuery DataTables e o novo WebGrid do ASP.NET MVC 3, nem sempre suas necessidades de aplicativo podem ser beneficiadas com o retorno de grandes quantidades de dados. Poder executar uma paginação de servidor eficiente é uma peça crucial da sua caixa de ferramentas. A Estrutura de Entidade e o ASP.NET MVC trabalham juntos para proporcionar uma excelente experiência de usuário e, ao mesmo tempo, simplificar suas tarefas de desenvolvimento para que isso aconteça.

Julie Lerman é uma Microsoft MVP, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrar sua apresentação sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências ao redor do mundo. Seu blog está em thedatafarm.com/blog e ela é autora do livro altamente reconhecido, “Programming Entity Framework” (O’Reilly Media, 2009). Acompanhe o trabalho dela em Twitter.com/julielerman.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Vishal Joshi