Мы продолжаем нашу колонку по теме ASP.NET Core публикацией от Станислава Ушакова ( JustStas) — team lead из компании DataArt. В статье Стас рассказывает о способах создания своих собственных тег-хелперов в ASP.NET Core. Предыдущие статьи из колонки всегда можно прочитать по ссылке #aspnetcolumn — Владимир Юнев
В прошлый раз мы рассмотрели уже существующие в ASP.NET Core 1.0 тег-хэлперы. В этот раз мы рассмотрим создание собственных тег-хэлперов, что может упростить генерацию небольших переиспользуемых участков HTML-разметки.


Введение


Формально тег-хэлпером является любой класс, наследующий от интерфейса ITagHelper.На практике напрямую от ITagHelper наследовать не надо, удобнее наследовать от класса TagHelper, переопределяя метод Process / ProcessAsync, которые рассмотрим по ходу статьи.

aspnetcolumngithubСовет! Вы можете попробовать все самостоятельно или загрузив исходный код из GitHub https://github.com/StanislavUshakov/CustomTagHelpers.

Инфраструктура


Мы будем использовать Visual Studio 2015 Community Edition. Создадим новый ASP.NET 5 сайт (рисунок 1).


Чтобы не начинать с полностью пустого проекта, выберем шаблон“WebApplication” (рисунок 2), но удалим аутентификацию: “Change Authentication”, далее выбрать “No authentication”.


В результате получим проект с уже подключенным MVC, одним контроллером и тремя действиями: Index, About, Contact. Добавим новую папку в проект, назовем ее TagHelpers: необязательно называть ее именно так, но можно сказать, что это договоренность, и лучше ее соблюдать – все собственноручно написанные тег-хэлперы класть в эту папку. Получим следующую структуру проекта (рисунок 3).



EmailTagHelper


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

<address>
<strong>Support:</strong><a href="mailto:Support@example.com">Support@example.com</a><br />
<strong>Marketing:</strong><a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

Создадим тег-хэлпер email, который будем использовать следующим образом:

<email>Support</email>

Генерируя следующую HTML разметку:

<a href="mailto:Support@example.com ">Support@example.com</a>

Такой тег-хэлпер пригодится, если надо генерировать много ссылок на адреса электронной почты одного домена. Добавим новый файл в папку TagHelpers, назовем его EmailTagHelper.cs.

usingMicrosoft.AspNet.Razor.TagHelpers;

namespaceCustomTagHelpersAndViewComponents.TagHelpers
{
    public class EmailTagHelper : TagHelper
    {
        public string MailTo { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "a";    // заменим тег <email> на тег <a>
        }
    }
}

Уже на примере этого самого простого тег-хэлпера можно увидеть следующее:
  • Тег-хэлперы придерживаются следующего правила именования по умолчанию: тег, используемый в cshtml представлениях, равен имени класса тег-хэлпера минус TagHelper. В нашем случае используем тег email. Дальше будет показано, как можно переопределить это поведение по умолчанию. Можно было бы назвать наш тег-хэлпер и просто Email, механизм работы остался бы тот же, но по правилу именования постфикс TagHelper требуется добавлять.
  • Переопределяя метод Process, мы указываем какой код будет выполняться нашим тег-хэлпером. Также класс TagHelper определяет метод ProcessAsync с такими же параметрами.
  • Параметр context содержит информацию о контексте выполнения текущего HTML тега.
  • Парметр output хранит данные об оригинальном HTML элементе, который был использован в представлении, и его содержимом.

Для того, чтобы использовать созданный нами тег-хэлпер, добавим в файл Views/_ViewImports.cshtml директиву для его подключения.

@using CustomTagHelpersAndViewComponents
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, CustomTagHelpersAndViewComponents"

Мы используем подстановочный символ, чтобы добавить в область видимости сразу все тег-хэлперы из сборки. Первый аргумент директивы @addTagHelper — это имя тег-хэлпера (или звездочка), второй – сборка, в которой они ищутся. Если мы хотим добавить один тег-хэлпер, а не все сразу, то надо указать его полное имя (FQN – fully qualified name):

@using CustomTagHelpersAndViewComponents
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "CustomTagHelpersAndViewComponents.TagHelpers.EmailTagHelper, CustomTagHelpersAndViewComponents"

Теперь обновим код представления Views/Home/Contact.cshtml, переписав ссылки на электронную почту:

<address>
    <strong>Support:</strong><email>Support</email><br />
    <strong>Marketing:</strong><email>Marketing</email>
</address>

Visual Studio 2015 замечательно подсвечивает тег-хэлперы, наш вновь созданный – не исключение:


