Parte 3, Páginas do Razor com EF Core no ASP.NET Core - Classificar, Filtrar, Paginar

Por Tom Dykstra, Jeremy Likness e Jon P. Smith

O aplicativo Web Contoso University demonstra como criar aplicativos Web das Razor Pages usando o EF Core e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial.

Se você encontrar problemas que não possa resolver, baixe o aplicativo concluído e compare esse código com o que você criou seguindo o tutorial.

Este tutorial adiciona as funcionalidades de classificação, filtragem e paginação à página do Aluno.

A ilustração a seguir mostra uma página concluída. Os títulos de coluna são links clicáveis para classificar a coluna. Clique no título de coluna para alternar repetidamente entre a ordem de classificação ascendente e descendente.

Students index page

Adicionar classificação

Substitua o código em Pages/Students/Index.cshtml.cs pelo seguinte código para adicionar a classificação.

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;
    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder)
    {
        // using System;
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

O código anterior:

  • Requer a adição de using System;.
  • Adiciona propriedades para conter os parâmetros de classificação.
  • Altera o nome da propriedade Student para Students.
  • Substitui o código no método OnGetAsync.

O método OnGetAsync recebe um parâmetro sortOrder da cadeia de caracteres de consulta na URL. A URL e a cadeia de caracteres de consulta são geradas pelo auxiliar de marcação da âncora.

O parâmetro sortOrder é Name ou Date. O parâmetro sortOrder é opcionalmente seguido de _desc para especificar a ordem descendente. A ordem de classificação padrão é crescente.

Quando a página Índice é solicitada do link Alunos, não há nenhuma cadeia de caracteres de consulta. Os alunos são exibidos em ordem ascendente por sobrenome. A ordem ascendente por sobrenome é o default na instrução switch. Quando o usuário clica em um link de título de coluna, o valor sortOrder apropriado é fornecido no valor de cadeia de caracteres de consulta.

NameSort e DateSort são usados pelo Páginas do Razor para configurar os hiperlinks de título da coluna com os valores de cadeia de caracteres de consulta apropriados:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

O código usa o operador condicional C# ?:. O operador ?: é um operador ternário, utiliza três operandos. A primeira linha especifica que, quando sortOrder é nulo ou vazio, NameSort é definido como name_desc. Se sortOrdernão é nulo nem vazio, NameSort é definido como uma cadeia de caracteres vazia.

Essas duas instruções permitem que a página defina os hiperlinks de título de coluna da seguinte maneira:

Ordem de classificação atual Hiperlink do sobrenome Hiperlink de data
Sobrenome ascendente descending ascending
Sobrenome descendente ascending ascending
Data ascendente ascending descending
Data descendente ascending ascending

O método usa o LINQ to Entities para especificar a coluna pela qual classificar. O código inicializa um IQueryable<Student> antes da instrução switch e modifica-o na instrução switch:

IQueryable<Student> studentsIQ = from s in _context.Students
                                select s;

switch (sortOrder)
{
    case "name_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
        break;
    case "Date":
        studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
        break;
    case "date_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
        break;
    default:
        studentsIQ = studentsIQ.OrderBy(s => s.LastName);
        break;
}

Students = await studentsIQ.AsNoTracking().ToListAsync();

Quando um IQueryable é criado ou modificado, nenhuma consulta é enviada ao banco de dados. A consulta não é executada até que o objeto IQueryable seja convertido em uma coleção. IQueryable são convertidos em uma coleção com uma chamada a um método como ToListAsync. Portanto, o código IQueryable resulta em uma única consulta que não é executada até que a seguinte instrução:

Students = await studentsIQ.AsNoTracking().ToListAsync();

OnGetAsync pode ficar detalhado com um grande número de colunas classificáveis. Para obter informações sobre uma maneira alternativa de codificar essa funcionalidade, confira Usar o LINQ dinâmico para simplificar o código na versão MVC desta série de tutoriais.

Substitua o código em Students/Index.cshtml pelo código a seguir. As alterações são realçadas.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

O código anterior:

  • Adiciona hiperlinks aos títulos de coluna LastName e EnrollmentDate.
  • Usa as informações em NameSort e DateSort para configurar hiperlinks com os valores de ordem de classificação atuais.
  • Altera o título da página de Índice para Alunos.
  • Altera Model.Student para Model.Students.

Para verificar se a classificação funciona:

  • Execute o aplicativo e selecione a guia Alunos.
  • Clique nos títulos de coluna.

Adicionar filtragem

Para adicionar a filtragem à página Índice de Alunos:

  • Uma caixa de texto e um botão Enviar são adicionados ao Páginas do Razor. A caixa de texto fornece uma cadeia de caracteres de pesquisa no nome ou sobrenome.
  • O modelo de página é atualizado para usar o valor da caixa de texto.

Atualizar o método OnGetAsync

Substitua o código em Students/Index.cshtml.cs pelo seguinte código para adicionar filtros:

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder, string searchString)
    {
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        CurrentFilter = searchString;
        
        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;
        if (!String.IsNullOrEmpty(searchString))
        {
            studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstMidName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

O código anterior:

  • Adiciona o parâmetro searchString ao método OnGetAsync e salva o valor do parâmetro na propriedade CurrentFilter. O valor de cadeia de caracteres de pesquisa é recebido de uma caixa de texto que é adicionada na próxima seção.
  • Adiciona uma cláusula Where à instrução LINQ. A cláusula Where seleciona somente os alunos cujo nome ou sobrenome contém a cadeia de caracteres de pesquisa. A instrução LINQ é executada somente se há um valor a ser pesquisado.

IQueryable x IEnumerable

O código chama o método Where em um objeto IQueryable, e o filtro é processado no servidor. Em alguns cenários, o aplicativo pode chamar o método Where como um método de extensão em uma coleção em memória. Por exemplo, suponha que _context.Students seja alterado do EF CoreDbSet para um método de repositório que retorna uma coleção IEnumerable. O resultado normalmente é o mesmo, mas em alguns casos pode ser diferente.

Por exemplo, a implementação do .NET Framework do Contains executa uma comparação diferencia maiúsculas de minúsculas por padrão. No SQL Server, a diferenciação de maiúsculas e minúsculas de Contains é determinada pela configuração de ordenação da instância do SQL Server. O SQL Server usa como padrão a não diferenciação de maiúsculas e minúsculas. O SQLite assume como padrão diferenciar maiúsculas de minúsculas. ToUpper pode ser chamado para fazer com que o teste diferencie maiúsculas de minúsculas de forma explícita:

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

O código anterior garantiria que o filtro não diferenciasse maiúsculas de minúsculas mesmo que o método Where fosse chamado em um IEnumerable ou fosse executado no SQLite.

Quando Contains é chamado em uma coleção IEnumerable, a implementação do .NET Core é usada. Quando Contains é chamado em um objeto IQueryable, a implementação do banco de dados é usada.

Chamar Contains em um IQueryable é geralmente preferível por motivos de desempenho. Com IQueryable, a filtragem é feita pelo servidor de banco de dados. Se um IEnumerable for criado primeiro, todas as linhas precisarão ser retornadas do servidor de banco de dados.

Há uma penalidade de desempenho por chamar ToUpper. O código ToUpper adiciona uma função à cláusula WHERE da instrução TSQL SELECT. A função adicionada impede que o otimizador use um índice. Considerando que o SQL é instalado como diferenciando maiúsculas de minúsculas, é melhor evitar a chamada ToUpper quando ela não for necessária.

Para obter mais informações, confira Como usar consulta que não diferencia maiúsculas de minúsculas com o provedor SQLite.

Atualizar a página Razor

Substitua o código em Pages/Students/Index.cshtml para adicionar um botão Pesquisar.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

O código anterior usa o auxiliar de marcação<form> para adicionar o botão e a caixa de texto de pesquisa. Por padrão, o auxiliar de marcação <form> envia dados de formulário com um POST. Com o POST, os parâmetros são passados no corpo da mensagem HTTP e não na URL. Quando o HTTP GET é usado, os dados de formulário são passados na URL como cadeias de consulta. Passar os dados com cadeias de consulta permite aos usuários marcar a URL. As diretrizes do W3C recomendam o uso de GET quando a ação não resulta em uma atualização.

Teste o aplicativo:

  • Selecione a guia Alunos e insira uma cadeia de caracteres de pesquisa. Se você estiver usando o SQLite, o filtro não diferenciará maiúsculas de minúsculas apenas se você tiver implementado o código opcional ToUpper mostrado anteriormente.

  • Selecione Pesquisar.

Observe que a URL contém a cadeia de caracteres de pesquisa. Por exemplo:

https://localhost:5001/Students?SearchString=an

Se a página estiver marcada, o indicador conterá a URL para a página e a cadeia de caracteres de consulta SearchString. O method="get" na marcação form é o que fez com que a cadeia de caracteres de consulta fosse gerada.

Atualmente, quando um link de classificação de título de coluna é selecionado, o valor de filtro da caixa Pesquisa é perdido. O valor de filtro perdido é corrigido na próxima seção.

Adicionar paginação

Nesta seção, uma classe PaginatedList é criada para dar suporte à paginação. A classe PaginatedList usa as instruções Skip e Take para filtrar dados no servidor em vez de recuperar todas as linhas da tabela. A ilustração a seguir mostra os botões de paginação.

Students index page with paging links

Criar a classe PaginatedList

Na pasta do projeto, crie PaginatedList.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

O método CreateAsync no código anterior usa o tamanho da página e o número da página e aplica as instruções Skip e Take ao IQueryable. Quando ToListAsync é chamado no IQueryable, ele retorna uma Lista que contém somente a página solicitada. As propriedades HasPreviousPage e HasNextPage são usadas para habilitar ou desabilitar os botões de paginação Anterior e Próximo.

O método CreateAsync é usado para criar o PaginatedList<T>. Um construtor não pode criar o objeto PaginatedList<T>; construtores não podem executar um código assíncrono.

Adicionar o tamanho da página à configuração

Adicione PageSize ao arquivo de appsettings.jsonConfiguração:

{
  "PageSize": 3,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Adicionar paginação ao IndexModel

Substitua o código em Students/Index.cshtml.cs para adicionar a paginação.

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly SchoolContext _context;
        private readonly IConfiguration Configuration;

        public IndexModel(SchoolContext context, IConfiguration configuration)
        {
            _context = context;
            Configuration = configuration;
        }

        public string NameSort { get; set; }
        public string DateSort { get; set; }
        public string CurrentFilter { get; set; }
        public string CurrentSort { get; set; }

        public PaginatedList<Student> Students { get; set; }

        public async Task OnGetAsync(string sortOrder,
            string currentFilter, string searchString, int? pageIndex)
        {
            CurrentSort = sortOrder;
            NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            DateSort = sortOrder == "Date" ? "date_desc" : "Date";
            if (searchString != null)
            {
                pageIndex = 1;
            }
            else
            {
                searchString = currentFilter;
            }

            CurrentFilter = searchString;

            IQueryable<Student> studentsIQ = from s in _context.Students
                                             select s;
            if (!String.IsNullOrEmpty(searchString))
            {
                studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                       || s.FirstMidName.Contains(searchString));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                    break;
            }

            var pageSize = Configuration.GetValue("PageSize", 4);
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}

O código anterior:

  • Altera o tipo da propriedade Students de IList<Student> para PaginatedList<Student>.
  • Adiciona o índice de página, o sortOrder atual e o currentFilter à assinatura do método OnGetAsync.
  • Salva a ordem de classificação na propriedade CurrentSort.
  • Redefine o índice de página como 1 quando há uma nova cadeia de caracteres de pesquisa.
  • Usa a classe PaginatedList para obter entidades de Aluno.
  • Define pageSize como 3 da Configuração, 4 se a configuração falhar.

Todos os parâmetros que OnGetAsync recebe são nulos quando:

  • A página é chamada no link Alunos.
  • O usuário ainda não clicou em um link de paginação ou classificação.

Quando um link de paginação recebe um clique, a variável de índice de páginas contém o número da página a ser exibido.

A propriedade CurrentSort fornece ao Páginas do Razor a ordem de classificação atual. A ordem de classificação atual precisa ser incluída nos links de paginação para que a ordem de classificação seja mantida durante a paginação.

A propriedade CurrentFilter fornece ao Páginas do Razor a cadeia de caracteres do filtro atual. O valor CurrentFilter:

  • Deve ser incluído nos links de paginação para que as configurações de filtro sejam mantidas durante a paginação.
  • Deve ser restaurado para a caixa de texto quando a página é exibida novamente.

Se a cadeia de caracteres de pesquisa é alterada durante a paginação, a página é redefinida como 1. A página precisa ser redefinida como 1, porque o novo filtro pode resultar na exibição de dados diferentes. Quando um valor de pesquisa é inserido e Enviar é selecionado:

  • A cadeia de caracteres de pesquisa foi alterada.
  • O parâmetro searchString não é nulo.

O método PaginatedList.CreateAsync converte a consulta de alunos em uma única página de alunos de um tipo de coleção compatível com paginação. Essa única página de alunos é passada para o Páginas do Razor.

Os dois pontos de interrogação em pageIndex na chamada PaginatedList.CreateAsync representam o operador de união de nulo. O operador de união de nulo define um valor padrão para um tipo que permite valor nulo. A expressão pageIndex ?? 1 retorna o valor de pageIndex se tiver um valor, caso contrário, retornará 1.

Substitua o código em Students/Index.cshtml pelo seguinte código. As alterações são realçadas:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: 
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

Os links de cabeçalho de coluna usam a cadeia de caracteres de consulta para passar a cadeia de caracteres de pesquisa atual para o método OnGetAsync:

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Students[0].LastName)
</a>

Os botões de paginação são exibidos por auxiliares de marcação:


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

Execute o aplicativo e navegue para a página de alunos.

  • Para verificar se a paginação funciona, clique nos links de paginação em ordens de classificação diferentes.
  • Para verificar se a paginação funciona corretamente com a classificação e filtragem, insira uma cadeia de caracteres de pesquisa e tente fazer a paginação.

students index page with paging links

Agrupamento

Essa seção cria uma página do About que exibe quantos alunos se inscreveram para cada data de inscrição. A atualização usa o agrupamento e inclui as seguintes etapas:

  • Crie um modelo de exibição para os dados usados pela página About.
  • Atualize a página About para usar o modelo de exibição.

Criar o modelo de exibição

Crie uma pasta Models/SchoolViewModels.

Crie SchoolViewModels/EnrollmentDateGroup.cs com o seguinte código:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Criar a Página do Razor

Crie um arquivo Pages/About.cshtml com o código a seguir:

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Students)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Criar o modelo de página

