Razor Pages с EF Core в ASP.NET Core — сортировка, фильтрация, разбиение на страницы — 3 из 8Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging - 3 of 8

Авторы: Том Дайкстра (Tom Dykstra), Рик Андерсон (Rick Anderson) и Йон П. Смит (Jon P Smith)By Tom Dykstra, Rick Anderson, and Jon P Smith

Веб-приложение университета Contoso демонстрирует создание веб-приложений Razor Pages с использованием EF Core и Visual Studio.The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. Сведения о серии руководств см. в первом руководстве серии.For information about the tutorial series, see the first tutorial.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.If you run into problems you can't solve, download the completed app and compare that code to what you created by following the tutorial.

В этом учебнике вы добавите на страницу учащихся функции сортировки, фильтрации и разбиения на страницы.This tutorial adds sorting, filtering, and paging functionality to the Students pages.

На следующем рисунке показана готовая страница.The following illustration shows a completed page. Заголовки столбцов являются ссылками, щелкнув которые, можно отсортировать столбец.The column headings are clickable links to sort the column. Щелкайте заголовок столбца для переключения между сортировкой по возрастанию и убыванию.Click a column heading repeatedly to switch between ascending and descending sort order.

Страница указателя учащихся

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

Чтобы добавить сортировку, замените код в файле Pages/Students/Index.cshtml.cs на приведенный ниже.Replace the code in Pages/Students/Index.cshtml.cs with the following code to add sorting.

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

namespace ContosoUniversity.Pages.Students
{
    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)
        {
            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();
        }
    }
}

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

  • добавляет свойства, которые будут содержать параметры сортировки;Adds properties to contain the sorting parameters.
  • изменяет имя свойства Student на Students;Changes the name of the Student property to Students.
  • заменяет код в методе OnGetAsync.Replaces the code in the OnGetAsync method.

Метод OnGetAsync принимает параметр sortOrder из строки запроса в URL-адресе.The OnGetAsync method receives a sortOrder parameter from the query string in the URL. URL-адрес (включая строку запроса) формируется вспомогательной функцией тегов привязки.The URL (including the query string) is generated by the Anchor Tag Helper.

Параметр sortOrder имеет значение "Name" или "Date".The sortOrder parameter is either "Name" or "Date." После параметра sortOrder может стоять "_desc", чтобы указать порядок по убыванию.The sortOrder parameter is optionally followed by "_desc" to specify descending order. По умолчанию используется порядок сортировки по возрастанию.The default sort order is ascending.

При запросе страницы "Index" по ссылке Students строка запроса отсутствует.When the Index page is requested from the Students link, there's no query string. Учащиеся отображаются по фамилии в порядке возрастания.The students are displayed in ascending order by last name. В операторе switch сортировка по фамилии в порядке возрастания используется по умолчанию.Ascending order by last name is the default (fall-through case) in the switch statement. Когда пользователь щелкает ссылку заголовка столбца, в строке запроса указывается соответствующее значение sortOrder.When the user clicks a column heading link, the appropriate sortOrder value is provided in the query string value.

Для формирования гиперссылок в заголовках столбцов страница Razor использует NameSort и DateSort с соответствующими значениями строки запроса:NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

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

В коде используется условный оператор C# ?:.The code uses the C# conditional operator ?:. Оператор ?: является тернарным (принимает три операнда).The ?: operator is a ternary operator (it takes three operands). Первая строка указывает, что когда sortOrder равен null или пуст, NameSort имеет значение "name_desc".The first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." Если sortOrderне является равным null или пустым, для NameSort задается пустая строка.If sortOrder is not null or empty, NameSort is set to an empty string.

Следующие два оператора устанавливают гиперссылки в заголовках столбцов на странице следующим образом:These two statements enable the page to set the column heading hyperlinks as follows:

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

Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities.The method uses LINQ to Entities to specify the column to sort by. Код инициализирует IQueryable<Student> до оператора switch и изменяет его в этом операторе:The code initializes an IQueryable<Student> before the switch statement, and modifies it in the switch statement:

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 запрос в базу данных не отправляется.When anIQueryable is created or modified, no query is sent to the database. Запрос не выполнится, пока объект IQueryable не будет преобразован в коллекцию.The query isn't executed until the IQueryable object is converted into a collection. IQueryable преобразуются в коллекцию путем вызова метода, такого как ToListAsync.IQueryable are converted to a collection by calling a method such as ToListAsync. Таким образом, код IQueryable создает одиночный запрос, который не выполняется до следующего оператора:Therefore, the IQueryable code results in a single query that's not executed until the following statement:

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

