Implementar a paginação eficiente de dados
pela Microsoft
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 mil jantares de uma só vez, exibiremos apenas 10 jantares futuros de cada vez e permitiremos que os usuários finais façam a página de volta e encaminhem toda a lista de maneira amigável ao SEO.
Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais Introdução With MVC 3 ou MVC Music Store.
NerdDinner Etapa 8: Suporte à Paginação
Se nosso site for bem sucedido, terá milhares de jantares futuros. Precisamos garantir que nossa interface do usuário seja dimensionada para lidar com todos esses jantares e permita que os usuários naveguem por eles. Para habilitar isso, adicionaremos suporte de paginação à nossa URL /Dinners para que, em vez de exibir mil jantares de uma só vez, exibiremos apenas 10 jantares futuros de cada vez e permitiremos que os usuários finais façam a página de volta e encaminhem toda a lista de maneira amigável ao 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:
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> de nosso 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 to 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. 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 Index() do DinnersController para que ele aplique 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. LINQ to SQL é inteligente o suficiente para construir uma consulta SQL otimizada que executa essa lógica de ignorar 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, apenas os 10 que queremos serão recuperados como parte dessa solicitação (tornando-a 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 indica qual intervalo de jantar um usuário está solicitando.
Usando um valor querystring
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 ignoradas. Estamos usando o operador "coalescing" nulo 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 Inserida
Uma alternativa ao uso de um valor querystring seria inserir o parâmetro de página dentro da própria URL real. Por exemplo: /Dinners/Page/2 ou /Dinners/2. ASP.NET MVC inclui um poderoso mecanismo de roteamento de URL 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 que desejamos. Tudo o que precisamos fazer é abrir o arquivo Global.asax em nosso projeto:
Em seguida, registre uma nova regra de mapeamento usando o método auxiliar MapRoute(), como a primeira chamada para rotas. 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);
}
Acima, estamos registrando uma nova regra de roteamento chamada "UpcomingDinners". Indicamos 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 que agora 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 /Dinners , veremos os primeiros 10 próximos jantares:
E quando digitarmos /Dinners/Page/1 , veremos a próxima página de jantares:
Adicionando interface do usuário 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 Jantares no banco de dados, bem como quantas páginas de dados isso se traduz. Em seguida, precisaremos calcular se o valor "page" 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.
Abaixo está uma classe auxiliar "PaginatedList" simples que deriva da classe de coleção List<T> interna no .NET Framework. Ele implementa uma classe de coleção reutilizável que pode ser usada para paginar qualquer sequência de dados IQueryable. Em nosso aplicativo NerdDinner, faremos com que ele funcione nos resultados do IQueryable<Dinner> , mas ele pode ser usado facilmente em resultados de IQueryable<Product> ou IQueryable<Customer> em outros cenários de aplicativo:
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". Em seguida, 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 de dados atual.
Em seguida, podemos atualizar nosso método auxiliar DinnersController.Index() para criar um Jantar> PaginatedList<do nosso resultado DinnerRepository.FindUpcomingDinners() e passá-lo para nosso modelo de exibiçã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 do Jantar ViewPage<NerdDinner.Helpers.PaginatedList<em vez do ViewPage<IEnumerable<Dinner>> e, em seguida, adicionar o seguinte código à parte inferior do nosso modelo de exibição para mostrar ou ocultar a interface do usuário de navegação seguinte 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 tem 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:
Também temos <<< uma interface do usuário de navegação e >>> na parte inferior da página que nos permite ignorar avanços e versões anteriores sobre nossos dados usando URLs acessíveis do mecanismo de pesquisa:
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 forma como usá-lo e garantir que ele não seja abusado. É importante reconhecer que retornar um resultado de T> IQueryable<de seu repositório permite que o código de chamada acrescente a ele métodos de operador encadeados e, portanto, participe da execução final da consulta. Se você não quiser fornecer esse código de chamada, retorne os resultados de 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. Nesse cenário, podemos atualizar nosso método finder FindUpcomingDinners() para ter uma assinatura que retornasse um PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Ou retorne um Jantar> IList<e use um param "totalCount" out para retornar a contagem total de Jantares: 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.