Atualize o arquivo Pages/About.cshtml.cs com o seguinte código:

using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Students { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Students
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Students = await data.AsNoTracking().ToListAsync();
        }
    }
}

A instrução LINQ agrupa as entidades de alunos por data de registro, calcula o número de entidades em cada grupo e armazena os resultados em uma coleção de objetos de modelo de exibição EnrollmentDateGroup.

Execute o aplicativo e navegue para a página Sobre. A contagem de alunos para cada data de registro é exibida em uma tabela.

About page

Próximas etapas

No próximo tutorial, o aplicativo usa migrações para atualizar o modelo de dados.

Neste tutorial, as funcionalidades de classificação, filtragem, agrupamento e paginação são adicionadas.

A ilustração a seguir mostra uma página concluída. Os títulos de coluna são links clicáveis para classificar a coluna. Clicar em um título de coluna alterna repetidamente entre a ordem de classificação ascendente e descendente.

Students index page

Caso tenha problemas que não consiga resolver, baixe o aplicativo concluído.

Adicionar uma classificação à página Índice

Adiciona cadeia de caracteres ao Students/Index.cshtml.csPageModel para conter os parâmetros de classificação:

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

Atualize o Students/Index.cshtml.csOnGetAsync com o seguinte código:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

O código anterior recebe um parâmetro sortOrder da cadeia de caracteres de consulta na URL. A URL (incluindo a cadeia de caracteres de consulta) é gerada pelo Auxiliar de Marcação de Âncora

