company_banner

Учебный курс. Сортировка, фильтрация и разбиение по страницам с Entity Framework в приложении ASP.NET MVC

Автор оригинала: ASP.NET Team
  • Перевод
Это продложение цикла статей, посвященого разработке с помощью Entity Framework и ASP.NET MVC 3. Первые главы вы можете найти по следующим ссылкам: Создание модели данных Entity Framework для приложения ASP.NET MVC и Реализация базовой CRUD-функциональности с Entity Framework в приложении ASP.NET MVC.

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

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

image

Добавление заголовков-сортировщиков в столбцы на странице 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 для заполнения ссылок подходящими значениями запроса. Запустите проект и щёлкните на заголовках, чтобы убедиться, что сортировка работает.

image

Добавление поиска


Чтобы добавить фильтрацию, вы должны добавить на представление текстовое поле и кнопку отправки и сделать соответствующие изменения в коде метода 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())); 
}

Реализация Contains .NET Framework возвращает все записи в том случае, когда вы передаете в него пустую строку, но провайдер Entity Framework для SQL Server Compact 4.0 для пустой строки возвращает пустое множество. Кроме этого, реализация в .NET Framework проводит регистрозависимое сравнение, в отличие от провайдеров Entity Framework SQL Server, которые по умолчанию проводят регистронезависимое сравнение.

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


В Views\Student\Index.cshtmlпрямо перед открывающим тегом table добавьте заголовок, текстовое поле и кнопку Search:

@using (Html.BeginForm()) 
{ 
    <p> 
        Find by name: @Html.TextBox("SearchString")   
        <input type="submit" value="Search" /></p> 
}

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

image

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


Для этого необходимо сначала установить NuGet-пакет PagedList, затем сделать изменения в методе Index и добавить на представление ссылки на страницы.

image

Установка NuGet-пакета NuGet Package


NuGet-пакет PagedList устанавливает тип коллекции PagedList. Когда вы добавляете в коллекцию этого типа результаты запроса, вам предоставляется набор свойств и методов для обеспечения разбиения результатов по страницам.

В Visual Studio выберите проект. Затем нажмите в меню Tools пунктLibrary Package Manager и потом Add Library Package Reference.

В Add Library Package Reference нажмите на вкладку Online слева и введите в строку поиска "pagedlist". Как только появится пакет PagedList нажмите Install.

image

Добавление функциональности разбиения по страницам в метод 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) &nbsp;

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

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

Запустите проект.

image

Щелкните ссылки на страницы в различных режимах сортировки и введите какую-либо строку поиска, чтобы убедиться, что всё работает в связке.

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


На странице 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>

Запустите проект.

image

Благодарим за помощь в переводе Александра Белоцерковского (ahriman).
  • +20
  • 20,3k
  • 4
Microsoft
481,00
Microsoft — мировой лидер в области ПО и ИТ-услуг
Поделиться публикацией

Комментарии 4

    +1
    А теперь расскажем о недостатках предлагаемого подхода для решения задач. На скриншотах мы видим, что в этом замечательном приложении, кроме вкладки Student Statistics, есть еще куча других вкладок, на которых видимо выводятся другие таблицы с фильтрацией и сортировкой.

    Если использовать предложенный подход, то код для этих вкладок придется делать методом copy-paste: дублировать контроллеры и шаблоны. Стоит ли говорить, насколько это плохо: получается гора некачественного кода, написанного с нарушением DRY, плюс. ни один уважающий себя программист не захочет с таким кодом работать, и придется искать украинских или индусских аутсорсеров для его поддержки.

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

    Жалко, это перевод, так что автор статьи вряд ли узнает, что его подход не самый умный и не самый правильный, но пусть хоть читатели знают.
      +1
      Автор, кстати, один из разработчиков ASP.NET MVC, как я понимаю. А к этому примеру стоит относиться лишь как к примеру сортировок, фильтраций и разбиению по страницам с EF. Безусловно ваш подход более совершенен, но тогда пришлось бы сюда добавить кроме всего этого кода еще код создания виджетов, а там еще что-нибудь прибавится. Так что статья несмотря на высказанные вами недостатки в решении все равно остается актуальной, а добавление виджетов может стать вполне хорошим ее продолжением.
      0
      Вопрос. Как при делении таблицы на страныцы будет выполнятся извлечение из бд. Извлекаться будет только та часть, которая относиться к странице или извлекается вся таблица?
        +1
        только та часть, конечно

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

      Самое читаемое