Часть 3. Razor Страницы с EF Core ASP.NET Core — сортировка, фильтрация, разбиение по страницам

Авторы: Том Дайкстра (Tom Dykstra), Джереми Ликнесс (Jeremy Likness) и Йон П. Смит (Jon P Smith)

Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.

В этом учебнике вы добавите на страницу учащихся функции сортировки, фильтрации и разбиения на страницы.

На следующем рисунке показана готовая страница. Заголовки столбцов являются ссылками, щелкнув которые, можно отсортировать столбец. Щелкайте заголовок столбца для переключения между сортировкой по возрастанию и убыванию.

Students index page

Добавление сортировки

Замените код в Pages/Students/Index.cshtml.cs следующем коде, чтобы добавить сортировку.

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

Предыдущий код:

  • Требует добавления using System;.
  • добавляет свойства, которые будут содержать параметры сортировки;
  • изменяет имя свойства Student на Students;
  • заменяет код в методе OnGetAsync.

Метод OnGetAsync принимает параметр sortOrder из строки запроса в URL-адресе. URL-адрес и строка запроса формируются вспомогательной функцией тегов привязки.

Параметр sortOrder имеет значение Name или Date. После параметра sortOrder может стоять _desc, чтобы указать порядок по убыванию. По умолчанию задан порядок сортировки по возрастанию.

При запросе страницы "Index" по ссылке Students строка запроса отсутствует. Учащиеся отображаются по фамилии в порядке возрастания. В операторе default сортировка по фамилии в порядке возрастания используется по умолчанию (switch). Когда пользователь щелкает ссылку заголовка столбца, в строке запроса указывается соответствующее значение sortOrder.

Для формирования гиперссылок в заголовках столбцов страница Razor использует NameSort и DateSort с соответствующими значениями строки запроса:

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

В коде используется условный оператор C# ?:. Оператор ?: является тернарным (принимает три операнда). Первая строка указывает, что, когда sortOrder равен null или пуст, NameSort имеет значение name_desc. Если sortOrderне является NULL или пустым, для NameSort задается пустая строка.

Следующие два оператора устанавливают гиперссылки в заголовках столбцов на странице следующим образом:

Текущий порядок сортировки Гиперссылка "Last Name" (Фамилия) Гиперссылка "Date" (Дата)
"Last Name" (Фамилия) по возрастанию по убыванию ascending
"Last Name" (Фамилия) по убыванию ascending ascending
"Date" (Дата) по возрастанию ascending по убыванию
"Date" (Дата) по убыванию ascending ascending

Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities. Код инициализирует IQueryable<Student> до оператора 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();

При создании или изменении IQueryable запрос в базу данных не отправляется. Запрос не выполнится, пока объект IQueryable не будет преобразован в коллекцию. IQueryable преобразуются в коллекцию путем вызова метода, такого как ToListAsync. Таким образом, код IQueryable создает одиночный запрос, который не выполняется до следующего оператора:

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

OnGetAsync можно расширить на случай большого числа сортируемых столбцов. Сведения об альтернативном способе программирования этой функциональности см. в статье Использование динамических запросов LINQ для упрощения кода в версии этой серии учебников для MVC.

Замените код в Students/Index.cshtml следующим: Изменения выделены.

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

Предыдущий код:

  • Добавляет гиперссылки в заголовки столбцов LastName и EnrollmentDate.
  • Использует эти сведения в NameSort и DateSort для настройки гиперссылок с использованием текущих значений порядка сортировки.
  • Изменяет заголовок страницы с Index (Индекс) на Students (Учащиеся).
  • Изменяет Model.Student на Model.Students.

Чтобы проверить работу сортировки, сделайте следующее:

  • Запустите приложение и откройте вкладку Students (Учащиеся).
  • Щелкните заголовки столбцов.

Добавление фильтрации

Для добавления фильтрации на страницу индекса учащихся:

  • На страницу Razor добавляется текстовое поле и кнопка отправки. Текстовое поле предоставляет строку поиска для имени или фамилии.
  • Страничная модель обновляется для использования значения из текстового поля.

Обновление метода OnGetAsync

Замените код в Students/Index.cshtml.cs следующем коде, чтобы добавить фильтрацию:

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

