В предыдущих темах было рассмотрено, как по отдельности произвести фильтрацию, сортировку и постраничный вывод. Теперь посмотрим, как можно все это объединить в одном приложении.
Для работы с проектом пусть у нас будут определены следующие модели User и Company:
public class User { public int Id { get; set; } public string? Name { get; set; } public int Age { get; set; } public int? CompanyId { get; set; } public Company? Company { get; set; } } public class Company { public int Id { get; set; } public string? Name { get; set; } public List<User> Users { get; set; } = new(); }
Для взаимодействия с MS SQL Server через Entity Framework добавим в проект через Nuget пакет Microsoft.EntityFrameworkCore.SqlServer. А затем добавим в проект класс контекста данных UsersContext:
using Microsoft.EntityFrameworkCore; namespace MvcApp.Models { public class UsersContext : DbContext { public DbSet<User> Users { get; set; } = null!; public DbSet<Company> Companies { get; set; } = null!; public UsersContext(DbContextOptions<UsersContext> options) : base(options) { Database.EnsureCreated(); } } }
И в файле Program.cs настроим использование этого контекста данных:
using Microsoft.EntityFrameworkCore; using MvcApp.Models; var builder = WebApplication.CreateBuilder(args); string connection = "Server = (localdb)\\mssqllocaldb;Database = userstoredb;Trusted_Connection=true"; builder.Services.AddDbContext<UsersContext>(options => options.UseSqlServer(connection)); builder.Services.AddControllersWithViews(); var app = builder.Build(); app.MapDefaultControllerRoute(); app.Run();
Итак, нам надо произвести три операции: фильтрацию, сортировку и пагинацию. Каждая из этих операций будет представлять свой набор параметров. Для упрощения для каждого набора параметров определим собственную модель. Так, для фильтрации добавим модель FilterViewModel:
using Microsoft.AspNetCore.Mvc.Rendering; using System.Collections.Generic; namespace MvcApp.Models { public class FilterViewModel { public FilterViewModel(List<Company> companies, int company, string name) { // устанавливаем начальный элемент, который позволит выбрать всех companies.Insert(0, new Company { Name = "Все", Id = 0 }); Companies = new SelectList(companies, "Id", "Name", company); SelectedCompany = company; SelectedName = name; } public SelectList Companies { get; } // список компаний public int SelectedCompany { get; } // выбранная компания public string SelectedName { get; } // введенное имя } }
В данном случае фильтрация будет идти по компаниям и по имени пользователей, поэтому для выбранных значений определены два свойства плюс список компаний.
Для сортировки добавим перечисление SortState, которое будет описывать все возможные варианты сортировки:
public enum SortState { NameAsc, // по имени по возрастанию NameDesc, // по имени по убыванию AgeAsc, // по возрасту по возрастанию AgeDesc, // по возрасту по убыванию CompanyAsc, // по компании по возрастанию CompanyDesc // по компании по убыванию }
И затем добавим новую модель SortViewModel:
namespace MvcApp.Models { public class SortViewModel { public SortState NameSort { get; } // значение для сортировки по имени public SortState AgeSort { get; } // значение для сортировки по возрасту public SortState CompanySort { get; } // значение для сортировки по компании public SortState Current { get; } // текущее значение сортировки public SortViewModel(SortState sortOrder) { NameSort = sortOrder == SortState.NameAsc ? SortState.NameDesc : SortState.NameAsc; AgeSort = sortOrder == SortState.AgeAsc ? SortState.AgeDesc : SortState.AgeAsc; CompanySort = sortOrder == SortState.CompanyAsc ? SortState.CompanyDesc : SortState.CompanyAsc; Current = sortOrder; } } }
Здесь для каждого свойства хранится его текущее значение SortState. Кроме того, отдельное свойство Current хранит выбранный критерий сортировки.
Для пагинации добавим новую модель PageViewModel:
namespace MvcApp.Models { public class PageViewModel { public int PageNumber { get;} public int TotalPages { get;} public bool HasPreviousPage => PageNumber > 1; public bool HasNextPage => PageNumber < TotalPages; public PageViewModel(int count, int pageNumber, int pageSize) { PageNumber = pageNumber; TotalPages = (int)Math.Ceiling(count / (double)pageSize); } } }
И в конце добавим общую модель IndexViewModel, которая объединит все эти модели и полученные данные:
namespace MvcApp.Models { public class IndexViewModel { public IEnumerable<User> Users { get; } public PageViewModel PageViewModel { get; } public FilterViewModel FilterViewModel { get;} public SortViewModel SortViewModel { get;} public IndexViewModel(IEnumerable<User> users, PageViewModel pageViewModel, FilterViewModel filterViewModel, SortViewModel sortViewModel) { Users = users; PageViewModel = pageViewModel; FilterViewModel = filterViewModel; SortViewModel = sortViewModel; } } }
После создания всех классов проект будет выглядеть следующим образом:
При фильтрации нам надо отдавать новый отфильтрованный набор пользователей. В этом плане фильтрация не зависит от параметров сортировки и пагинации. Не имеет значения на какой странице мы находимся и по какому столбцу отсортирована выборка, если мы отправляем новое значение для фильтрации.
При сортировке необходимо учитывать параметры фильтрации, то есть сортировка может идти только в отфильтрованном наборе. В то же время для сортировки не играют никакого значения параметры пагинации. И неважно, на какой странице мы находимся, если мы решили отсортировать набор по другому столбцу.
При пагинации необходимо учитывать параметры фильтрации и сортировки, то есть переход по страницам будет идти только в отсортированном и отфильтрованном наборе.Таким образом, фильтрация ни от чего не зависит. Сортировка зависит от фильтрации. А пагинация зависит от фильтрации и сортировки. Поэтому сначала надо выполнить фильтрацию, потом сортировку и в конце пагинацию. Исходя из этого, определим в контроллере HomeController следующий код:
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MvcApp.Models; namespace MvcApp.Controllers { public class HomeController : Controller { UsersContext db; public HomeController(UsersContext context) { db = context; // добавляем начальные данные if (!db.Companies.Any()) { Company oracle = new Company { Name = "Oracle" }; Company google = new Company { Name = "Google" }; Company microsoft = new Company { Name = "Microsoft" }; Company apple = new Company { Name = "Apple" }; User user1 = new User { Name = "Олег Васильев", Company = oracle, Age = 26 }; User user2 = new User { Name = "Александр Овсов", Company = oracle, Age = 24 }; User user3 = new User { Name = "Алексей Петров", Company = microsoft, Age = 25 }; User user4 = new User { Name = "Иван Иванов", Company = microsoft, Age = 26 }; User user5 = new User { Name = "Петр Андреев", Company = microsoft, Age = 23 }; User user6 = new User { Name = "Василий Иванов", Company = google, Age = 23 }; User user7 = new User { Name = "Олег Кузнецов", Company = google, Age = 25 }; User user8 = new User { Name = "Андрей Петров", Company = apple, Age = 24 }; db.Companies.AddRange(oracle, microsoft, google, apple); db.Users.AddRange(user1, user2, user3, user4, user5, user6, user7, user8); db.SaveChanges(); } } public async Task<IActionResult> Index(string name, int company = 0, int page = 1, SortState sortOrder = SortState.NameAsc) { int pageSize = 3; //фильтрация IQueryable<User> users = db.Users.Include(x => x.Company); if (company != 0) { users = users.Where(p => p.CompanyId == company); } if (!string.IsNullOrEmpty(name)) { users = users.Where(p => p.Name!.Contains(name)); } // сортировка switch (sortOrder) { case SortState.NameDesc: users = users.OrderByDescending(s => s.Name); break; case SortState.AgeAsc: users = users.OrderBy(s => s.Age); break; case SortState.AgeDesc: users = users.OrderByDescending(s => s.Age); break; case SortState.CompanyAsc: users = users.OrderBy(s => s.Company!.Name); break; case SortState.CompanyDesc: users = users.OrderByDescending(s => s.Company!.Name); break; default: users = users.OrderBy(s => s.Name); break; } // пагинация var count = await users.CountAsync(); var items = await users.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); // формируем модель представления IndexViewModel viewModel = new IndexViewModel( items, new PageViewModel(count, page, pageSize), new FilterViewModel(db.Companies.ToList(), company, name), new SortViewModel(sortOrder) ); return View(viewModel); } } }
В методе Index в качестве параметров принимаем выбранную компанию (ее id), введенное имя для поиска, номер страницы и значение сортировки. Если последние два параметра не переданы, то для них устанавливаются значения по умолчанию - первая страница и сортировка по имени по возрастанию.
Код метода Index получился довольно большим и органично разбивается на три секции: фильтрация, сортировка и пагинация. В идеале код каждой отдельной секции можно было бы выделить в отдельный класс, но в данном случае я не буду чрезмерно раздувать проект новыми классами.
И для действия Index контроллера HomeController определим следующее представление Index.cshtml:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @using MvcApp.Models @model IndexViewModel <style> .glyphicon { display: inline-block; padding:0 5px;} .glyphicon-chevron-right:after { content: "\00BB"; } .glyphicon-chevron-left:before { content: "\00AB"; } </style> <h1>Список пользователей</h1> <form method="get"> <label>Имя: </label> <input name="name" value="@Model.FilterViewModel.SelectedName" /> <label>Компания: </label> <select name="company" asp-items="Model.FilterViewModel.Companies"></select> <input type="submit" value="Фильтр" /> </form> <table> <tr> <th> <a asp-action="Index" asp-route-sortOrder="@(Model.SortViewModel.NameSort)" asp-route-name="@(Model.FilterViewModel.SelectedName)" asp-route-company="@(Model.FilterViewModel.SelectedCompany)">Имя</a> </th> <th> <a asp-action="Index" asp-route-sortOrder="@(Model.SortViewModel.AgeSort)" asp-route-name="@(Model.FilterViewModel.SelectedName)" asp-route-company="@(Model.FilterViewModel.SelectedCompany)">Возраст</a> </th> <th> <a asp-action="Index" asp-route-sortOrder="@(Model.SortViewModel.CompanySort)" asp-route-name="@(Model.FilterViewModel.SelectedName)" asp-route-company="@(Model.FilterViewModel.SelectedCompany)">Компания</a> </th> </tr> @foreach (User u in Model.Users) { <tr><td>@u.Name</td><td>@u.Age</td><td>@u.Company?.Name</td></tr> } </table> <p> @if (Model.PageViewModel.HasPreviousPage) { <a asp-action="Index" asp-route-page="@(Model.PageViewModel.PageNumber - 1)" asp-route-name="@(Model.FilterViewModel.SelectedName)" asp-route-company="@(Model.FilterViewModel.SelectedCompany)" asp-route-sortorder="@(Model.SortViewModel.Current)" class="glyphicon glyphicon-chevron-left"> Назад </a> } @if (Model.PageViewModel.HasNextPage) { <a asp-action="Index" asp-route-page="@(Model.PageViewModel.PageNumber + 1)" asp-route-name="@(Model.FilterViewModel.SelectedName)" asp-route-company="@(Model.FilterViewModel.SelectedCompany)" asp-route-sortorder="@(Model.SortViewModel.Current)" class="glyphicon glyphicon-chevron-right"> Вперед </a> } </p>
Так как сортировка зависит от фильтрации, то в ссылки для сортировки передаются выбранные значения при фильтрации.
И аналогично так как пагинация зависит от фильтрации и сортировки, в ссылки для пагинации передаются выбранные значения при фильтрации и сортировки.