Сортировка, фильтрация и разбиение по страницам с Entity Framework в приложении ASP.NET MVC
В предыдущем уроке мы реализовали страницы для совершения CRUD-операций для сущностей Student. В этом уроке мы добавим сортировку, фильтрацию и разбиение по страницам, а также создадим страницу, на которой будет простая группировка.
На следующем изображении представлен окончательный вид страницы. Заголовки столбцов являются ссылками, реализующими сортировку по убыванию и возрастанию.
Добавление заголовков-сортировщиков в столбцы на странице Students Index
Для добавления сортировки вам нужно изменить метод Index контроллера Student и добавить код на представление Student Index.
Добавление сортировки в метод Index
В Controllers\StudentController.cs замените метод Index на следующий код:
public ViewResult Index(string sortOrder)
{
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
var students = from s in db.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(students.ToList());
}
Метод принимает sortOrder как параметр из строки запроса в URL, который предоставляется ASP.NET как параметр для метода. Параметр является строкой «Name» или «Date» с (опционально) последующим пробелом и строкой “desc” для указания того, что необходимо сортировать по убыванию.
При первом вызове страницы Index строки запроса нет, и студенты отображаются в порядке по возрастанию LastName, что указано как вариант по умолчанию в switch. После того, как пользователь щелкает на заголовке столбца, соответствующее значение sortOrder добавляется в строку запроса.
The two ViewBag variables are used so that the view can configure the column heading hyperlinks with the appropriate query string values:
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
Это тернарное утверждения. Первое утверждает, что, если sortOrder равен null или пустой, то значение ViewBag.NameSortParam устанавливается в “Name desc”, иначе устанавливается в пустую строку.
Есть четыре варианта, в зависимости от того, как сортируются данные:
- Если сортируется по возрастанию по LastName, ссылка LastName должна указывать на сортировку по убыванию по LastName и ссылка EnrollmentDate на сортировку по возрастанию по Date соответственно.
- Если сортируется по убыванию по LastName, ссылки должны указывать на сортировку по возрастанию как по LastName так и по Date.
- Если сортируется по возрастанию по Date, ссылки должны указывать на сортировку по возрастанию по LastName и по убыванию по Date.
- Если сортируется по убыванию по Date, ссылки должны указывать на сортировку по возрастанию по LastName и по возрастанию по Date.
Метод использует LINQ to Entities чтобы указать на столбец, который будет сортироваться. В коде перед switch создаётся переменная, затем эта переменная изменяется в условиях switch, при этом перед закрытием switch вызывается метод ToList. Когда вы создаете и изменяете переменные IQueryable, к базе данных не выполняется никаких запросов. Запрос не выполняется до тех пор, пока вы не сконвертируете ваш объект IQueryable в коллекцию с помощью вызова, подобного ToList. Этот метод возвращает один запрос, который выполняется при return View.
Добавление заголовков-ссылок в Student Index
В Views \ Student \ Index . cshtml замените код контейнеров<tr> и<th> следующим:
<tr>
<th></th>
<th>
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm })
</th>
<th>
First Name
</th>
<th>
@Html.ActionLink("Enrollment Date", "Index", new { sortOrder=ViewBag.DateSortParm })
</th>
</tr>
В этом коде используется информация в свойствах ViewBag для заполнения ссылок подходящими значениями запроса. Запустите проект и щёлкните на заголовках, чтобы убедиться, что сортировка работает.
Добавление поиска
Чтобы добавить фильтрацию, вы должны добавить на представление текстовое поле и кнопку отправки и сделать соответствующие изменения в коде метода Index.
ИзменениекодаметодаIndex
В Controllers\StudentController.cs замените код метода Index
public ViewResult Index(string sortOrder, string searchString)
{
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
var students = from s in db.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
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(students.ToList());
}
Мы добавили в метод Index параметр searchString, кляузу в LINQ-утверждение, с помощью которого выбираются только те студенты, имя или фамилия которых содержит строку поиска. Строка поиска получается из текстового поля, которое вы позже добавите на представление. Код, который добавляет кляузу where в запрос, выполняется только в том случае, если задано значение для поиска:
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
Note Реализация Contains .NET Framework возвращает все записи в том случае, когда вы передаете в него пустую строку, но провайдер Entity Framework для SQL Server Compact 4.0 для пустой строки возвращает пустое множество. Кроме этого, реализация в .NET Framework проводит регистрозависимое сравнение, в отличие от провайдеров Entity Framework SQL Server, которые по умолчанию проводят регистронезависимое сравнение. Therefore, calling the ToUpper method to make the test explicitly case-insensitive ensures that results do not change when you change the code later to use a repository, which will return an IEnumerable collection instead of an IQueryable object. (When you call the Contains method on an IEnumerable collection, you get the .NET Framework implementation; when you call it on an IQueryable object, you get the database provider implementation.)
Добавление поиска на представление
В Views \ Student \ Index . cshtmlпрямо перед открывающим тегом table добавьте заголовок, текстовое поле и кнопку Search:
@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString")
<input type="submit" value="Search" /></p>
}
Запустите проект, введите что-нибудь в строку поиска и нажмите на кнопку Search чтобы убедиться в работе фильтрации.
Добавление разбиения по страницам
Для этого необходимо сначала установить NuGet-пакет PagedList, затем сделать изменения в методе Index и добавить на представление ссылки на страницы.
Установка NuGet-пакета NuGet Package
NuGet-пакет PagedList устанавливает тип коллекции PagedList. Когда вы добавляете в коллекцию этого типа результаты запроса, вам предоставляется набор свойств и методов для обеспечения разбиения результатов по страницам.
В Visual Studio выберите проект. Затем нажмите в меню Tools пунктLibrary Package Manager и потом Add Library Package Reference.
В Add Library Package Reference нажмите на вкладку Online слева и введите в строку поиска "pagedlist". Как только появится пакет PagedList нажмите Install.
Добавление функциональности разбиения по страницам в метод Index
В Controllers\StudentController.cs добавьте using PagedList:
using PagedList;
Замените код метода Index:
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
{
ViewBag.CurrentSort = sortOrder;
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;
var students = from s in db.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
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;
int pageIndex = (page ?? 1) - 1;
return View(students.ToPagedList(pageIndex, pageSize));
}
Добавлен параметр page, несущий информацию о параметре, по которому в данный момент производится сортировка, и параметр в сигнатуре метода:
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
При первом вызове страницы (или если пользователь не нажал на одну из ссылок на страницы), переменная page равна null. После нажатия в эту переменную помещается номер страницы.
Свойство ViewBag передаёт в представление текущий параметр сортировки для её сохранения при переходе на другие страницы:
ViewBag.CurrentSort = sortOrder;
Второе свойство ViewBag передаёт в представление строку фильтрации для того, чтобы при переходе на другую страницу введёная строка поиска не терялась и сохранялись настройки фильтрации. Кроме этого, если строка поиска меняется в случае перехода на другую страницу результатов, номер страницы должен быть скинут в 1, поскольку новая фильтрация предоставляет новый набор данных.
if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;
В конце метода запрос конвертируется вместо List в PagedList, после чего его можно передавать в представление, поддерживающее разбиение результатов по страницам.
int pageSize = 3;
int pageIndex = (page ?? 1) - 1;
return View(students.ToPagedList(pageIndex, pageSize));
В метод ToPagedList передаётся значение индекса страницы, которое равно 0, в отличие от номера страницы, который равен 1. Поэтому код извлекает 1 из номера страницы, чтобы получить значения индекса страницы (два знака вопроса обозначают оператор, определяющий значение по умолчанию для типа nullable, таким образом, выражение (page ?? 1) возвращает значение page в том случае, если оно имеет значение, или 1, если page равен null. Другими словами, установите pageIndex в page - 1 если page не равен null, или установите его в 1-1 если он равен null)
Добавление ссылок на страницы на представление
В Views \ Student \ Index . cshtml замените исходный код на:
@model PagedList.IPagedList<ContosoUniversity.Models.Student>
@{
ViewBag.Title = "Students";
}
<h2>Students</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
<input type="submit" value="Search" /></p>
}
<table>
<tr>
<th></th>
<th>
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
</th>
<th>
First Name
</th>
<th>
@Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.StudentID }) |
@Html.ActionLink("Details", "Details", new { id=item.StudentID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.StudentID })
</td>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
</tr>
}
</table>
<div>
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber)
of @Model.PageCount
@if (Model.HasPreviousPage)
{
@Html.ActionLink("<<", "Index", new { page = 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink("< Prev", "Index", new { page = Model.PageNumber - 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:<<
@Html.Raw(" ");
@:< Prev
}
@if (Model.HasNextPage)
{
@Html.ActionLink("Next >", "Index", new { page = Model.PageNumber + 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink(">>", "Index", new { page = Model.PageCount, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:Next >
@Html.Raw(" ")
@:>>
}
</div>
Утверждение @model показывает, что представление принимает на вход объект типа PagedList вместо объекта типа List.
Текстовая строка инициализируется текущей строкой поиска чтобы пользователь мог переходить со страницы на страницу не теряя строку поиска:
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
Ссылки в заголовках столбцов используют строку запроса для передачи текущей строки поиска в контроллер, чтобы пользователь мог сортировать возвращённые механизмом фильтра результаты:
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
В «подвале» страницы находится запись, демонстрирующая номер страницы и ссылки навигации:
Page [current page number] of [total number of pages] << < Prev Next > >>
<< - ссылка на первую страницу, < Prev – ссылка на предыдущую страницу, и так далее. Если пользователь находится на странице с номером 1, ссылки на предыдущие страницы недоступны, и также для последней страницы. Каждая ссылка на страницу передаёт номер выбранной страницы и текущие данные о фильтрации и сортировки в контроллер в строке запроса, таким образом позволяя управлять этими данными в процессе разбиения на страницы.
Если результатов нет, показывается надпись "Page 0 of 0".
Запустите проект.
Щелкните ссылки на страницы в различных режимах сортировки и введите какую-либо строку поиска, чтобы убедиться, что всё работает в связке.
Создание страницы со статистикой
На странице About мы будем показывать, сколько студентов записались на каждую дату записи. Для этого необходима группировка и небольшие вычисления, для чего мы должны сделать следующее:
- Создать класс с моделью представления для данных, которые мы будем передавать в представление
- Изменить код метода About в контроллере Home.
- Изменить код представления About.
Создание модели представления
Создайте папку ViewModels и в ней создайте файл EnrollmentDateGroup . cs со следующим содержанием:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.ViewModels
{
public class EnrollmentDateGroup
{
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
Изменение контроллера Home
В HomeController . cs добавьте необходимые using:
using ContosoUniversity.DAL;
using ContosoUniversity.Models;
using ContosoUniversity.ViewModels;
Добавьте переменную с контекстом базы данных:
private SchoolContext db = new SchoolContext();
Замените код метода About на:
public ActionResult About()
{
var data = from student in db.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(data);
}
LINQ-утверждения группируют сущности студентов по дате записи, затем вычисляют количество сущностей в каждой группе и сохраняют результат в EnrollmentDateGroup.
Изменение кода представления About
Замените код в Views\Home\About.cshtml file на:
@model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup>
@{
ViewBag.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>
@String.Format("{0:d}", item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
Запустите проект.
Это перевод оригинальной статьи Sorting, Filtering, and Paging with the Entity Framework in an ASP.NET MVC Application. Благодарим за помощь в переводе Александра Белоцерковского.