Предыдущий код:

  • Добавляет параметр searchString в метод OnGetAsync и сохраняет значение параметра в свойстве CurrentFilter. Значение строки поиска получается из текстового поля, добавляемого в следующем разделе.
  • Добавляет в инструкцию LINQ предложение Where. Это предложение Where отбирает только учащихся, чье имя или фамилия содержат строку поиска. Оператор LINQ выполняется, только если задано значение для поиска.

IQueryable vs. IEnumerable

Код вызывает метод Where объекта IQueryable, при этом фильтр обрабатывается на сервере. В некоторых случаях приложение может вызывать метод Where как метод расширения для коллекции в памяти. Например, предположим _context.Students , что изменения из EF CoreDbSet метода репозитория, возвращающего коллекцию IEnumerable . Обычно результат остается прежним, но в некоторых случаях он может отличаться.

Например, реализация Contains в .NET Framework по умолчанию выполняет сравнение с учетом регистра. В SQL Server Contains учет регистра определяется параметрами сортировки у экземпляра SQL Server. По умолчанию SQL Server не учитывает регистр. В SQLite по умолчанию регистр учитывается. Можно вызвать ToUpper, чтобы явно велеть тесту не учитывать регистр.

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

Приведенный выше код гарантирует, что фильтр не учитывает регистр, даже если метод Where вызывается для IEnumerable или выполняется в SQLite.

Когда Contains вызывается для коллекции IEnumerable, используется реализация .NET Core. Когда Contains вызывается для объекта IQueryable, используется реализация базы данных.

Вызов Contains для IQueryable обычно предпочтительнее по соображениям производительности. При использовании IQueryable фильтрация выполняется сервером базы данных. Если сначала создается IEnumerable, все строки должны возвращаться с сервера базы данных.

Вызов ToUpper снижает производительность. Код ToUpper добавляет функцию в предложение WHERE TSQL-оператора SELECT. Добавленная функция не позволяет оптимизатору использовать индекс. Учитывая, что SQL устанавливается без учета регистра, рекомендуется не использовать вызов ToUpper, когда он не требуется.

Дополнительные сведения см. в статье Использование запроса без учета регистра с поставщиком SQLite.

Обновление страницы Razor

Замените код, Pages/Students/Index.cshtml чтобы добавить кнопку поиска .

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

Для добавления кнопки и поля поиска предыдущий код использует вспомогательную функцию тегов<form>. По умолчанию вспомогательная функция тегов <form> отправляет данные формы с помощью POST. При этом параметры передаются в тексте сообщения HTTP, а не в URL-адресе. При использовании HTTP GET данные формы передаются в виде строк запроса в URL-адресе. Передача данных со строками запроса позволяет пользователям добавлять URL-адрес в закладки. Руководства консорциума W3C рекомендуют использовать GET, когда действие не приводит к обновлению.

Проверьте работу приложения:

  • Выберите вкладку Students (Учащиеся) и введите строку поиска. При использовании SQLite фильтр не учитывает регистр, только если вы реализовали необязательный код ToUpper, приведенный ранее.

  • Нажмите Поиск.

Обратите внимание, что URL-адрес содержит строку поиска. Например:

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

Если страница добавлена в закладки, закладка содержит URL-адрес страницы и строку запроса SearchString. Формирование строки запроса обеспечивает method="get" в теге form.

Сейчас при выборе ссылки сортировки заголовка столбца теряется значение фильтра в поле Search (Поиск). Потерянное значение фильтра исправляется в следующем разделе.

Добавление разбиения по страницам

В этом разделе создается класс PaginatedList для поддержки разбиения на страницы. Класс PaginatedList использует операторы Skip и Take для фильтрации данных на сервере вместо того, чтобы извлекать все строки таблицы. На следующем рисунке показаны кнопки перелистывания.

Students index page with paging links

Создание класса PaginatedList

В папке проекта создайте файл PaginatedList.cs со следующим кодом:

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

В предыдущем коде метод CreateAsync принимает размер и номер страницы и вызывает соответствующие методы Skip и Take объекта IQueryable. Метод ToListAsync объекта IQueryable при вызове возвращает список, содержащий только запрошенную страницу. Для включения и отключения кнопок перелистывания страниц Previous (Назад) и Next (Далее) используются свойства HasPreviousPage и HasNextPage.