O parâmetro sortOrder é "Name" ou "Date". Opcionalmente, o parâmetro sortOrder é seguido por "_desc" para especificar a ordem descendente. A ordem de classificação padrão é crescente.

Quando a página Índice é solicitada do link Alunos, não há nenhuma cadeia de caracteres de consulta. Os alunos são exibidos em ordem ascendente por sobrenome. A ordem ascendente por sobrenome é o padrão (caso fall-through) na instrução switch. Quando o usuário clica em um link de título de coluna, o valor sortOrder apropriado é fornecido no valor de cadeia de caracteres de consulta.

NameSort e DateSort são usados pelo Páginas do Razor para configurar os hiperlinks de título da coluna com os valores de cadeia de caracteres de consulta apropriados:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

O seguinte código contém o operador ?: condicional do C#:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

A primeira linha especifica que, quando sortOrder é nulo ou vazio, NameSort é definido como "name_desc". Se sortOrdernão for nulo ou vazio, NameSort será definido como uma cadeia de caracteres vazia.

O ?: operator também é conhecido como o operador ternário.

Essas duas instruções permitem que a página defina os hiperlinks de título de coluna da seguinte maneira:

Ordem de classificação atual Hiperlink do sobrenome Hiperlink de data
Sobrenome ascendente descending ascending
Sobrenome descendente ascending ascending
Data ascendente ascending descending
Data descendente ascending ascending

