Поделиться через


Руководство. Добавление сортировки, фильтрации и разбиения по страницам — ASP.NET MVC с помощью EF Core

В предыдущем руководстве был создан набор веб-страниц для основных операций CRUD для сущностей Student. Из этого руководства вы узнаете, как добавить на страницу указателя учащихся сортировку, фильтрацию и разбиение на страницы. Здесь также описывается создание страницы с простой группировкой.

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

Students index page

Изучив это руководство, вы:

  • Добавление ссылок для сортировки столбцов
  • Добавление поля поиска
  • Добавление разбиения по страницам в указатель учащихся
  • Добавление разбиения по страницам в метод Index
  • Добавление ссылок для разбиения по страницам
  • Создание страницы сведений

Необходимые компоненты

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

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

В StudentsController.cs замените метод Index следующим кодом:

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Этот код принимает параметр sortOrder из строки запроса в URL. Значение строки запроса в ASP.NET Core MVC передается как параметр метода действия. Имя параметра представляет собой строку, состоящую из "Name" или "Date" с возможным добавлением знака подчеркивания и строки "desc" для указания убывающего порядка сортировки. По умолчанию задан порядок сортировки по возрастанию.

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

Для формирования гиперссылок в заголовках столбцов в представлении используются два элемента ViewData (NameSortParm и DateSortParm) с соответствующими значениями строки запроса.

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Это тернарные условные операторы. Первое выражение означает, что если параметр sortOrder пуст или равен null, то параметр NameSortParm должен принять значение "name_desc", в противном случае параметру NameSortParm присваивается пустая строка. Следующие два оператора устанавливают гиперссылки в заголовках столбцов в представлении следующим образом:

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

Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities. Данный код создает переменную IQueryable перед оператором switch, изменяет ее значение внутри оператора switch и вызывает метод ToListAsync после switch. После создания и изменения переменных IQueryable запрос в базу данных не отправляется. Запрос не выполнится, пока вы не преобразуете объект IQueryable в коллекцию, вызвав соответствующий метод, такой как ToListAsync. Таким образом, этот код создает одиночный запрос, который не будет выполнен до выполнения выражения return View.

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

Замените код в Views/Students/Index.cshtmlследующем коде, чтобы добавить гиперссылки заголовков столбцов. Измененные строки выделены.

@model IEnumerable<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Для формирования гиперссылок с соответствующей строкой запроса этот код использует информацию из свойств ViewData.

Для проверки работы сортировки запустите приложение, выберите вкладку Students и нажимайте на заголовки столбцов Last Name и Enrollment Date.

Students index page in name order

Для добавления фильтра на страницу указателя учащихся необходимо добавить в представление текстовое поле и кнопку отправки и внести изменения в метод Index. Текстовое поле необходимо для ввода строки для поиска в полях имени и фамилии.

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

В файле StudentsController.cs замените метод Index следующим кодом (изменения выделены).

public async Task<IActionResult> Index(string sortOrder, string searchString)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Мы добавили в метод Index параметр searchString. Значение строки поиска получается из текстового поля, которое мы добавили в представление Index. Мы также добавили в запрос LINQ предложение where, которое отбирает только студентов, чье имя или фамилия содержат строку поиска. Выражение с предложением where выполняется только в том случае, если задано значение для поиска.

Примечание.

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

Например, в .NET Framework метод Contains по умолчанию выполняет сравнение с учетом регистра, а в SQL Server это определяется параметром сортировки конкретного экземпляра SQL сервера. По умолчанию параметр установлен на сравнение без учета регистра. Можно вызвать метод ToUpper, чтобы сделать сравнение явно регистронезависимым: Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()). Это гарантирует, что поведение программы не изменится, если вы измените код на использование репозитория, который возвращает IEnumerable, а не объект IQueryable. (При вызове Contains метода в IEnumerable коллекции вы получаете реализацию платформа .NET Framework; при вызове метода в IQueryable объекте вы получите реализацию поставщика базы данных.) Однако для этого решения существует штраф за производительность. Метод ToUpper добавляет функцию в предложение WHERE TSQL-выражения SELECT. Это не позволяет оптимизатору использовать индекс. Учитывая, что SQL обычно настраивается на то, чтобы не учитывать регистр, рекомендуется не использовать код ToUpper до миграции на хранилище данных, учитывающее регистр.

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

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

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

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

<table class="table">

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

Для проверки работы фильтра запустите приложение, выберите вкладку Students, введите строку поиска и нажмите Search.

Students index page with filtering

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

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

Если вы добавите эту страницу в закладки, то при открытии закладки будет открываться уже отфильтрованный список. Формирование строки запроса обеспечивает добавление method="get" в тег form.

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

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

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

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.

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

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

В StudentsController.cs замените метод Index следующим кодом:

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        pageNumber = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    ViewData["CurrentFilter"] = searchString;

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

    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}

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

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)

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

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

Элемент ViewData с именем CurrentFilter передает в представление текущую строку фильтра. Это значение необходимо включить в ссылки для перелистывания, чтобы при смене страницы сохранить настройки фильтра, кроме того, необходимо восстановить значение фильтра в текстовом поле после обновления страницы.

Если строка поиска изменяется во время перелистывания, то номер страницы должен быть сброшен на 1, так как с новым фильтром изменится состав отображаемых данных. Изменение строки поиска происходит при вводе в текстовое поле значения и нажатии на кнопку отправки. В этом случае значение параметра searchString не null.

if (searchString != null)
{
    pageNumber = 1;
}
else
{
    searchString = currentFilter;
}

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

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));

Метод PaginatedList.CreateAsync принимает номер страницы. Два вопросительных знака являются оператором объединения с null. Этот оператор определяет значение по умолчанию для значения null; выражение (pageNumber ?? 1) возвращает значение переменной pageNumber, если она имеет значение, и возвращает 1, если переменная pageNumber имеет значение null.

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

@model PaginatedList<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

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

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

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
                First Name
            </th>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

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

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</a>

Оператор @model в начале страницы указывает на то, что теперь представление принимает объект PaginatedList<T>, а не объект List<T>.

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

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>

Кнопки перелистывания отображаются вспомогательными функциями тегов:

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
   Previous
</a>

Запустите приложение и перейдите на страницу Students.

Students index page with paging links

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

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

На странице About веб-сайта "Университет Contoso" будет отображаться количество зачисленных студентов по дням. Для этого понадобится группировка и выполнение простых расчетов в группах. Для выполнения этой задачи нам потребуется следующее:

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

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

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

В новой папке добавьте файл 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; }
    }
}

Изменение контроллера Home

В HomeController.cs добавьте следующие операторы Using в верхнюю часть файла:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;

Добавьте переменную класса для контекста базы данных сразу же после открывающей фигурной скобки описания класса и получите экземпляр контекста из ASP.NET Core DI:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }

Добавьте метод About со следующим кодом.

public async Task<ActionResult> About()
{
    IQueryable<EnrollmentDateGroup> data = 
        from student in _context.Students
        group student by student.EnrollmentDate into dateGroup
        select new EnrollmentDateGroup()
        {
            EnrollmentDate = dateGroup.Key,
            StudentCount = dateGroup.Count()
        };
    return View(await data.AsNoTracking().ToListAsync());
}

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

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

Добавьте файл Views/Home/About.cshtml с помощью следующего кода:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
    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)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

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

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

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

Изучив это руководство, вы:

  • Добавление ссылок для сортировки столбцов
  • Добавление поля поиска
  • Добавление разбиения по страницам в указатель учащихся
  • Добавление разбиения по страницам в метод Index
  • Добавление ссылок для разбиения по страницам
  • Создание страницы сведений

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