Откроем сайт в браузере и увидим, что тег email заменился на тег a, самое время добавить необходимую логику для генерации правильной ссылки на электронную почту. Необходимую информацию будем передавать с помощью атрибута email-to, домен электронной почты будет задан константой в тег-хэлпере.

using Microsoft.AspNet.Razor.TagHelpers;

namespace CustomTagHelpersAndViewComponents.TagHelpers
{
    public class EmailTagHelper : TagHelper
    {
        private const string EmailDomain = "example.com";

        public string MailTo { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "a";
            string address = MailTo + "@" + EmailDomain;
            output.Attributes["href"] = "mailto:" + address;
            output.Content.SetContent(address);
        }
    }
}

Отметим интересные моменты этого кода. Первый момент: свойство MailTo из PascalCase в C# коде станет low-kebab-case в коде представления: email-to, как это и принято сейчас во фронт-енде. Последняя строка метода устанавливает содержимое созданного тега. Наряду с таким синтаксисом добавления атрибутов output.Attributes[«href»] = «mailto:» + address; можно использовать метод output.Attributes.Add.
Теперь обновим представление Views/Home/Contact.cshtml:

<address>
    <strong>Support:</strong> <email mail-to="Support"></email><br />
    <strong>Marketing:</strong> <email mail-to="Marketing"></email>
</address>

В результате будет сгенерирована следующая HTML разметка:

<address>
    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

Заметим, что сейчас мы не можем написать <email mail-to=«Support» />, для этого надо добавить специальный атрибут, подключив сборку Microsoft.AspNet.Razor.TagHelpers:

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]

Также вместо TagStructure.WithoutEndTag можно использовать значение TagStructure.NormalOrSelfClosing, в этом случае можно будет писать как <email></email>, так и <email />.

Тег-хэлпер bold


Напишем простой тег-хэлпер, который можно будет применять к различным HTML тегам. Пусть он превращает текст в полужирный для всех HTML тегов, к которым применяется. Нам опять потребуется атрибут HtmlTargetElement. Добавим в папку TagHelpers новый файл BoldTagHelper.cs со следующим содержимым:

using Microsoft.AspNet.Razor.TagHelpers;

namespace CustomTagHelpersAndViewComponents.TagHelpers
{
    [HtmlTargetElement(Attributes = "bold")]
    public class BoldTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.RemoveAll("bold");
            output.PreContent.SetHtmlContent("<strong>");
            output.PostContent.SetHtmlContent("</strong>");
        }
    }
}

Теперь в атрибут HtmlTargetElement мы не передаем имя тега, к которому тег-хэлпер применяется, а имя HTML атрибута, который можно добавлять к разным HTML тегам. Поменяем разметку в представлении About.cshtml:

<p bold>Use this area to provide additional information.</p>

В результате сгенерируется следующая HTML разметка:

<p><strong>Use this area to provide additional information.</strong></p>

Однако, если мы попробуем использовать bold не как атрибут, а как имя HTML тега, код тег-хэлпера вызван не будет. Это можно исправить, добавив к классу тег-хэлпера атрибут HtmlTargetElement еще раз, только передав ему имя тега bold:

[HtmlTargetElement("bold")]

Теперь в представлении мы можем написать и так:

<bold>Use this area to provide additional information.</bold >

Если мы добавим сразу оба атрибута HtmlTargetElement к нашему классу тег-хэлпера:

[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]

То они будут между собой связаны логическим ИЛИ: или тег bold, или атрибут bold. Можно объединять больше двух атрибутов HtmlTargetElement, все они будут связаны через ИЛИ. Если мы хотим связать их через И: применять к тегу bold с атрибутом bold, нужно объединить эти условия в одном атрибуте HtmlTargetElement:

[HtmlTargetElement("bold", Attributes = "bold")]

Теперь рассмотрим код внутри метода Process. Метод output.Attributes.RemoveAll(«bold»); удаляет атрибут bold у HTML тега. Затем мы должны все содержимое тега обрамить тегом strong. Для этого используем свойства PreContent и PostContent объекта output. Они позволяют добавить новое содержимое внутри тега, но до основного содержимого и сразу после. Схематично это выглядит вот так:

<p bold>PRECONTENT Use this area to provide additional information. POST_CONTENT</p>

Если же мы хотим добавить новое содержимое вне тега, надо использовать свойства PreElement и PostElement, тогда получим следующее:

PRE_ELEMENT<p bold>Use this area to provide additional information.</p>POST_ELEMENT

И не забываем использовать метод SetHtmlContent, а не SetContent, если мы хотим добавить HTML теги, иначе метод SetContent перекодирует переданную строку и тег strong не применится.

Асинхронный тег-хэлпер copyright


До этого мы перегружали метод Process, сейчас перегрузим его асинхронную версию: метод ProcessAsync. Напишем простой тег-хэлпер, который будет выводить такую разметку:

