Создание tag-хелпера сортировки

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

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

Продолжим работу с проектом, который был создан в прошлой теме, где у нас были модели User и Company:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Company { 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>();
    }
}

Также был класс UsersContext для взаимодействия с бд:

using Microsoft.EntityFrameworkCore;

namespace SortApp.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();
        }
    }
}

И было перечисление, которое описывает все критерии сортировки:

public enum SortState
{
    NameAsc,
    NameDesc,
    AgeAsc,
    AgeDesc,
    CompanyAsc,
    CompanyDesc
}

Теперь добавим специальный tag-хелпер для создания ссылок, по нажатию на которые будет производиться сортировка. Это позволит управлять созданием заголовков, настраивать их. Для этого добавим в проект новую папку TagHelpers. А в эту папку поместим новый класс SortHeaderTagHelper:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SortApp.Models;

namespace SortApp.TagHelpers
{
    public class SortHeaderTagHelper : TagHelper
    {
        public SortState Property { get; set; } // значение текущего свойства, для которого создается тег
        public SortState Current { get; set; }  // значение активного свойства, выбранного для сортировки
        public string Action { get; set; }  // действие контроллера, на которое создается ссылка
        public bool Up { get; set; }    // сортировка по возрастанию или убыванию

        private IUrlHelperFactory urlHelperFactory;
        public SortHeaderTagHelper(IUrlHelperFactory helperFactory)
        {
            urlHelperFactory = helperFactory;
        }
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
            output.TagName = "a";
            string url = urlHelper.Action(Action, new { sortOrder = Property });
            output.Attributes.SetAttribute("href", url);
            // если текущее свойство имеет значение CurrentSort
            if (Current == Property)
            {
                TagBuilder tag = new TagBuilder("i");
                tag.AddCssClass("glyphicon");
               
                if (Up == true)   // если сортировка по возрастанию
                    tag.AddCssClass("glyphicon-chevron-up");
                else   // если сортировка по убыванию
                    tag.AddCssClass("glyphicon-chevron-down");

                output.PreContent.AppendHtml(tag);
            }
        }
    }
}

Данные в tag-хелпер будут передаваться извне через набор свойств:

public SortState Property { get; set; } // значение текущего свойства, для которого создается тег
public SortState Current { get; set; }  // значение активного свойства, выбранного для сортировки
public string Action { get; set; }  // действие контроллера, на которое создается ссылка
public bool Up { get; set; }    // сортировка по возрастанию или убыванию

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

Для создания адреса ссылки по методу контроллера потребуется объект IUrlHelperFactory. И мы можем получить его в конструкторе, так как он встраивается по умолчанию через встроенный в ASP.NET Core механизм dependency injection.

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

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }

С помощью этого объекта мы сможем получить объект IUrlHelper, который необходим для создания ссылки.

Далее в методе Process идет создание ссылки. Для ее стилизации используются классы, которые будут определены далее в представлении и которые для визуализации будут использовать шрифты библиотеки font-awesome.

Теперь нам надо передать данные для этого хелпера. Для этого определим в папке Models новый класс SortViewModel:

namespace SortApp.Models
{
    public class SortViewModel
    {
        public SortState NameSort { get; set; } // значение для сортировки по имени
        public SortState AgeSort { get; set; }    // значение для сортировки по возрасту
        public SortState CompanySort { get; set; }   // значение для сортировки по компании
        public SortState Current { get; set; }     // значение свойства, выбранного для сортировки
        public bool Up { get; set; }  // Сортировка по возрастанию или убыванию

        public SortViewModel(SortState sortOrder)
        {
            // значения по умолчанию
            NameSort = SortState.NameAsc;
            AgeSort = SortState.AgeAsc;
            CompanySort = SortState.CompanyAsc;
            Up = true;

            if (sortOrder == SortState.AgeDesc || sortOrder == SortState.NameDesc
                || sortOrder == SortState.CompanyDesc)
            {
                Up = false;
            }

            switch (sortOrder)
            {
                case SortState.NameDesc:
                    Current = NameSort = SortState.NameAsc;
                    break;
                case SortState.AgeAsc:
                    Current = AgeSort = SortState.AgeDesc;
                    break;
                case SortState.AgeDesc:
                    Current = AgeSort = SortState.AgeAsc;
                    break;
                case SortState.CompanyAsc:
                    Current = CompanySort = SortState.CompanyDesc;
                    break;
                case SortState.CompanyDesc:
                    Current = CompanySort = SortState.CompanyAsc;
                    break;
                default:
                    Current = NameSort = SortState.NameDesc;
                    break;
            }
        }
    }
}

