Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В предыдущем руководстве был создан набор веб-страниц для основных операций CRUD для сущностей Student. В этом руководстве вы добавите функции сортировки, фильтрации и разбиения на страницу "Индекс учащихся". Вы также создаете страницу, которая выполняет простое группирование.
В этом упражнении вы добавляете функции сортировки, фильтрации и разбиения на страницу "Индекс учащихся". Вы также создаете страницу, которая выполняет простую группировку. На следующем рисунке показано, как выглядит страница при завершении. Заголовки столбцов — это ссылки, которые пользователь может выбрать для сортировки по этим столбцам. Выбор заголовка столбца неоднократно приводит к переключению порядка сортировки между возрастаниями и убыванием.
Изучив это руководство, вы:
- Добавление ссылок для поддержки сортировки столбцов
- Добавление поля поиска для поддержки поиска
- Добавление разбиения по страницам в индекс учащихся
- Добавление ссылок для поддержки действия разбиения по страницам
- Создание страницы "Сведения" для сайта
Предварительные условия
- Выполните предыдущее руководство по реализации основных функций CRUD в ASP.NET MVC.
Добавление ссылок для сортировки столбцов
Чтобы добавить сортировку на страницу "Индекс учащихся ", измените Index метод контроллера учащихся и добавьте код в представление индекса учащихся.
Добавление функций сортировки в метод 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 передается как параметр метода действия. Параметр является строковым значением: "Имя" или "Дата". За значением может следовать символ подчеркивания и строка "desc", чтобы указать порядок убывания. По умолчанию задан порядок сортировки по возрастанию.
При первом запросе страницы индекса строка запроса отсутствует. Список студентов отсортирован по фамилиям по возрастанию, порядок сортировки по умолчанию задается в выражении 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". В противном случае элемент имеет пустую строку. Эти два оператора позволяют представлению устанавливать гиперссылки в заголовках столбцов следующим образом:
| Текущий порядок сортировки | Гиперссылка на фамилию | Гиперссылка на дату |
|---|---|---|
| Фамилия по возрастанию | Descending | По возрастанию |
| Фамилия по убыванию | По возрастанию | По возрастанию |
| Дата по возрастанию | По возрастанию | Descending |
| Дата по убыванию | По возрастанию | По возрастанию |
Для указания столбца, по которому выполняется сортировка, этот метод использует LINQ to Entities. Код создает IQueryable переменную перед switch оператором, изменяет ее в switch операторе и вызывает метод ToListAsync после оператора switch. После создания и изменения переменных IQueryable запрос в базу данных не отправляется. Запрос не выполнится, пока вы не преобразуете объект IQueryable в коллекцию, вызвав соответствующий метод, такой как ToListAsync. Поэтому этот код приводит к выполнению одного запроса, который не выполняется до тех пор, пока инструкция не будет выполнена return View .
Этот код может стать излишне многословным при работе с большим количеством столбцов. В последнем руководстве этой серии, Узнайте о расширенных сценариях > и используйте динамические выражения LINQ для упрощения кода, показано, как написать код, позволяющий передать имя столбца в строковой переменной.
Добавьте гиперссылки для заголовков столбцов в представлении индекса учащихся.
Замените код в файле 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 свойствах для настройки гиперссылок с соответствующими значениями строки запроса.
Запустите приложение, перейдите на вкладку "Учащиеся " и выберите заголовки столбцов "Фамилия " и " Дата регистрации ", чтобы проверить работу сортировки.
Добавление поля поиска
Чтобы добавить фильтрацию на страницу индекса учащихся , добавьте текстовое поле и кнопку "Отправить " в представление и внесите соответствующие изменения в 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());
}
Вы добавили searchString параметр в Index метод. Значение строки поиска получено из текстового поля, добавляемого в представление индекса. Вы также добавили where условие в LINQ запрос, который выбирает только тех учащихся, чьи имя или фамилия совпадают со строкой поиска. Инструкция, добавляющая where предложение, выполняется только в том случае, если есть значение, которое можно искать.
Определение способа использования метода Where
В этом сценарии вы вызываете метод Where у объекта IQueryable, и фильтр обрабатывается на сервере. В некоторых сценариях вы можете вызвать метод Where как метод расширения на коллекцию в памяти. Предположим, вы измените ссылку на _context.Students. Вместо вызова метода Entity Framework DbSet он ссылается на метод репозитория, который возвращает коллекцию IEnumerable . Результат обычно совпадает, но некоторые случаи могут отличаться.
Например, реализация метода Contains в .NET Framework выполняет сравнение с учетом регистра по умолчанию. В SQL Server определение происходит с помощью параметра сортировки экземпляра SQL Server. Параметр по умолчанию устанавливается как нечувствительный к регистру. Вы можете вызвать метод 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 добавьте выделенный код непосредственно перед открывающим тегом <table> таблицы, чтобы создать подпись, текстовое поле и кнопку поиска .
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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 если действие не приводит к обновлению.
Запустите приложение, перейдите на вкладку "Учащиеся ", введите строку поиска и выберите "Поиск ", чтобы убедиться, что фильтрация работает.
Обратите внимание, что URL-адрес содержит строку поиска.
http://localhost:5813/Students?SearchString=an
Если вы добавите эту страницу в закладки, то при её использовании получите отфильтрованный список. Добавление условия в тег method="get" приводит к созданию строки запроса form.
На этом этапе разработки при выборе ссылки сортировки заголовков столбцов вы потеряете значение фильтра, введенное в поле поиска . Вы узнаете, как устранить эту ситуацию в следующем разделе.
Добавление разбиения по страницам в индекс учащихся
Чтобы добавить разбиение на страницу "Индекс учащихся ", создайте PaginatedList класс, который использует Skip и Take операторы для фильтрации данных на сервере, а не всегда извлекает все строки таблицы. Затем вы вносите дополнительные изменения в Index метод и добавляете кнопки разбиения на страницы в Index представление. На следующем рисунке показаны кнопки разбиения по страницам.
В папке проекта создайте файл 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 метод возвращает List, содержащий только запрошенную страницу.
HasPreviousPage и HasNextPage свойства могут быть использованы для включения или отключения кнопок разбиения на страницы: Назад и Вперед.
Для создания объекта CreateAsync вместо конструктора используется метод PaginatedList<T>, поскольку конструкторы не могут выполнять асинхронный код.
Добавление разбиения по страницам в метод 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. Если выбрана ссылка на страницу, переменные страницы содержат номер страницы для отображения.
Элемент с ViewData именем CurrentSort предоставляет представление с текущим порядком сортировки. Это значение должно быть включено в ссылки на разбиение по страницам, чтобы порядок сортировки оставался неизменным при разбиении по страницам.
Элемент с ViewData именем CurrentFilter предоставляет представлению текущую строку фильтра. Это значение должно быть включено в ссылки на разбиение по страницам, чтобы сохранить параметры фильтра во время разбиения по страницам. Значение должно быть восстановлено обратно в текстовое поле при загрузке страницы.
Если строка поиска изменяется во время постраничной навигации, страница возвращается на первую позицию, так как новый фильтр может привести к отображению различных данных. Строка поиска изменяется при вводе значения в текстовое поле, а пользователь выбирает "Отправить". В этом сценарии 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 определяет значение по умолчанию для типа, допускающего значение 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>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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>
Запустите приложение и перейдите на страницу "Учащиеся ".
Выберите ссылки для постраничной навигации в разных порядках сортировки и убедитесь, что функция постраничного разбивания работает. Введите строку поиска и попробуйте разбиение на страницы снова, чтобы проверить правильность работы разбиения с сортировкой и фильтрацией.
Создание страницы "О нас"
На странице "Сведения о университете 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 объектов модели представления.
Создайте представление "О программе"
Добавьте файл 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>
Запустите приложение и перейдите на страницу "Сведения ". Количество зачисленных студентов по дням отображается в таблице.
Получение кода
Скачайте или просмотрите готовое приложение.
Следующий шаг
ASP.NET Core