O método usa o LINQ to Entities para especificar a coluna pela qual classificar. O código inicializa um IQueryable<Student> antes da instrução switch e modifica-o na instrução switch:

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Quando um IQueryable é criado ou modificado, nenhuma consulta é enviada ao banco de dados. A consulta não é executada até que o objeto IQueryable seja convertido em uma coleção. IQueryable são convertidos em uma coleção com uma chamada a um método como ToListAsync. Portanto, o código IQueryable resulta em uma única consulta que não é executada até que a seguinte instrução:

Student = await studentIQ.AsNoTracking().ToListAsync();

OnGetAsync pode ficar detalhado com um grande número de colunas classificáveis.

Substitua o código em Students/Index.cshtml pelo seguinte código realçado:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

O código anterior:

  • Adiciona hiperlinks aos títulos de coluna LastName e EnrollmentDate.
  • Usa as informações em NameSort e DateSort para configurar hiperlinks com os valores de ordem de classificação atuais.

Para verificar se a classificação funciona:

  • Execute o aplicativo e selecione a guia Alunos.
  • Clique em Sobrenome.
  • Clique em Data de Registro.

Para obter um melhor entendimento do código:

  • Em Students/Index.cshtml.cs, defina um ponto de interrupção em switch (sortOrder).
  • Adicione uma inspeção para NameSort e DateSort.
  • Em Students/Index.cshtml, defina um ponto de interrupção em @Html.DisplayNameFor(model => model.Student[0].LastName).