Метод CreateAsync используется для создания PaginatedList<T>. Конструктор не позволяет создать объект PaginatedList<T>, так как конструкторы не могут выполнять асинхронный код.

Добавление размера страницы в конфигурацию

Добавьте PageSize в файл appsettings.jsonконфигурации:

{
  "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"
  }
}

Добавление разбиения по страницам в IndexModel

Замените код, Students/Index.cshtml.cs чтобы добавить разбиение по страницам.

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

Предыдущий код:

  • изменяет тип свойства Students с IList<Student> на PaginatedList<Student>;
  • добавляет индекс страницы, текущий порядок sortOrder и фильтр currentFilter в сигнатуру метода OnGetAsync;
  • сохраняет порядок сортировки в свойстве CurrentSort;
  • сбрасывает индекс страницы в значение 1 при получении новой строки поиска;
  • использует класс PaginatedList для получения сущностей Student.
  • Задает для pageSize значение 3 из файла конфигурации или 4, если настройка завершается сбоем.

Все параметры, получаемые методом OnGetAsync, равны NULL, когда:

  • Страница вызывается по ссылке Students (Учащиеся).
  • Пользователь не открывал ссылку перелистывания или сортировки.

При выборе ссылки перелистывания переменная индекса страницы содержит номер страницы для отображения.

Свойство CurrentSort предоставляет странице Razor текущий порядок сортировки. Текущий порядок сортировки нужно включить в ссылки перелистывания, чтобы сохранить его при смене страницы.

Свойство CurrentFilter предоставляет странице Razor текущую строку фильтра. Значение CurrentFilter:

  • Нужно включить в ссылки для перелистывания, чтобы сохранить параметры фильтра при смене страницы.
  • Нужно восстановить в текстовом поле после обновления страницы.

Если строка поиска изменяется во время перелистывания, номер страницы сбрасывается в значение 1. Номер страницы должен быть сброшен на 1, так как с новым фильтром может измениться состав отображаемых данных. Если введено значение для поиска и нажата кнопка Submit (Отправить):

  • Строка поиска изменяется.
  • Значение параметра searchString отличается от null.

Метод PaginatedList.CreateAsync преобразует результат запроса учащихся в отдельную страницу коллекции, поддерживающую разбиение на страницы. Эта страница с учащимися передается на страницу Razor.

Два вопросительных знака после pageIndex в вызове PaginatedList.CreateAsync являются оператором объединения с NULL. Оператор объединения с null определяет значение по умолчанию для типа, допускающего значение null. Выражение pageIndex ?? 1 возвращает значение свойства pageIndex, если оно есть. В противном случае возвращается значение 1.

Замените код в Students/Index.cshtml следующим: Изменения выделены:

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

Ссылки в заголовках столбцов передают текущую строку поиска в метод OnGetAsync с помощью строки запроса:

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

Запустите приложение и перейдите на страницу учащихся.

  • Чтобы убедиться, что разбиение на страницы работает, нажимайте кнопки перелистывания при различном порядке сортировки.
  • Чтобы убедиться, что разбиение на страницы работает корректно вместе с сортировкой и фильтрацией, введите строку поиска и попробуйте перелистнуть страницу.

students index page with paging links

Группировка

В этом разделе показано, как создать страницу общих сведений About, на которой показано количество учащихся, зарегистрированных на каждую дату. Это изменение использует группирование и включает следующие шаги:

  • Создайте модель представления для данных, используемых страницей About .
  • Обновите страницу About для использования модели представления.

Создание модели представления

Создайте папку Models/SchoolViewModels.

Создайте SchoolViewModels/EnrollmentDateGroup.cs, используя следующий код:

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

Создание Razor страницы

Создайте файл Pages/About.cshtml со следующим кодом:

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

Создание модели страницы

Pages/About.cshtml.cs Обновите файл со следующим кодом:

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

Запрос LINQ группирует записи из таблицы студентов по дате зачисления, вычисляет число записей в каждой группе и сохраняет результаты в коллекцию объектов моделей представления EnrollmentDateGroup.

