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

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

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

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

Для работы с проектом пусть у нас будут определены следующие модели 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; }
    public Company()
    {
        Users = new List<User>();
    }
}

Для взаимодействия с MS SQL Server через Entity Framework добавим в проект через Nuget пакет Microsoft.EntityFrameworkCore.SqlServer. А затем добавим в проект класс контекста данных UsersContext:

using Microsoft.EntityFrameworkCore;

namespace FilterSortPagingApp.Models
{
    public class UsersContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Company> Companies { get; set; }
        public UsersContext(DbContextOptions<UsersContext> options)
            : base(options)
        {
			Database.EnsureCreated();
        }
    }
}

И в классе Startup в методе ConfigureServices() настроим использование этого контекста данных:

public void ConfigureServices(IServiceCollection services)
{
	string connection = "Server=(localdb)\\mssqllocaldb; Database=userstoredb; Trusted_Connection=true;";
	services.AddDbContext<UsersContext>(options => options.UseSqlServer(connection));
	services.AddControllersWithViews();
}

Итак, нам надо произвести три операции: фильтрацию, сортировку и пагинацию. Каждая из этих операций будет представлять свой набор параметров. Для упрощения для каждого набора параметров определим собственную модель. Так, для фильтрации добавим модель FilterViewModel:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FilterSortPagingApp.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; private set; } // список компаний
        public int? SelectedCompany { get; private set; }	// выбранная компания
        public string SelectedName { get; private set; }	// введенное имя
    }
}

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

Для сортировки добавим перечисление SortState, которое будет описывать все возможные варианты сортировки:

public enum SortState
{
    NameAsc,	// по имени по возрастанию
    NameDesc,	// по имени по убыванию
    AgeAsc,	// по возрасту по возрастанию
    AgeDesc,	// по возрасту по убыванию
    CompanyAsc,	// по компании по возрастанию
    CompanyDesc	// по компании по убыванию
}

И затем добавим новую модель SortViewModel:

namespace FilterSortPagingApp.Models
{
    public class SortViewModel
    {
        public SortState NameSort { get; private set; } // значение для сортировки по имени
        public SortState AgeSort { get; private set; }    // значение для сортировки по возрасту
        public SortState CompanySort { get; private set; }   // значение для сортировки по компании
        public SortState Current { get; private set; }     // текущее значение сортировки

        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:

using System;

namespace FilterSortPagingApp.Models
{
    public class PageViewModel
    {
        public int PageNumber { get; private set; }
        public int TotalPages { get; private set; }

        public PageViewModel(int count, int pageNumber, int pageSize)
        {
            PageNumber = pageNumber;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);
        }

        public bool HasPreviousPage
        {
            get
            {
                return (PageNumber > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (PageNumber < TotalPages);
            }
        }
    }
}

И в конце добавим общую модель IndexViewModel, которая объединит все эти модели и полученные данные:

using System.Collections.Generic;

namespace FilterSortPagingApp.Models
{
    public class IndexViewModel
    {
        public IEnumerable<User> Users { get; set; }
        public PageViewModel PageViewModel { get; set; }
        public FilterViewModel FilterViewModel { get; set; }
        public SortViewModel SortViewModel { get; set; }
    }
}

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

Создание проекта для пагинации в ASP.NET Core

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

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

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

Таким образом, фильтрация ни от чего не зависит. Сортировка зависит от фильтрации. А пагинация зависит от фильтрации и сортировки. Поэтому сначала надо выполнить фильтрацию, потом сортировку и в конце пагинацию. Исходя из этого, определим в контроллере HomeController следующий код:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FilterSortPagingApp.Models;
using System.Linq;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace FilterSortPagingApp.Controllers
{
    public class HomeController : Controller
    {
        UsersContext db;
        public HomeController(UsersContext context)
        {
            this.db = context;
			// добавляем начальные данные
			if (db.Companies.Count() == 0)
            {
                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(int? company, string name, int page = 1, 
            SortState sortOrder = SortState.NameAsc)
        {
            int pageSize = 3;

            //фильтрация
            IQueryable<User> users = db.Users.Include(x=>x.Company);

            if (company != null && 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
            {
                PageViewModel = new PageViewModel(count, page, pageSize),
                SortViewModel = new SortViewModel(sortOrder),
                FilterViewModel = new FilterViewModel(db.Companies.ToList(), company, name),
                Users = items
            };
            return View(viewModel);
        }
    }
}

В методе Index в качестве параметров принимаем выбранную компанию (ее id), введенное имя для поиска, номер страницы и значение сортировки. Если последние два параметра не переданы, то для них устанавливаются значения по умолчанию - первая страница и сортировка по имени по возрастанию.

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

И также изменим код представления Index.cshtml:

@using FilterSortPagingApp.Models
@model IndexViewModel

@{
    ViewData["Title"] = "Home Page";
}
    <style>
        @@font-face {
            font-family: 'FontAwesome';
            src: url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2') format('woff2'), 
			url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/fonts/fontawesome-webfont.woff') format('woff'), 
			url('https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/fonts/fontawesome-webfont.ttf') format('truetype');
            font-weight: normal;
            font-style: normal
        }
        .glyphicon {
            display: inline-block;
            font: normal normal normal 14px/1 FontAwesome;
            font-size: inherit;
            text-rendering: auto;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale
        }
        .glyphicon-chevron-right:before {
            content: "\f054";
        }
        .glyphicon-chevron-left:before {
            content: "\f053";
        }
    </style>
<h1>Список пользователей</h1>
<form method="get">
    <div class="form-inline">
        <label>Имя: </label>
        <input name="name" value="@Model.FilterViewModel.SelectedName" class="form-control" />
        
        <label>Компания: </label>
        <select name="company" asp-items="Model.FilterViewModel.Companies" class="form-control"></select>

        <input type="submit" value="Фильтр" class="btn btn-outline-dark" />
    </div>
</form>

<table class="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>
@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="btn  btn-outline-dark">
        <i class="glyphicon glyphicon-chevron-left"></i>
        Назад
    </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="btn btn-outline-dark">
        Вперед
        <i class="glyphicon glyphicon-chevron-right"></i>
    </a>
}

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

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

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