<p>© 2016 MyCompany</p>

Добавим в папку TagHelpers новый файл CopyrightTagHelper.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Razor.TagHelpers;

namespace CustomTagHelpersAndViewComponents.TagHelpers
{
    /// <summary>
    /// Custom tag helper for generating copyright information. Content inside will be added after: © Year
    /// </summary>
    [HtmlTargetElement("copyright")]
    public class CopyrightTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            var content = await output.GetChildContentAsync();
            string copyright = $"<p>© {DateTime.Now.Year} {content.GetContent()}</p>";
            output.Content.SetHtmlContent(copyright);
        }
    }
}

В отличие от синхронного метода Process, метод ProcessAsync возвращает объект типа Task. Также к методу мы добавили модификатор async, потому что внутри метода содержимое тега получаем с помощью конструкции await output.GetChildContentAsync();. Затем с помощью нового механизма строк форматирования в C# 6 формируем содержимое тега и добавляем его с помощью метода output.Content.SetHtmlContent(copyright);. Во всех остальных аспектах, этот простой асинхронный тег-хэлпер не отличается от синхронного.

Best practices и выводы


Чтобы написать хороший тег-хэлпер, надо знать, как написаны отличные. Для этого посмотрим на стандартные тег-хэлперы, доступные в MVC: Microsoft.AspNetCore.Mvc.TagHelpers. И добавим в свои созданные тег-хэлперы достаточное количество комментариев и проверок.

Весь исходный код проекта досутпен в ГитХабе по адресу: Custom Tag Helpers.

Собственные тег-хэлперы позволяют оптимизировать часто встречающие задачи по генерации HTML разметки небольшого размера. Если же вы хотите инкапсулировать более сложную логику представления, лучше использовать View Components.

Пробуйте ASP.NET Core 1.0, сообщайте о багах, добавляйте фичи!

Авторам


Друзья, если вам интересно поддержать колонку своим собственным материалом, то прошу написать мне на vyunev@microsoft.com для того чтобы обсудить все детали. Мы разыскиваем авторов, которые могут интересно рассказать про ASP.NET и другие темы.


Об авторе


Станислав Ушаков
Senior .Net Developer / Team lead в DataArt

Родился, учился в Воронеже. .Net'ом профессионально занимаюсь уже больше 6 лет, до этого даже MFC видел. Люблю программировать (недавно дошли руки до Ардуино, играюсь), читать книги (особенно бумажные), играть в ЧтоГдеКогда (спортивное), учить и разбираться во всем новом. Хочу наконец защитить написанную кандидатскую.

JustStas

Анонс! Глубокий интенсив на конференции DevCon 2016


image

Мы рады объявить о проведении глубокого интенсив-курса по ASP.NET Core в рамках [конференции DevCon 2016](https://events.techdays.ru/DevCon/2016/registration). Этот курс будет проходить во второй день конференции и займет полный день конференции в течение 6 часов.

Разработка современных веб-приложений на открытой платформе ASP.NET 5
В рамках этого интенсива участники примут участие в увлекательном и полном приключений путешествии, посвященном разработке веб-приложений на новейшей платформе ASP.NET 5. Мы пройдем весь путь от нуля до полноценного приложения, развернутого в облаке. И по дороге участники смогут остановиться для изучения внутреннего устройства ASP.NET, работы с реляционными и NoSQL базами данных с помощью Entity Framework 7, разработки приложений на фреймворке MVC, построении моделей, представлений и бизнес-логики, создании REST API, организации процессов непрерывной разработки и тестирования с помощью Git и Visual Studio Online, а также развертывания с помощью Azure и контейнеров Docker. В конце путешествия все участники пройдут посвящение и станут заслуженными рыцарями ASP.NET.

Регистрация на конференцию DevCon 2016 уже открыта! Регистрируйтесь здесь.
Приходилось ли вам использовать собственные Tag Helpers?

Проголосовало 77 человек. Воздержалось 17 человек.

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

Комментарии (2)


  1. ZOXEXIVO
    02.03.2016 19:02

    Очень сомнительное занятие заниматься подобными вещами в данное время.
    Если бы это все было к моменту выхода Razor, то тогда согласен, удобно, но сейчас смотреть на такое просто не хочется, да и сам MS подготавливает почву для того чтобы позже перескочить на UI-фреймворки (с возможной изоморфностью)


    1. JustStas
      03.03.2016 11:59

      Да, если проект пишется с нуля, то особо таким заниматься не потребуется, но если есть унаследованный код, написанный на старых версиях MVC, а в нем есть кастомные HtmlHelpers вида Html.MyHelper(), то перенести их в новую версию будет удобно через тег-хэлперы. Да и не очень сложно.