Implementar paginação de dados eficiente

pela Microsoft

Baixar PDF

Esta é a etapa 8 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.

A Etapa 8 mostra como adicionar suporte à paginação à nossa URL /Dinners para que, em vez de exibir 1.000 jantares de uma só vez, iremos exibir apenas 10 próximos jantares de cada vez e permitiremos que os usuários finais naveguem para trás e para frente por toda a lista de uma maneira otimizada para SEO.

Se você estiver usando ASP.NET MVC 3, recomendamos que você siga os tutoriais Introdução ao MVC 3 ou MVC Music Store.

Etapa 8 do NerdDinner: Suporte para paginação

Se nosso site for bem sucedido, terá milhares de jantares futuros. Precisamos ter certeza de que nossa interface do usuário é escalável para lidar com todos esses jantares e permite que os usuários os explorem. Para habilitar isso, adicionaremos suporte de paginação à nossa URL /Dinners. Assim, em vez de exibir 1.000 jantares de uma só vez, exibiremos apenas 10 próximos jantares de cada vez. Isso permitirá que os usuários finais naveguem para trás e para frente por toda a lista de uma maneira compatível com SEO.

Recapitulação do método de ação Index()

O método de ação Index() em nossa classe DinnersController atualmente tem a seguinte aparência:

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

Quando uma solicitação é feita para a URL /Dinners , ela recupera uma lista de todos os jantares futuros e, em seguida, renderiza uma listagem de todos eles:

Captura de tela da lista de Próximos Jantares do Jantar Nerd.

Noções básicas sobre IQueryable<T>

Iqueryable<T> é uma interface que foi introduzida com LINQ como parte do .NET 3.5. Ele permite cenários avançados de "execução adiada" que podemos aproveitar para implementar o suporte à paginação.

Em nosso DinnerRepository, estamos retornando uma sequência IQueryable<Dinner> do método FindUpcomingDinners():