Execute o depurador em etapas.

Adicionar uma Caixa de Pesquisa à página Índice de Alunos

Para adicionar a filtragem à página Índice de Alunos:

  • Uma caixa de texto e um botão Enviar são adicionados ao Páginas do Razor. A caixa de texto fornece uma cadeia de caracteres de pesquisa no nome ou sobrenome.
  • O modelo de página é atualizado para usar o valor da caixa de texto.

Adicionar a funcionalidade de filtragem a método Index

Atualize o Students/Index.cshtml.csOnGetAsync com o seguinte código:

public async Task OnGetAsync(string sortOrder, string searchString)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

O código anterior:

  • Adiciona o parâmetro searchString ao método OnGetAsync. O valor de cadeia de caracteres de pesquisa é recebido de uma caixa de texto que é adicionada na próxima seção.
  • Adicionou uma cláusula Where à instrução LINQ. A cláusula Where seleciona somente os alunos cujo nome ou sobrenome contém a cadeia de caracteres de pesquisa. A instrução LINQ é executada somente se há um valor a ser pesquisado.

Observação: o código anterior chama o método Where em um objeto IQueryable, e o filtro é processado no servidor. Em alguns cenários, o aplicativo pode chamar o método Where como um método de extensão em uma coleção em memória. Por exemplo, suponha que _context.Students seja alterado do EF CoreDbSet para um método de repositório que retorna uma coleção IEnumerable. O resultado normalmente é o mesmo, mas em alguns casos pode ser diferente.

Por exemplo, a implementação do .NET Framework do Contains executa uma comparação diferencia maiúsculas de minúsculas por padrão. No SQL Server, a diferenciação de maiúsculas e minúsculas de Contains é determinada pela configuração de ordenação da instância do SQL Server. O SQL Server usa como padrão a não diferenciação de maiúsculas e minúsculas. ToUpper pode ser chamado para fazer com que o teste diferencie maiúsculas de minúsculas de forma explícita:

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