Запустите приложение и перейдите на страницу "About" (О программе). Количество зачисленных студентов по дням отображается в таблице.

About page

Следующие шаги

В следующем руководстве приложение использует миграции для обновления модели данных.

Это руководство описывает добавление функций сортировки, фильтрации, группировки и разбиения на страницы.

На следующем рисунке показана готовая страница. Заголовки столбцов являются ссылками, щелкнув которые, можно отсортировать столбец. Многократно щелкая заголовок столбца, можно переключаться между сортировкой по возрастанию и убыванию.

Students index page

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение.

Добавление сортировки на страницу индекса

Добавьте строки в файл Students/Index.cshtml.csPageModel для хранения параметров сортировки:

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

Измените Students/Index.cshtml.csOnGetAsync, используя следующий код:

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

Предыдущий код принимает параметр sortOrder из строки запроса в URL-адресе. URL-адрес (включая строку запроса) формируется вспомогательной функцией тегов привязки

Параметр sortOrder имеет значение "Name" или "Date". После параметра sortOrder может стоять "_desc", чтобы указать порядок по убыванию. По умолчанию задан порядок сортировки по возрастанию.

При запросе страницы "Index" по ссылке Students строка запроса отсутствует. Учащиеся отображаются по фамилии в порядке возрастания. В операторе switch сортировка по фамилии в порядке возрастания используется по умолчанию. Когда пользователь щелкает ссылку заголовка столбца, в строке запроса указывается соответствующее значение sortOrder.

Для формирования гиперссылок в заголовках столбцов страница Razor использует NameSort и DateSort с соответствующими значениями строки запроса:

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

Следующий код содержит условный оператор ?: C#:

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

Первая строка указывает, что когда sortOrder равен null или пуст, NameSort имеет значение "name_desc". Если sortOrderне является NULL или пустым, для NameSort задается пустая строка.

?: operator также называется тернарным оператором.

Следующие два оператора устанавливают гиперссылки в заголовках столбцов на странице следующим образом:

Текущий порядок сортировки Гиперссылка "Last Name" (Фамилия) Гиперссылка "Date" (Дата)
"Last Name" (Фамилия) по возрастанию по убыванию ascending
"Last Name" (Фамилия) по убыванию ascending ascending
"Date" (Дата) по возрастанию ascending по убыванию
"Date" (Дата) по убыванию ascending ascending

Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities. Код инициализирует IQueryable<Student> до оператора 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();
}

При создании или изменении IQueryable запрос в базу данных не отправляется. Запрос не выполнится, пока объект IQueryable не будет преобразован в коллекцию. IQueryable преобразуются в коллекцию путем вызова метода, такого как ToListAsync. Таким образом, код IQueryable создает одиночный запрос, который не выполняется до следующего оператора:

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

OnGetAsync можно расширить на случай большого числа сортируемых столбцов.

Замените код в Students/Index.cshtmlследующем выделенном коде:

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

Предыдущий код:

  • Добавляет гиперссылки в заголовки столбцов LastName и EnrollmentDate.
  • Использует эти сведения в NameSort и DateSort для настройки гиперссылок с использованием текущих значений порядка сортировки.

Чтобы проверить работу сортировки, сделайте следующее:

  • Запустите приложение и откройте вкладку Students (Учащиеся).
  • Щелкните Last Name (Фамилия).
  • Щелкните Enrollment Date (Дата зачисления).

Чтобы лучше понять код:

  • В Students/Index.cshtml.cs задайте точку останова в switch (sortOrder).
  • Добавьте контрольное значение для NameSort и DateSort.
  • В Students/Index.cshtml задайте точку останова в @Html.DisplayNameFor(model => model.Student[0].LastName).

Осуществите пошаговое выполнение в отладчике.

Добавление поля поиска на страницу указателя учащихся

Для добавления фильтрации на страницу индекса учащихся:

  • На страницу Razor добавляется текстовое поле и кнопка отправки. Текстовое поле предоставляет строку поиска для имени или фамилии.
  • Страничная модель обновляется для использования значения из текстового поля.

Добавление функций фильтрации в метод Index

Измените Students/Index.cshtml.csOnGetAsync, используя следующий код:

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