Здесь важно понимать смысл свойства Current. Оно нам нужно лишь для того, чтобы в выше определенном tag-хелпере определить, что данное свойство, для которого применяется хелпер, используется в текущий момент для сортировки. Поэтому свойство Current указывает на значение текущего выбраного свойства, по которому проводится сортировка. То есть свойство Current будет равно одну из свойств NameSort, AgeSort или CompanySort

И далее добавим в папку Models новый класс IndexViewModel, который будет представлять модель для представления Index.cshtml:

using System.Collections.Generic;

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

Теперь изменим код контроллера HomeController:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SortApp.Models;
using System.Linq;

namespace SortApp.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(SortState sortOrder = SortState.NameAsc)
        {
            IQueryable<User> users = db.Users.Include(x=>x.Company);

            users = sortOrder switch
            {
                SortState.NameDesc => users.OrderByDescending(s => s.Name),
                SortState.AgeAsc => users.OrderBy(s => s.Age),
                SortState.AgeDesc => users.OrderByDescending(s => s.Age),
                SortState.CompanyAsc => users.OrderBy(s => s.Company.Name),
                SortState.CompanyDesc => users.OrderByDescending(s => s.Company.Name),
                _ => users.OrderBy(s => s.Name),
            };
            IndexViewModel viewModel = new IndexViewModel
            {
                Users = await users.AsNoTracking().ToListAsync(),
                SortViewModel = new SortViewModel(sortOrder)
            };
            return View(viewModel);
        }
    }
}

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

@using SortApp.Models

@model IndexViewModel
<!--импортируем tag-хелперы проекта-->
@addTagHelper *, SortApp

@{
    ViewData["Title"] = "Список пользователей";
}
<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-down:before {
content: "\f078";
}

.glyphicon-chevron-up:before {
content: "\f077";
}
</style>
<h1>Список пользователей</h1>
<table class="table">
    <tr>
        <th>
            <sort-header action="Index"  up="@Model.SortViewModel.Up"
                    current="@Model.SortViewModel.Current" property="@Model.SortViewModel.NameSort">
                Имя
            </sort-header>
        </th>
        <th>
            <sort-header action="Index" up="@Model.SortViewModel.Up"
                    current="@Model.SortViewModel.Current" property="@Model.SortViewModel.AgeSort">
                Возраст
            </sort-header>
        </th>
        <th>
            <sort-header action="Index" up="@Model.SortViewModel.Up"
                    current="@Model.SortViewModel.Current" property="@Model.SortViewModel.CompanySort">
                Компания
            </sort-header>
        </th>
    </tr>
    @foreach (User u in Model.Users)
    {
        <tr><td>@u.Name</td><td>@u.Age</td><td>@u.Company.Name</td></tr>
    }
</table>

Поскольку создаваемый тег-хелпер использует классы glyphicon, glyphicon-chevron-down и glyphicon-chevron-up, которые визуализируются с помощью библиотеки font-awesome. В данном случае подключение необходимых шрифтов font-awesome и определение используемых их классов для краткости производися в представлении, но в реальном приложении, конечно, все это можно вынести в отдельный css-файл.

Поскольку класс tag-хелпера в своем названии имеет несколько слов, которые начинаются с большой буквы - SortHeaderTagHelper, то в имени соотвествующего тега все части названия будут разделяться дефисом: <sort-header> (суффикс TagHelper при этом отбрасывается).

Через атрибуты тега sort-header мы можем передать значения для соотвествующих одноименных свойств класса SortHeaderTagHelper.

В итоге получился следующий проект:

TagHelper и сортировка в ASP.NET Core

Запустим проект и отсортируем по разным критериям:

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