O código anterior garantirá que os resultados diferenciem maiúsculas de minúsculas se o código for alterado para usar IEnumerable. Quando Contains é chamado em uma coleção IEnumerable, a implementação do .NET Core é usada. Quando Contains é chamado em um objeto IQueryable, a implementação do banco de dados é usada. O retorno de um IEnumerable de um repositório pode ter uma penalidade significativa de desempenho:

  1. Todas as linhas são retornadas do servidor de BD.
  2. O filtro é aplicado a todas as linhas retornadas no aplicativo.

Há uma penalidade de desempenho por chamar ToUpper. O código ToUpper adiciona uma função à cláusula WHERE da instrução TSQL SELECT. A função adicionada impede que o otimizador use um índice. Considerando que o SQL é instalado como diferenciando maiúsculas de minúsculas, é melhor evitar a chamada ToUpper quando ela não for necessária.

Adicionar uma Caixa de Pesquisa à página Student Index

Em Pages/Students/Index.cshtml, adicione o código realçado a seguir para criar um botão Pesquisar e o cromado variado.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">

O código anterior usa o auxiliar de marcação<form> para adicionar o botão e a caixa de texto de pesquisa. Por padrão, o auxiliar de marcação <form> envia dados de formulário com um POST. Com o POST, os parâmetros são passados no corpo da mensagem HTTP e não na URL. Quando o HTTP GET é usado, os dados de formulário são passados na URL como cadeias de consulta. Passar os dados com cadeias de consulta permite aos usuários marcar a URL. As diretrizes do W3C recomendam o uso de GET quando a ação não resulta em uma atualização.

Teste o aplicativo:

  • Selecione a guia Alunos e insira uma cadeia de caracteres de pesquisa.
  • Selecione Pesquisar.

Observe que a URL contém a cadeia de caracteres de pesquisa.

http://localhost:5000/Students?SearchString=an

Se a página estiver marcada, o indicador conterá a URL para a página e a cadeia de caracteres de consulta SearchString. O method="get" na marcação form é o que fez com que a cadeia de caracteres de consulta fosse gerada.

Atualmente, quando um link de classificação de título de coluna é selecionado, o valor de filtro da caixa Pesquisa é perdido. O valor de filtro perdido é corrigido na próxima seção.

Adicionar a funcionalidade de paginação à página Índice de Alunos

Nesta seção, uma classe PaginatedList é criada para dar suporte à paginação. A classe PaginatedList usa as instruções Skip e Take para filtrar dados no servidor em vez de recuperar todas as linhas da tabela. A ilustração a seguir mostra os botões de paginação.

Students index page with paging links

Na pasta do projeto, crie PaginatedList.cs com o seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

O método CreateAsync no código anterior usa o tamanho da página e o número da página e aplica as instruções Skip e Take ao IQueryable. Quando ToListAsync é chamado no IQueryable, ele retorna uma Lista que contém somente a página solicitada. As propriedades HasPreviousPage e HasNextPage são usadas para habilitar ou desabilitar os botões de paginação Anterior e Próximo.

O método CreateAsync é usado para criar o PaginatedList<T>. Um construtor não pode criar o objeto PaginatedList<T>; construtores não podem executar um código assíncrono.

Adicionar a funcionalidade de paginação ao método Index

Em Students/Index.cshtml.cs, atualize o tipo de Student do IList<Student> para PaginatedList<Student>:

public PaginatedList<Student> Student { get; set; }