public class DinnerRepository {

    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindUpcomingDinners() {
    
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

O objeto IQueryable<Dinner> retornado pelo nosso método FindUpcomingDinners() encapsula uma consulta para recuperar objetos Dinner de nosso banco de dados usando LINQ para SQL. É importante ressaltar que ele não executará a consulta no banco de dados até tentarmos acessar/iterar os dados na consulta ou até chamarmos o método ToList() nele. O código que chama nosso método FindUpcomingDinners() pode opcionalmente optar por adicionar operações/filtros "encadeados" adicionais ao objeto IQueryable<Dinner> antes de executar a consulta. O LINQ to SQL é inteligente o suficiente para executar a consulta combinada no banco de dados quando os dados são solicitados.

Para implementar a lógica de paginação, podemos atualizar nosso método de ação índice() do DinnersController para que ele aplique os operadores adicionais "Skip" e "Take" à sequência IQueryable<Dinner> retornada antes de chamar ToList() nela:

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

O código acima ignora os primeiros 10 jantares futuros no banco de dados e retorna 20 jantares. O LINQ to SQL é inteligente o suficiente para construir uma consulta SQL otimizada que executa essa lógica de pular no banco de dados SQL – e não no servidor web. Isso significa que, mesmo que tenhamos milhões de jantares futuros no banco de dados, somente os 10 que queremos serão recuperados como parte dessa solicitação (tornando-o eficiente e escalonável).

Adicionando um valor de "página" à URL

Em vez de codificar um intervalo de páginas específico, queremos que nossas URLs incluam um parâmetro "page" que indique qual intervalo de jantar um usuário está solicitando.

Usando um valor de string de consulta

O código a seguir demonstra como podemos atualizar nosso método de ação Index() para dar suporte a um parâmetro querystring e habilitar URLs como /Dinners?page=2:

//
// GET: /Dinners/
//      /Dinners?page=2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();

    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

O método de ação Index() acima tem um parâmetro chamado "page". O parâmetro é declarado como um inteiro anulável (isso é o que int? indica). Isso significa que a URL /Dinners?page=2 fará com que um valor "2" seja passado como o valor do parâmetro. A URL /Dinners (sem um valor querystring) fará com que um valor nulo seja passado.

Estamos multiplicando o valor da página pelo tamanho da página (neste caso, 10 linhas) para determinar quantos jantares devem ser ignorados. Estamos usando o operador "coalescing" nulo em C# (??), que é útil ao lidar com tipos anuláveis. O código acima atribui à página o valor de 0 se o parâmetro de página for nulo.

Usando valores de URL incorporados

Uma alternativa ao uso de um valor de querystring seria inserir o parâmetro de página dentro da URL real em si. Por exemplo: /Dinners/Page/2 ou /Dinners/2. ASP.NET MVC inclui um mecanismo de roteamento de URL avançado que facilita o suporte a cenários como este.

Podemos registrar regras de roteamento personalizadas que mapeiam qualquer formato de URL ou URL de entrada para qualquer classe de controlador ou método de ação desejado. Tudo o que precisamos fazer é abrir o arquivo Global.asax em nosso projeto.

Captura de tela da árvore de navegação Nerd Dinner. O ponto global a s a x está selecionado e realçado.

Em seguida, registre uma nova regra de mapeamento usando o método auxiliar MapRoute(), como a primeira chamada para routes.MapRoute() abaixo:

public void RegisterRoutes(RouteCollection routes) {

   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(                                        
        "UpcomingDinners",                               // Route name
        "Dinners/Page/{page}",                           // URL with params
        new { controller = "Dinners", action = "Index" } // Param defaults
    );

    routes.MapRoute(
        "Default",                                       // Route name
        "{controller}/{action}/{id}",                    // URL with params
        new { controller="Home", action="Index",id="" }  // Param defaults
    );
}

void Application_Start() {
    RegisterRoutes(RouteTable.Routes);
}

Aqui acima, estamos registrando uma nova regra de roteamento chamada "UpcomingDinners". Estamos indicando que ele tem o formato de URL "Dinners/Page/{page}" – em que {page} é um valor de parâmetro inserido na URL. O terceiro parâmetro para o método MapRoute() indica que devemos mapear URLs que correspondam a esse formato ao método de ação Index() na classe DinnersController.

Podemos usar exatamente o mesmo código Index() que tínhamos antes com nosso cenário de Querystring– exceto agora que nosso parâmetro "page" virá da URL e não da querystring:

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    
    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

E agora, quando executarmos o aplicativo e digitarmos /Jantares , veremos os primeiros 10 próximos jantares:

Captura de tela da lista de Próximos Jantares do Nerd Dinners.

E quando digitarmos /Dinners/Page/1 , veremos a próxima página de jantares:

Captura de tela da próxima página da lista de Jantares Futuros.

Adicionando interface de navegação de página

A última etapa para concluir nosso cenário de paginação será implementar a interface do usuário de navegação "próxima" e "anterior" em nosso modelo de exibição para permitir que os usuários ignorem facilmente os dados do Dinner.

Para implementar isso corretamente, precisaremos saber o número total de Refeições no banco de dados, bem como em quantas páginas de dados isso equivale. Em seguida, precisaremos calcular se o valor de "página" solicitado no momento está no início ou no final dos dados e mostrar ou ocultar a interface do usuário "anterior" e "próxima" adequadamente. Poderíamos implementar essa lógica em nosso método de ação Index(). Como alternativa, podemos adicionar uma classe auxiliar ao nosso projeto que encapsula essa lógica de uma maneira mais reutilizável.

Veja abaixo uma classe auxiliar simples "PaginatedList" que deriva da classe de coleção List<T>, parte integrante do .NET Framework. Ele implementa uma classe de coleção reutilizável que pode ser usada para paginar qualquer sequência de dados IQueryable. Em nossa aplicação NerdDinner, ele funcionará com os resultados do IQueryable<Dinner>, mas pode ser facilmente usado em resultados do IQueryable<Product> ou IQueryable<Customer> em outros cenários de aplicação.

public class PaginatedList<T> : List<T> {

    public int PageIndex  { get; private set; }
    public int PageSize   { get; private set; }
    public int TotalCount { get; private set; }
    public int TotalPages { get; private set; }

    public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
        PageIndex = pageIndex;
        PageSize = pageSize;
        TotalCount = source.Count();
        TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);

        this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
    }

