Объединение сортировки, фильтрации и пагинации

Последнее обновление: 12.04.2022

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

Для работы с проектом пусть у нас будут определены следующие модели 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;
        }
    }
}

После создания всех классов проект будет выглядеть следующим образом:

Создание проекта для пагинации, фильтрации и сортироки в ASP.NET Core MVC и C#

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

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

При пагинации необходимо учитывать параметры фильтрации и сортировки, то есть переход по страницам будет идти только в отсортированном и отфильтрованном наборе.

Таким образом, фильтрация ни от чего не зависит. Сортировка зависит от фильтрации. А пагинация зависит от фильтрации и сортировки. Поэтому сначала надо выполнить фильтрацию, потом сортировку и в конце пагинацию. Исходя из этого, определим в контроллере 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>

Так как сортировка зависит от фильтрации, то в ссылки для сортировки передаются выбранные значения при фильтрации.

И аналогично так как пагинация зависит от фильтрации и сортировки, в ссылки для пагинации передаются выбранные значения при фильтрации и сортировки.

Сортировка и пагинация и фильтрация через Entity Framework в ASP.NET Core MVC и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850