Atualize o Students/Index.cshtml.csOnGetAsync com o seguinte código:

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)
{
    CurrentSort = sortOrder;
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    if (searchString != null)
    {
        pageIndex = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    Student = await PaginatedList<Student>.CreateAsync(
        studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}

O código anterior adiciona o índice de página, o sortOrder atual e o currentFilter à assinatura do método.

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)

Todos os parâmetros são nulos quando:

  • A página é chamada no link Alunos.
  • O usuário ainda não clicou em um link de paginação ou classificação.

Quando um link de paginação recebe um clique, a variável de índice de páginas contém o número da página a ser exibido.

CurrentSort fornece à Página do Razor a ordem de classificação atual. A ordem de classificação atual precisa ser incluída nos links de paginação para que a ordem de classificação seja mantida durante a paginação.

CurrentFilter fornece à Página do Razor a cadeia de caracteres do filtro atual. O valor CurrentFilter:

  • Deve ser incluído nos links de paginação para que as configurações de filtro sejam mantidas durante a paginação.
  • Deve ser restaurado para a caixa de texto quando a página é exibida novamente.

Se a cadeia de caracteres de pesquisa é alterada durante a paginação, a página é redefinida como 1. A página precisa ser redefinida como 1, porque o novo filtro pode resultar na exibição de dados diferentes. Quando um valor de pesquisa é inserido e Enviar é selecionado:

  • A cadeia de caracteres de pesquisa foi alterada.
  • O parâmetro searchString não é nulo.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

O método PaginatedList.CreateAsync converte a consulta de alunos em uma única página de alunos de um tipo de coleção compatível com paginação. Essa única página de alunos é passada para o Páginas do Razor.

Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

Os dois pontos de interrogação em PaginatedList.CreateAsync representam o operador de união de nulo. O operador de união de nulo define um valor padrão para um tipo que permite valor nulo. A expressão (pageIndex ?? 1) significará retornar o valor de pageIndex se ele tiver um valor. Se pageIndex não tiver um valor, 1 será retornado.

Atualize a marcação em Students/Index.cshtml. As alterações são realçadas:

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

Os links de cabeçalho de coluna usam a cadeia de caracteres de consulta para passar a cadeia de caracteres de pesquisa atual para o método OnGetAsync, de modo que o usuário possa classificar nos resultados do filtro:

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Student[0].LastName)
</a>

Os botões de paginação são exibidos por auxiliares de marcação:


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

Execute o aplicativo e navegue para a página de alunos.

  • Para verificar se a paginação funciona, clique nos links de paginação em ordens de classificação diferentes.
  • Para verificar se a paginação funciona corretamente com a classificação e filtragem, insira uma cadeia de caracteres de pesquisa e tente fazer a paginação.

students index page with paging links

Para obter um melhor entendimento do código:

  • Em Students/Index.cshtml.cs, defina um ponto de interrupção em switch (sortOrder).
  • Adicione uma inspeção para NameSort, DateSort, CurrentSort e Model.Student.PageIndex.
  • Em Students/Index.cshtml, defina um ponto de interrupção em @Html.DisplayNameFor(model => model.Student[0].LastName).

Execute o depurador em etapas.

Atualizar a página Sobre para mostras estatísticas de alunos

Nesta etapa, Pages/About.cshtml é atualizado para exibir quantos alunos se registraram para cada data de inscrição. A atualização usa o agrupamento e inclui as seguintes etapas:

  • Criar um modelo de exibição para os dados usados pela página Sobre.
  • Atualizar a página Sobre para usar o modelo de exibição.

Criar o modelo de exibição

Crie uma pasta SchoolViewModels na pasta Models.

Na pasta SchoolViewModels, adicione um EnrollmentDateGroup.cs com o seguinte código:

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Atualizar o modelo da página Sobre

Os modelos da Web no ASP.NET Core 2.2 não incluem a página Sobre. Se estiver usando o ASP.NET Core 2.2, crie a página Sobre o Razor.

Atualize o arquivo Pages/About.cshtml.cs com o seguinte código:

using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Student { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Student
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Student = await data.AsNoTracking().ToListAsync();
        }
    }
}

A instrução LINQ agrupa as entidades de alunos por data de registro, calcula o número de entidades em cada grupo e armazena os resultados em uma coleção de objetos de modelo de exibição EnrollmentDateGroup.

Modificar a página Sobre o Razor

Substitua o código no arquivo Pages/About.cshtml pelo seguinte código:

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Student)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Execute o aplicativo e navegue para a página Sobre. A contagem de alunos para cada data de registro é exibida em uma tabela.

Caso tenha problemas que não consiga resolver, baixe o aplicativo concluído para este estágio.

About page

Recursos adicionais

No próximo tutorial, o aplicativo usa migrações para atualizar o modelo de dados.