OnGetAsync можно расширить на случай большого числа сортируемых столбцов.OnGetAsync could get verbose with a large number of sortable columns. Сведения об альтернативном способе программирования этой функциональности см. в статье Использование динамических запросов LINQ для упрощения кода в версии этой серии учебников для MVC.For information about an alternative way to code this functionality, see Use dynamic LINQ to simplify code in the MVC version of this tutorial series.

Замените код в файле Students/Index.cshtml на приведенный ниже код.Replace the code in Students/Index.cshtml, with the following code. Изменения выделены.The changes are highlighted.

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

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

  • Добавляет гиперссылки в заголовки столбцов LastName и EnrollmentDate.Adds hyperlinks to the LastName and EnrollmentDate column headings.
  • Использует эти сведения в NameSort и DateSort для настройки гиперссылок с использованием текущих значений порядка сортировки.Uses the information in NameSort and DateSort to set up hyperlinks with the current sort order values.
  • Изменяет заголовок страницы с Index (Индекс) на Students (Учащиеся).Changes the page heading from Index to Students.
  • Изменяет Model.Student на Model.Students.Changes Model.Student to Model.Students.

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

  • Запустите приложение и откройте вкладку Students (Учащиеся).Run the app and select the Students tab.
  • Щелкните заголовки столбцов.Click the column headings.

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

Для добавления фильтрации на страницу индекса учащихся:To add filtering to the Students Index page:

  • На страницу Razor добавляется текстовое поле и кнопка отправки.A text box and a submit button is added to the Razor Page. Текстовое поле предоставляет строку поиска для имени или фамилии.The text box supplies a search string on the first or last name.
  • Страничная модель обновляется для использования значения из текстового поля.The page model is updated to use the text box value.

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

Чтобы добавить фильтрацию, замените код в файле Students/Index.cshtml.cs на приведенный ниже.Replace the code in Students/Index.cshtml.cs with the following code to add filtering:

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

namespace ContosoUniversity.Pages.Students
{
    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();
        }
    }
}

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

  • Добавляет параметр searchString в метод OnGetAsync и сохраняет значение параметра в свойстве CurrentFilter.Adds the searchString parameter to the OnGetAsync method, and saves the parameter value in the CurrentFilter property. Значение строки поиска получается из текстового поля, добавляемого в следующем разделе.The search string value is received from a text box that's added in the next section.
  • Добавляет в инструкцию LINQ предложение Where.Adds to the LINQ statement a Where clause. Это предложение Where отбирает только учащихся, чье имя или фамилия содержат строку поиска.The Where clause selects only students whose first name or last name contains the search string. Оператор LINQ выполняется, только если задано значение для поиска.The LINQ statement is executed only if there's a value to search for.

IQueryable и IEnumerableIQueryable vs. IEnumerable

Код вызывает метод Where объекта IQueryable, при этом фильтр обрабатывается на сервере.The code calls the Where method on an IQueryable object, and the filter is processed on the server. В некоторых случаях приложение может вызывать метод Where как метод расширения для коллекции в памяти.In some scenarios, the app might be calling the Where method as an extension method on an in-memory collection. Предположим, например, что _context.Students меняется с EF Core DbSet на метод репозитория, возвращающий коллекцию IEnumerable.For example, suppose _context.Students changes from EF Core DbSet to a repository method that returns an IEnumerable collection. Обычно результат остается прежним, но в некоторых случаях он может отличаться.The result would normally be the same but in some cases may be different.

Например, реализация Contains в .NET Framework по умолчанию выполняет сравнение с учетом регистра.For example, the .NET Framework implementation of Contains performs a case-sensitive comparison by default. В SQL Server Contains учет регистра определяется параметрами сортировки у экземпляра SQL Server.In SQL Server, Contains case-sensitivity is determined by the collation setting of the SQL Server instance. По умолчанию SQL Server не учитывает регистр.SQL Server defaults to case-insensitive. В SQLite по умолчанию регистр учитывается.SQLite defaults to case-sensitive. Можно вызвать ToUpper, чтобы явно велеть тесту не учитывать регистр.ToUpper could be called to make the test explicitly case-insensitive:

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

Приведенный выше код гарантирует, что фильтр не учитывает регистр, даже если метод Where вызывается для IEnumerable или выполняется в SQLite.The preceding code would ensure that the filter is case-insensitive even if the Where method is called on an IEnumerable or runs on SQLite.

Когда Contains вызывается для коллекции IEnumerable, используется реализация .NET Core.When Contains is called on an IEnumerable collection, the .NET Core implementation is used. Когда Contains вызывается для объекта IQueryable, используется реализация базы данных.When Contains is called on an IQueryable object, the database implementation is used.