Предыдущий код:

  • Добавляет параметр searchString в метод OnGetAsync. Значение строки поиска получается из текстового поля, добавляемого в следующем разделе.
  • Добавил в оператор LINQ предложение Where. Это предложение Where отбирает только учащихся, чье имя или фамилия содержат строку поиска. Оператор LINQ выполняется, только если задано значение для поиска.

Примечание. Предыдущий код вызывает метод Where объекта IQueryable, при этом фильтр обрабатывается на сервере. В некоторых случаях приложение может вызывать метод Where как метод расширения для коллекции в памяти. Например, предположим _context.Students , что изменения из EF CoreDbSet метода репозитория, возвращающего коллекцию IEnumerable . Обычно результат остается прежним, но в некоторых случаях он может отличаться.

Например, реализация Contains в .NET Framework по умолчанию выполняет сравнение с учетом регистра. В SQL Server Contains учет регистра определяется параметрами сортировки у экземпляра SQL Server. По умолчанию SQL Server не учитывает регистр. Можно вызвать ToUpper, чтобы явно велеть тесту не учитывать регистр.

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

Предыдущий код обеспечивает, что результаты не учитывают регистр, если код изменяется для использования IEnumerable. Когда Contains вызывается для коллекции IEnumerable, используется реализация .NET Core. Когда Contains вызывается для объекта IQueryable, используется реализация базы данных. При возвращение IEnumerable из репозитория производительность может значительно снижаться:

  1. Все строки возвращаются с сервера базы данных.
  2. Фильтр применяется ко всем возвращенным строкам в приложении.

Вызов ToUpper снижает производительность. Код ToUpper добавляет функцию в предложение WHERE TSQL-оператора SELECT. Добавленная функция не позволяет оптимизатору использовать индекс. Учитывая, что SQL устанавливается без учета регистра, рекомендуется не использовать вызов ToUpper, когда он не требуется.

Добавление поля поиска на страницу индексов учащихся

В Pages/Students/Index.cshtmlполе добавьте следующий выделенный код, чтобы создать кнопку поиска и хром.

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

Для добавления кнопки и поля поиска предыдущий код использует вспомогательную функцию тегов<form>. По умолчанию вспомогательная функция тегов <form> отправляет данные формы с помощью POST. При этом параметры передаются в тексте сообщения HTTP, а не в URL-адресе. При использовании HTTP GET данные формы передаются в виде строк запроса в URL-адресе. Передача данных со строками запроса позволяет пользователям добавлять URL-адрес в закладки. Руководства консорциума W3C рекомендуют использовать GET, когда действие не приводит к обновлению.

Проверьте работу приложения:

  • Выберите вкладку Students (Учащиеся) и введите строку поиска.
  • Нажмите Поиск.

Обратите внимание, что URL-адрес содержит строку поиска.

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

Если страница добавлена в закладки, закладка содержит URL-адрес страницы и строку запроса SearchString. Формирование строки запроса обеспечивает method="get" в теге form.

Сейчас при выборе ссылки сортировки заголовка столбца теряется значение фильтра в поле Search (Поиск). Потерянное значение фильтра исправляется в следующем разделе.

Добавление на страницу указателя учащихся разбиения на страницы

В этом разделе создается класс PaginatedList для поддержки разбиения на страницы. Класс PaginatedList использует операторы Skip и Take для фильтрации данных на сервере вместо того, чтобы извлекать все строки таблицы. На следующем рисунке показаны кнопки перелистывания.

Students index page with paging links

В папке проекта создайте файл PaginatedList.cs со следующим кодом:

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

В предыдущем коде метод CreateAsync принимает размер и номер страницы и вызывает соответствующие методы Skip и Take объекта IQueryable. Метод ToListAsync объекта IQueryable при вызове возвращает список, содержащий только запрошенную страницу. Для включения и отключения кнопок перелистывания страниц Previous (Назад) и Next (Далее) используются свойства HasPreviousPage и HasNextPage.

Метод CreateAsync используется для создания PaginatedList<T>. Конструктор не позволяет создать объектPaginatedList<T>, так как конструкторы не могут выполнять асинхронный код.

Добавление разбиения на страницы в метод Index

В Students/Index.cshtml.cs обновите тип Student с IList<Student> на PaginatedList<Student>:

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