    public bool HasPreviousPage {
        get {
            return (PageIndex > 0);
        }
    }

    public bool HasNextPage {
        get {
            return (PageIndex+1 < TotalPages);
        }
    }
}

Observe acima como ele calcula e expõe propriedades como "PageIndex", "PageSize", "TotalCount" e "TotalPages". Ele também expõe duas propriedades auxiliares "HasPreviousPage" e "HasNextPage" que indicam se a página de dados na coleção está no início ou no final da sequência original. O código acima fará com que duas consultas SQL sejam executadas – a primeira para recuperar a contagem do número total de objetos Dinner (isso não retorna os objetos– em vez disso, executa uma instrução "SELECT COUNT" que retorna um inteiro) e a segunda para recuperar apenas as linhas de dados que precisamos do nosso banco de dados para a página atual de dados.

Em seguida, podemos atualizar nosso método auxiliar DinnersController.Index() para criar uma PaginatedList<Dinner> do resultado DinnerRepository.FindUpcomingDinners() e passá-lo para o nosso modelo de visão:

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);

    return View(paginatedDinners);
}

Em seguida, podemos atualizar o modelo de exibição \Views\Dinners\Index.aspx para herdar de ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> em vez de ViewPage<IEnumerable<Dinner>>. Depois, adicione o seguinte código à parte inferior do modelo de exibição para mostrar ou ocultar a interface do usuário de navegação de próxima e anterior:

<% if (Model.HasPreviousPage) { %>

    <%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>

<% } %>

<% if (Model.HasNextPage) {  %>

    <%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>

<% } %>

Observe acima como estamos usando o método auxiliar Html.RouteLink() para gerar nossos hiperlinks. Esse método é semelhante ao método auxiliar Html.ActionLink() que usamos anteriormente. A diferença é que estamos gerando a URL usando a regra de roteamento "UpcomingDinners" que configuramos em nosso arquivo Global.asax. Isso garante que geraremos URLs para nosso método de ação Index() que tenha o formato: /Dinners/Page/{page} – em que o valor {page} é uma variável que estamos fornecendo acima com base no PageIndex atual.

E agora, quando executarmos nosso aplicativo novamente, veremos 10 jantares por vez em nosso navegador:

Captura de tela da lista de Próximos Jantares na página Jantar Nerd.

Também temos <<< e uma interface do usuário de >>> navegação na parte inferior da página que nos permite pular para frente e para trás sobre nossos dados usando URLs acessíveis por mecanismos de busca.

Captura de tela da página Jantares Nerd com a lista de Jantares Futuros.

Tópico Lateral: Noções básicas sobre as implicações do IQueryable<T>
IQueryable<T> é um recurso muito poderoso que permite uma variedade de cenários de execução adiados interessantes (como consultas baseadas em paginação e composição). Assim como acontece com todos os recursos poderosos, você deseja ter cuidado com a maneira como usá-lo e garantir que ele não seja abusado. É importante reconhecer que retornar um resultado IQueryable<T> de seu repositório permite que o código de chamada adicione métodos de operadores encadeados, participando assim da execução final da consulta. Se você não quiser fornecer ao código de chamada essa capacidade, deverá retornar os resultados da IList<T> ou IEnumerable<T> , que contêm os resultados de uma consulta que já foi executada. Para cenários de paginação, isso exigiria que você enviasse a lógica de paginação de dados real para o método de repositório que está sendo chamado. Neste cenário, podemos atualizar nosso método finder FindUpcomingDinners() para ter uma assinatura que retorne uma PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } ou retorne um IList<Dinner> e use um parâmetro "out totalCount" para retornar a contagem total de Dinners: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

Próxima Etapa

Agora vamos examinar como podemos adicionar suporte de autenticação e autorização ao nosso aplicativo.