Вызов Contains для IQueryable обычно предпочтительнее по соображениям производительности.Calling Contains on an IQueryable is usually preferable for performance reasons. При использовании IQueryable фильтрация выполняется сервером базы данных.With IQueryable, the filtering is done by the database server. Если сначала создается IEnumerable, все строки должны возвращаться с сервера базы данных.If an IEnumerable is created first, all the rows have to be returned from the database server.

Вызов ToUpper снижает производительность.There's a performance penalty for calling ToUpper. Код ToUpper добавляет функцию в предложение WHERE TSQL-оператора SELECT.The ToUpper code adds a function in the WHERE clause of the TSQL SELECT statement. Добавленная функция не позволяет оптимизатору использовать индекс.The added function prevents the optimizer from using an index. Учитывая, что SQL устанавливается без учета регистра, рекомендуется не использовать вызов ToUpper, когда он не требуется.Given that SQL is installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.

Дополнительные сведения см. в статье Использование запроса без учета регистра с поставщиком SQLite.For more information, see How to use case-insensitive query with Sqlite provider.

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

Замените код в файле Pages/Students/Index.cshtml, чтобы создать кнопку Search (Поиск) и различные элементы хрома.Replace the code in Pages/Students/Index.cshtml to create a Search button and assorted chrome.

@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> вспомогательную функцию тегов.The preceding code uses the <form> tag helper to add the search text box and button. По умолчанию вспомогательная функция тегов <form> отправляет данные формы с помощью POST.By default, the <form> tag helper submits form data with a POST. При этом параметры передаются в тексте сообщения HTTP, а не в URL-адресе.With POST, the parameters are passed in the HTTP message body and not in the URL. При использовании HTTP GET данные формы передаются в виде строк запроса в URL-адресе.When HTTP GET is used, the form data is passed in the URL as query strings. Передача данных со строками запроса позволяет пользователям добавлять URL-адрес в закладки.Passing the data with query strings enables users to bookmark the URL. Руководства консорциума W3C рекомендуют использовать GET, когда действие не приводит к обновлению.The W3C guidelines recommend that GET should be used when the action doesn't result in an update.

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

  • Выберите вкладку Students (Учащиеся) и введите строку поиска.Select the Students tab and enter a search string. При использовании SQLite фильтр не учитывает регистр, только если вы реализовали необязательный код ToUpper, приведенный ранее.If you're using SQLite, the filter is case-insensitive only if you implemented the optional ToUpper code shown earlier.

  • Выберите Search (Поиск).Select Search.

Обратите внимание, что URL-адрес содержит строку поиска.Notice that the URL contains the search string. Пример:For example:

https://localhost:<port>/Students?SearchString=an

Если страница добавлена в закладки, закладка содержит URL-адрес страницы и строку запроса SearchString.If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. Формирование строки запроса обеспечивает method="get" в теге form.The method="get" in the form tag is what caused the query string to be generated.

Сейчас при выборе ссылки сортировки заголовка столбца теряется значение фильтра в поле Search (Поиск).Currently, when a column heading sort link is selected, the filter value from the Search box is lost. Потерянное значение фильтра исправляется в следующем разделе.The lost filter value is fixed in the next section.

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

В этом разделе создается класс PaginatedList для поддержки разбиения на страницы.In this section, a PaginatedList class is created to support paging. Класс PaginatedList использует операторы Skip и Take для фильтрации данных на сервере вместо того, чтобы извлекать все строки таблицы.The PaginatedList class uses Skip and Take statements to filter data on the server instead of retrieving all rows of the table. На следующем рисунке показаны кнопки перелистывания.The following illustration shows the paging buttons.

Страница указателя учащихся со ссылками для перелистывания

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