Измените Students/Index.cshtml.csOnGetAsync, используя следующий код:

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

Предыдущий код добавляет страницу индекса, текущий sortOrder и currentFilter в сигнатуру метода.

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

Все параметры равны null, когда:

  • Страница вызывается по ссылке Students (Учащиеся).
  • Пользователь не открывал ссылку перелистывания или сортировки.

При выборе ссылки перелистывания переменная индекса страницы содержит номер страницы для отображения.

CurrentSort предоставляет странице Razor текущий порядок сортировки. Текущий порядок сортировки нужно включить в ссылки перелистывания, чтобы сохранить его при смене страницы.

CurrentFilter предоставляет странице Razor текущую строку фильтра. Значение CurrentFilter:

  • Нужно включить в ссылки для перелистывания, чтобы сохранить параметры фильтра при смене страницы.
  • Нужно восстановить в текстовом поле после обновления страницы.

Если строка поиска изменяется во время перелистывания, номер страницы сбрасывается в значение 1. Номер страницы должен быть сброшен на 1, так как с новым фильтром может измениться состав отображаемых данных. Если введено значение для поиска и нажата кнопка Submit (Отправить):

  • Строка поиска изменяется.
  • Значение параметра searchString отличается от null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

Метод PaginatedList.CreateAsync преобразует результат запроса учащихся в отдельную страницу коллекции, поддерживающую разбиение на страницы. Эта страница с учащимися передается на страницу Razor.

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

Два вопросительных знака в PaginatedList.CreateAsync являются оператором объединения с null. Оператор объединения с null определяет значение по умолчанию для типа, допускающего значение null. Выражение (pageIndex ?? 1) означает возвращение значения pageIndex, если он имеет значение. Если у pageIndex нет значения, возвращается 1.

Обновите разметку в Students/Index.cshtml. Изменения выделены:

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

Ссылки в заголовках столбцов передают в метод OnGetAsync с помощью строки запроса текущее значение строки поиска, чтобы пользователь мог сортировать отфильтрованные результаты:

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

Запустите приложение и перейдите на страницу учащихся.

  • Чтобы убедиться, что разбиение на страницы работает, нажимайте кнопки перелистывания при различном порядке сортировки.
  • Чтобы убедиться, что разбиение на страницы работает корректно вместе с сортировкой и фильтрацией, введите строку поиска и попробуйте перелистнуть страницу.

students index page with paging links

Чтобы лучше понять код:

  • В Students/Index.cshtml.cs задайте точку останова в switch (sortOrder).
  • Добавьте контрольное значение для NameSort, DateSort, CurrentSort и Model.Student.PageIndex.
  • В Students/Index.cshtml задайте точку останова в @Html.DisplayNameFor(model => model.Student[0].LastName).

Осуществите пошаговое выполнение в отладчике.

Изменение страницы "About" (О программе) для отображения статистики учащихся

На этом шаге обновляется, чтобы отобразить количество учащихся, Pages/About.cshtml зарегистрированных для каждой даты регистрации. Это изменение использует группирование и включает следующие шаги:

  • Создание модели представления для данных, используемых страницей About (О программе).
  • Обновление страницы "About" (О программе) для использования модели представления.

Создание модели представления

Создайте папку SchoolViewModels в папке Models.

В папке SchoolViewModels добавьте EnrollmentDateGroup.cs следующий код:

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

Обновление модели страницы "About" (О программе)

Веб-шаблоны в ASP.NET Core 2.2 не включают страницу About. Если вы используете ASP.NET Core 2.2, создайте страницу About Razor Page.

Pages/About.cshtml.cs Обновите файл со следующим кодом:

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

Запрос LINQ группирует записи из таблицы студентов по дате зачисления, вычисляет число записей в каждой группе и сохраняет результаты в коллекцию объектов моделей представления EnrollmentDateGroup.

Изменение страницы Razor About

Замените код в файле Pages/About.cshtml следующим кодом:

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

Запустите приложение и перейдите на страницу "About" (О программе). Количество зачисленных студентов по дням отображается в таблице.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение для этого этапа.

About page

Дополнительные ресурсы

В следующем руководстве приложение использует миграции для обновления модели данных.