В папке проекта создайте файл PaginatedList.cs со следующим кодом:In the project folder, create PaginatedList.cs with the following code:

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
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (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.The CreateAsync method in the preceding code takes page size and page number and applies the appropriate Skip and Take statements to the IQueryable. Метод ToListAsync объекта IQueryable при вызове возвращает список, содержащий только запрошенную страницу.When ToListAsync is called on the IQueryable, it returns a List containing only the requested page. Для включения и отключения кнопок перелистывания страниц Previous (Назад) и Next (Далее) используются свойства HasPreviousPage и HasNextPage.The properties HasPreviousPage and HasNextPage are used to enable or disable Previous and Next paging buttons.

Метод CreateAsync используется для создания PaginatedList<T>.The CreateAsync method is used to create the PaginatedList<T>. Конструктор не позволяет создать объект PaginatedList<T>, так как конструкторы не могут выполнять асинхронный код.A constructor can't create the PaginatedList<T> object; constructors can't run asynchronous code.

Добавление разбиения на страницы в класс PageModelAdd paging to the PageModel class

Чтобы добавить разбиение на страницы, замените код в файле Students/Index.cshtml.cs.Replace the code in Students/Index.cshtml.cs to add paging.

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

namespace ContosoUniversity.Pages.Students
{
    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 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;
            }

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

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

  • изменяет тип свойства Students с IList<Student> на PaginatedList<Student>;Changes the type of the Students property from IList<Student> to PaginatedList<Student>.
  • добавляет индекс страницы, текущий порядок sortOrder и фильтр currentFilter в сигнатуру метода OnGetAsync;Adds the page index, the current sortOrder, and the currentFilter to the OnGetAsync method signature.
  • сохраняет порядок сортировки в свойстве CurrentSort;Saves the sort order in the CurrentSort property.
  • сбрасывает индекс страницы в значение 1 при получении новой строки поиска;Resets page index to 1 when there's a new search string.
  • использует класс PaginatedList для получения сущностей Student.Uses the PaginatedList class to get Student entities.

Все параметры, получаемые методом OnGetAsync, равны NULL, когда:All the parameters that OnGetAsync receives are null when:

  • Страница вызывается по ссылке Students (Учащиеся).The page is called from the Students link.
  • Пользователь не открывал ссылку перелистывания или сортировки.The user hasn't clicked a paging or sorting link.

При выборе ссылки перелистывания переменная индекса страницы содержит номер страницы для отображения.When a paging link is clicked, the page index variable contains the page number to display.

Свойство CurrentSort предоставляет странице Razor текущий порядок сортировки.The CurrentSort property provides the Razor Page with the current sort order. Текущий порядок сортировки нужно включить в ссылки перелистывания, чтобы сохранить его при смене страницы.The current sort order must be included in the paging links to keep the sort order while paging.

Свойство CurrentFilter предоставляет странице Razor текущую строку фильтра.The CurrentFilter property provides the Razor Page with the current filter string. Значение CurrentFilter:The CurrentFilter value:

  • Нужно включить в ссылки для перелистывания, чтобы сохранить параметры фильтра при смене страницы.Must be included in the paging links in order to maintain the filter settings during paging.
  • Нужно восстановить в текстовом поле после обновления страницы.Must be restored to the text box when the page is redisplayed.

Если строка поиска изменяется во время перелистывания, номер страницы сбрасывается в значение 1.If the search string is changed while paging, the page is reset to 1. Номер страницы должен быть сброшен на 1, так как с новым фильтром может измениться состав отображаемых данных.The page has to be reset to 1 because the new filter can result in different data to display. Если введено значение для поиска и нажата кнопка Submit (Отправить):When a search value is entered and Submit is selected:

  • Строка поиска изменяется.The search string is changed.
  • Значение параметра searchString отличается от null.The searchString parameter isn't null.

Метод PaginatedList.CreateAsync преобразует результат запроса учащихся в отдельную страницу коллекции, поддерживающую разбиение на страницы.The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type that supports paging. Эта страница с учащимися передается на страницу Razor.That single page of students is passed to the Razor Page.

Два вопросительных знака после pageIndex в вызове PaginatedList.CreateAsync являются оператором объединения с NULL.The two question marks after pageIndex in the PaginatedList.CreateAsync call represent the null-coalescing operator. Оператор объединения с null определяет значение по умолчанию для типа, допускающего значение null.The null-coalescing operator defines a default value for a nullable type. Выражение (pageIndex ?? 1) означает возвращение значения pageIndex, если он имеет значение.The expression (pageIndex ?? 1) means return the value of pageIndex if it has a value. Если у pageIndex нет значения, возвращается 1.If pageIndex doesn't have a value, return 1.

Замените код в файле Students/Index.cshtml на приведенный ниже код.Replace the code in Students/Index.cshtml with the following code. Изменения выделены:The changes are highlighted:

@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 с помощью строки запроса:The column header links use the query string to pass the current search string to the OnGetAsync method:

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

Кнопки перелистывания отображаются вспомогательными функциями тегов:The paging buttons are displayed by tag helpers:


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

Запустите приложение и перейдите на страницу учащихся.Run the app and navigate to the students page.

  • Чтобы убедиться, что разбиение на страницы работает, нажимайте кнопки перелистывания при различном порядке сортировки.To make sure paging works, click the paging links in different sort orders.
  • Чтобы убедиться, что разбиение на страницы работает корректно вместе с сортировкой и фильтрацией, введите строку поиска и попробуйте перелистнуть страницу.To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

Страница указателя учащихся со ссылками для перелистывания

Добавление группированияAdd grouping

В этом разделе создается страница About (Сведения), на которой показано количество учащихся, зарегистрированных на каждую дату.This section creates an About page that displays how many students have enrolled for each enrollment date. Это изменение использует группирование и включает следующие шаги:The update uses grouping and includes the following steps:

  • Создание модели представления для данных, используемых страницей About (Сведения).Create a view model for the data used by the About page.
  • Обновление страницы "About" (О программе) для использования модели представления.Update the About page to use the view model.

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

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

Создайте файл SchoolViewModels/EnrollmentDateGroup.cs со следующим кодом:Create SchoolViewModels/EnrollmentDateGroup.cs with the following code:

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

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

Создайте файл Pages/About.cshtml со следующим кодом:Create a Pages/About.cshtml file with the following code:

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

Создание модели страницыCreate the page model

Создайте файл Pages/About.cshtml.cs со следующим кодом:Create a Pages/About.cshtml.cs file with the following code:

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.The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of EnrollmentDateGroup view model objects.

Запустите приложение и перейдите на страницу "About" (О программе).Run the app and navigate to the About page. Количество зачисленных студентов по дням отображается в таблице.The count of students for each enrollment date is displayed in a table.

Страница About

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

В следующем руководстве приложение использует миграции для обновления модели данных.In the next tutorial, the app uses migrations to update the data model.

Это руководство описывает добавление функций сортировки, фильтрации, группировки и разбиения на страницы.In this tutorial, sorting, filtering, grouping, and paging, functionality is added.

На следующем рисунке показана готовая страница.The following illustration shows a completed page. Заголовки столбцов являются ссылками, щелкнув которые, можно отсортировать столбец.The column headings are clickable links to sort the column. Многократно щелкая заголовок столбца, можно переключаться между сортировкой по возрастанию и убыванию.Clicking a column heading repeatedly switches between ascending and descending sort order.

Страница указателя учащихся

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение.If you run into problems you can't solve, download the completed app.

Добавление сортировки на страницу индексаAdd sorting to the Index page

Добавьте строки в файл Students/Index.cshtml.cs PageModel для хранения параметров сортировки:Add strings to the Students/Index.cshtml.cs PageModel to contain the sorting parameters:

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.cs OnGetAsync, используя следующий код:Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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-адресе.The preceding code receives a sortOrder parameter from the query string in the URL. URL-адрес (включая строку запроса) формируется вспомогательной функцией тегов привязкиThe URL (including the query string) is generated by the Anchor Tag Helper

Параметр sortOrder имеет значение "Name" или "Date".The sortOrder parameter is either "Name" or "Date." После параметра sortOrder может стоять "_desc", чтобы указать порядок по убыванию.The sortOrder parameter is optionally followed by "_desc" to specify descending order. По умолчанию используется порядок сортировки по возрастанию.The default sort order is ascending.

При запросе страницы "Index" по ссылке Students строка запроса отсутствует.When the Index page is requested from the Students link, there's no query string. Учащиеся отображаются по фамилии в порядке возрастания.The students are displayed in ascending order by last name. В операторе switch сортировка по фамилии в порядке возрастания используется по умолчанию.Ascending order by last name is the default (fall-through case) in the switch statement. Когда пользователь щелкает ссылку заголовка столбца, в строке запроса указывается соответствующее значение sortOrder.When the user clicks a column heading link, the appropriate sortOrder value is provided in the query string value.

Для формирования гиперссылок в заголовках столбцов страница Razor использует NameSort и DateSort с соответствующими значениями строки запроса:NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

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#:The following code contains the C# conditional ?: operator:

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

Первая строка указывает, что когда sortOrder равен null или пуст, NameSort имеет значение "name_desc".The first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." Если sortOrderне является равным null или пустым, для NameSort задается пустая строка.If sortOrder is not null or empty, NameSort is set to an empty string.

?: operator также называется тернарным оператором.The ?: operator is also known as the ternary operator.

Следующие два оператора устанавливают гиперссылки в заголовках столбцов на странице следующим образом:These two statements enable the page to set the column heading hyperlinks as follows:

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

Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities.The method uses LINQ to Entities to specify the column to sort by. Код инициализирует IQueryable<Student> до оператора switch и изменяет его в этом операторе:The code initializes an IQueryable<Student> before the switch statement, and modifies it in the switch statement:

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 запрос в базу данных не отправляется.When anIQueryable is created or modified, no query is sent to the database. Запрос не выполнится, пока объект IQueryable не будет преобразован в коллекцию.The query isn't executed until the IQueryable object is converted into a collection. IQueryable преобразуются в коллекцию путем вызова метода, такого как ToListAsync.IQueryable are converted to a collection by calling a method such as ToListAsync. Таким образом, код IQueryable создает одиночный запрос, который не выполняется до следующего оператора:Therefore, the IQueryable code results in a single query that's not executed until the following statement:

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

OnGetAsync можно расширить на случай большого числа сортируемых столбцов.OnGetAsync could get verbose with a large number of sortable columns.

Замените код в файле Students/Index.cshtml на следующий выделенный код:Replace the code in Students/Index.cshtml, with the following highlighted code:

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

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

  • Добавляет гиперссылки в заголовки столбцов LastName и EnrollmentDate.Adds hyperlinks to the LastName and EnrollmentDate column headings.
  • Использует эти сведения в NameSort и DateSort для настройки гиперссылок с использованием текущих значений порядка сортировки.Uses the information in NameSort and DateSort to set up hyperlinks with the current sort order values.

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

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

Чтобы лучше понять код:To get a better understanding of the code:

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

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

Добавление поля поиска на страницу указателя учащихсяAdd a Search Box to the Students Index page

Для добавления фильтрации на страницу индекса учащихся:To add filtering to the Students Index page:

  • На страницу Razor добавляется текстовое поле и кнопка отправки.A text box and a submit button is added to the Razor Page. Текстовое поле предоставляет строку поиска для имени или фамилии.The text box supplies a search string on the first or last name.
  • Страничная модель обновляется для использования значения из текстового поля.The page model is updated to use the text box value.

Добавление функций фильтрации в метод IndexAdd filtering functionality to the Index method

Измените файл Students/Index.cshtml.cs OnGetAsync, используя следующий код:Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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

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

  • Добавляет параметр searchString в метод OnGetAsync.Adds the searchString parameter to the OnGetAsync method. Значение строки поиска получается из текстового поля, добавляемого в следующем разделе.The search string value is received from a text box that's added in the next section.
  • Добавил в оператор LINQ предложение Where.Added to the LINQ statement a Where clause. Это предложение Where отбирает только учащихся, чье имя или фамилия содержат строку поиска.The Where clause selects only students whose first name or last name contains the search string. Оператор LINQ выполняется, только если задано значение для поиска.The LINQ statement is executed only if there's a value to search for.

Примечание. Предыдущий код вызывает метод Where объекта IQueryable, при этом фильтр обрабатывается на сервере.Note: The preceding code calls the Where method on an IQueryable object, and the filter is processed on the server. В некоторых случаях приложение может вызывать метод Where как метод расширения для коллекции в памяти.In some scenarios, the app might be calling the Where method as an extension method on an in-memory collection. Предположим, например, что _context.Students меняется с EF Core DbSet на метод репозитория, возвращающий коллекцию IEnumerable.For example, suppose _context.Students changes from EF Core DbSet to a repository method that returns an IEnumerable collection. Обычно результат остается прежним, но в некоторых случаях он может отличаться.The result would normally be the same but in some cases may be different.

Например, реализация Contains в .NET Framework по умолчанию выполняет сравнение с учетом регистра.For example, the .NET Framework implementation of Contains performs a case-sensitive comparison by default. В SQL Server Contains учет регистра определяется параметрами сортировки у экземпляра SQL Server.In SQL Server, Contains case-sensitivity is determined by the collation setting of the SQL Server instance. По умолчанию SQL Server не учитывает регистр.SQL Server defaults to case-insensitive. Можно вызвать ToUpper, чтобы явно велеть тесту не учитывать регистр.ToUpper could be called to make the test explicitly case-insensitive:

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

Предыдущий код обеспечивает, что результаты не учитывают регистр, если код изменяется для использования IEnumerable.The preceding code would ensure that results are case-insensitive if the code changes to use IEnumerable. Когда Contains вызывается для коллекции IEnumerable, используется реализация .NET Core.When Contains is called on an IEnumerable collection, the .NET Core implementation is used. Когда Contains вызывается для объекта IQueryable, используется реализация базы данных.When Contains is called on an IQueryable object, the database implementation is used. При возвращение IEnumerable из репозитория производительность может значительно снижаться:Returning an IEnumerable from a repository can have a significant performance penalty:

  1. Все строки возвращаются с сервера базы данных.All the rows are returned from the DB server.
  2. Фильтр применяется ко всем возвращенным строкам в приложении.The filter is applied to all the returned rows in the application.

Вызов ToUpper снижает производительность.There's a performance penalty for calling ToUpper. Код ToUpper добавляет функцию в предложение WHERE TSQL-оператора SELECT.The ToUpper code adds a function in the WHERE clause of the TSQL SELECT statement. Добавленная функция не позволяет оптимизатору использовать индекс.The added function prevents the optimizer from using an index. Учитывая, что SQL устанавливается без учета регистра, рекомендуется не использовать вызов ToUpper, когда он не требуется.Given that SQL is installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.

Добавление поля поиска на страницу индексов учащихсяAdd a Search Box to the Student Index page

Добавьте в Pages/Student/Index.cshtml приведенный ниже выделенный код, чтобы создать кнопку Search и различные элементы хрома.In Pages/Students/Index.cshtml, add the following highlighted code to create a Search button and assorted chrome.

@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> вспомогательную функцию тегов.The preceding code uses the <form> tag helper to add the search text box and button. По умолчанию вспомогательная функция тегов <form> отправляет данные формы с помощью POST.By default, the <form> tag helper submits form data with a POST. При этом параметры передаются в тексте сообщения HTTP, а не в URL-адресе.With POST, the parameters are passed in the HTTP message body and not in the URL. При использовании HTTP GET данные формы передаются в виде строк запроса в URL-адресе.When HTTP GET is used, the form data is passed in the URL as query strings. Передача данных со строками запроса позволяет пользователям добавлять URL-адрес в закладки.Passing the data with query strings enables users to bookmark the URL. Руководства консорциума W3C рекомендуют использовать GET, когда действие не приводит к обновлению.The W3C guidelines recommend that GET should be used when the action doesn't result in an update.

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

  • Выберите вкладку Students (Учащиеся) и введите строку поиска.Select the Students tab and enter a search string.
  • Выберите Search (Поиск).Select Search.

Обратите внимание, что URL-адрес содержит строку поиска.Notice that the URL contains the search string.

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

Если страница добавлена в закладки, закладка содержит URL-адрес страницы и строку запроса SearchString.If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. Формирование строки запроса обеспечивает method="get" в теге form.The method="get" in the form tag is what caused the query string to be generated.

Сейчас при выборе ссылки сортировки заголовка столбца теряется значение фильтра в поле Search (Поиск).Currently, when a column heading sort link is selected, the filter value from the Search box is lost. Потерянное значение фильтра исправляется в следующем разделе.The lost filter value is fixed in the next section.

Добавление на страницу указателя учащихся разбиения на страницыAdd paging functionality to the Students Index page

В этом разделе создается класс PaginatedList для поддержки разбиения на страницы.In this section, a PaginatedList class is created to support paging. Класс PaginatedList использует операторы Skip и Take для фильтрации данных на сервере вместо того, чтобы извлекать все строки таблицы.The PaginatedList class uses Skip and Take statements to filter data on the server instead of retrieving all rows of the table. На следующем рисунке показаны кнопки перелистывания.The following illustration shows the paging buttons.

Страница указателя учащихся со ссылками для перелистывания

В папке проекта создайте файл PaginatedList.cs со следующим кодом:In the project folder, create PaginatedList.cs with the following code:

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
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (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.The CreateAsync method in the preceding code takes page size and page number and applies the appropriate Skip and Take statements to the IQueryable. Метод ToListAsync объекта IQueryable при вызове возвращает список, содержащий только запрошенную страницу.When ToListAsync is called on the IQueryable, it returns a List containing only the requested page. Для включения и отключения кнопок перелистывания страниц Previous (Назад) и Next (Далее) используются свойства HasPreviousPage и HasNextPage.The properties HasPreviousPage and HasNextPage are used to enable or disable Previous and Next paging buttons.

Метод CreateAsync используется для создания PaginatedList<T>.The CreateAsync method is used to create the PaginatedList<T>. Конструктор не позволяет создать объектPaginatedList<T>, так как конструкторы не могут выполнять асинхронный код.A constructor can't create the PaginatedList<T> object, constructors can't run asynchronous code.

Добавление разбиения на страницы в метод IndexAdd paging functionality to the Index method

В Students/Index.cshtml.cs измените тип Student с IList<Student> на PaginatedList<Student>:In Students/Index.cshtml.cs, update the type of Student from IList<Student> to PaginatedList<Student>:

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

Измените файл Students/Index.cshtml.cs OnGetAsync, используя следующий код:Update the Students/Index.cshtml.cs OnGetAsync with the following code:

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 в сигнатуру метода.The preceding code adds the page index, the current sortOrder, and the currentFilter to the method signature.

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

Все параметры равны null, когда:All the parameters are null when:

  • Страница вызывается по ссылке Students (Учащиеся).The page is called from the Students link.
  • Пользователь не открывал ссылку перелистывания или сортировки.The user hasn't clicked a paging or sorting link.

При выборе ссылки перелистывания переменная индекса страницы содержит номер страницы для отображения.When a paging link is clicked, the page index variable contains the page number to display.

CurrentSort предоставляет странице Razor текущий порядок сортировки.CurrentSort provides the Razor Page with the current sort order. Текущий порядок сортировки нужно включить в ссылки перелистывания, чтобы сохранить его при смене страницы.The current sort order must be included in the paging links to keep the sort order while paging.

CurrentFilter предоставляет странице Razor текущую строку фильтра.CurrentFilter provides the Razor Page with the current filter string. Значение CurrentFilter:The CurrentFilter value:

  • Нужно включить в ссылки для перелистывания, чтобы сохранить параметры фильтра при смене страницы.Must be included in the paging links in order to maintain the filter settings during paging.
  • Нужно восстановить в текстовом поле после обновления страницы.Must be restored to the text box when the page is redisplayed.

Если строка поиска изменяется во время перелистывания, номер страницы сбрасывается в значение 1.If the search string is changed while paging, the page is reset to 1. Номер страницы должен быть сброшен на 1, так как с новым фильтром может измениться состав отображаемых данных.The page has to be reset to 1 because the new filter can result in different data to display. Если введено значение для поиска и нажата кнопка Submit (Отправить):When a search value is entered and Submit is selected:

  • Строка поиска изменяется.The search string is changed.
  • Значение параметра searchString отличается от null.The searchString parameter isn't null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

Метод PaginatedList.CreateAsync преобразует результат запроса учащихся в отдельную страницу коллекции, поддерживающую разбиение на страницы.The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type that supports paging. Эта страница с учащимися передается на страницу Razor.That single page of students is passed to the Razor Page.

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

Два вопросительных знака в PaginatedList.CreateAsync являются оператором объединения с null.The two question marks in PaginatedList.CreateAsync represent the null-coalescing operator. Оператор объединения с null определяет значение по умолчанию для типа, допускающего значение null.The null-coalescing operator defines a default value for a nullable type. Выражение (pageIndex ?? 1) означает возвращение значения pageIndex, если он имеет значение.The expression (pageIndex ?? 1) means return the value of pageIndex if it has a value. Если у pageIndex нет значения, возвращается 1.If pageIndex doesn't have a value, return 1.

Измените разметку в Students/Index.cshtml.Update the markup in Students/Index.cshtml. Изменения выделены:The changes are highlighted:

@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 с помощью строки запроса текущее значение строки поиска, чтобы пользователь мог сортировать отфильтрованные результаты:The column header links use the query string to pass the current search string to the OnGetAsync method so that the user can sort within filter results:

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

Кнопки перелистывания отображаются вспомогательными функциями тегов:The paging buttons are displayed by tag helpers:


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

Запустите приложение и перейдите на страницу учащихся.Run the app and navigate to the students page.

  • Чтобы убедиться, что разбиение на страницы работает, нажимайте кнопки перелистывания при различном порядке сортировки.To make sure paging works, click the paging links in different sort orders.
  • Чтобы убедиться, что разбиение на страницы работает корректно вместе с сортировкой и фильтрацией, введите строку поиска и попробуйте перелистнуть страницу.To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

Страница указателя учащихся со ссылками для перелистывания

Чтобы лучше понять код:To get a better understanding of the code:

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

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

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

В этом шаге изменяется страница Pages/About.cshtml, чтобы отобразить количество зачисленных учащихся по дням.In this step, Pages/About.cshtml is updated to display how many students have enrolled for each enrollment date. Это изменение использует группирование и включает следующие шаги:The update uses grouping and includes the following steps:

  • Создание модели представления для данных, используемых страницей About (О программе).Create a view model for the data used by the About Page.
  • Обновление страницы "About" (О программе) для использования модели представления.Update the About page to use the view model.

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

Создайте папку SchoolViewModels в папке Models.Create a SchoolViewModels folder in the Models folder.

Добавьте в папку SchoolViewModels файл EnrollmentDateGroup.cs со следующим кодом:In the SchoolViewModels folder, add a EnrollmentDateGroup.cs with the following code:

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" (О программе)Update the About page model

Веб-шаблоны в ASP.NET Core 2.2 не включают страницу About.The web templates in ASP.NET Core 2.2 do not include the About page. Если вы используете ASP.NET Core 2.2, создайте страницу About Razor Page.If you are using ASP.NET Core 2.2, create the About Razor Page.

Измените файл Pages/About.cshtml.cs, используя следующий код:Update the Pages/About.cshtml.cs file with the following code:

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.The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of EnrollmentDateGroup view model objects.

Изменение страницы Razor "About" (О программе)Modify the About Razor Page

Замените код в файле Pages/About.cshtml следующим кодом:Replace the code in the Pages/About.cshtml file with the following code:

@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" (О программе).Run the app and navigate to the About page. Количество зачисленных студентов по дням отображается в таблице.The count of students for each enrollment date is displayed in a table.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение для этого этапа.If you run into problems you can't solve, download the completed app for this stage.

Страница About

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

В следующем руководстве приложение использует миграции для обновления модели данных.In the next tutorial, the app uses migrations to update the data model.