Создаем читабельную разметку при помощи Tag Helpers в ASP.NET 5

Razor — неотъемлемая часть ASP.NET. Он позволяет динамически генерировать HTML-разметку, используя конструкции языка C#. Но так ли уж хорош синтаксис Razor для описания сложных сценариев?

Давайте взглянем на следующий код:

@foreach (var item in items) {
  <li @(item.IsActive ? "class=\"active\"" : string.Empty)>@item.name</li>
}

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

@foreach (var item in items) {
  var cl = new List<string>();
  if (item.IsActive)
    cl.Add("active");

  if (item.IsFeatured)
    cl.Add("featured");

  <li @(cl.Any() ? "class=\"" + String.Join(" ", cl.ToArray() + "\"" : string.Empty)>@item.name</li>
}

Можно использовать собственные хелперы, но это тоже не всегда лучшим образом отражается на читаемости кода.

Для решения этой проблемы в ASP.NET 5 появились специальные объекты — Tag Helpers. Они позволяют вынести сложную логику из шаблона Razor в небольшие классы, а в разметке оставить только HTML-атрибуты. К примеру, приведенный выше код мог бы быть переписан вот так:

@foreach (var item in items) {
  <li asp-active="@item.IsActive" asp-featured="@item.IsFeatured">@item.name</li>
}

Подключение собственного хелпера

С ASP.NET уже поставляется базовый набор хелперов. Но вся сила тег хелперов в простоте их создания в конкретно вашей ситуации.

Каждый хелпер — это небольшой объект, унаследованный от TagHelper. Также при помощи атрибута HtmlTargetElement мы должны указать в каких ситуациях применим этот тег хелпер.

[HtmlTargetElement("li", Attributes = "asp-active, asp-featured")]
public class ItemTagHelper : TagHelper
{
}

Простейший тег-хелпер готов. Пока он ничего не делает, но к этому мы вернемся чуть позже. А пока давайте сообщим ASP.NET, что мы создали собственный хелпер.

Для подключения тег-хелпера появилась директива @addTagHelper. Чтобы использовать тег хелпер, мы должны добавить его при помощи этой директивы в разметке страницы. Если мы хотим использовать этот хелпер на всех страницах приложения, то вызов @addTagHelper можно добавить в файл _ViewImports.cshtml:

@addTagHelper "*, WebApplication15"

Таким вызовом мы добавляем все тег-хелперы из указанной сборки.

Получение значений атрибутов внутри хелпера

Вернемся к реализации логики хелпера. Нам требуется получить значения атрибутов asp-active и asp-featured. Для этого добавим два свойства в класс хелпера и пометим их атрибутом HtmlAttributeName — это укажет на то, что эти свойства нужно заполнить значениями этих HTML-атрибутов.

[HtmlTargetElement("li", Attributes = "asp-active, asp-featured")]
public class ItemTagHelper : TagHelper
{
  [HtmlAttributeName("asp-active")]
  public bool Active { get; set; }

  [HtmlAttributeName("asp-featured")]
  public bool Featured { get; set; }
}

Обработка элемента

Работа тег хелпера предполагает модификацию исходного HTML-элемента (иначе зачем нам вообще нужен хелпер). В нашем примере мы хотим превратить значение атрибутов asp-active и asp-featured в набор CSS-классов.

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

[HtmlTargetElement("li", Attributes = "asp-active, asp-featured")]
public class ItemTagHelper : TagHelper
{
  [HtmlAttributeName("asp-active")]
  public bool Active { get; set; }

  [HtmlAttributeName("asp-featured")]
  public bool Featured { get; set; }

  public override void Process(TagHelperContext context, TagHelperOutput output)
  {
  }
}

Второй параметр (TagHelperOutput) позволяет модифицировать исходный тег. Добавим немного логики, получим такой код:

[HtmlTargetElement("li", Attributes = "asp-active, asp-featured")]
public class ItemTagHelper : TagHelper
{
  [HtmlAttributeName("asp-active")]
  public bool Active { get; set; }

  [HtmlAttributeName("asp-featured")]
  public bool Featured { get; set; }

  public override void Process(TagHelperContext context, TagHelperOutput output)
  {
    TagBuilder builder = new TagBuilder(output.TagName);

    if (Active)
      builder.AddCssClass("active");

    if (Featured)
      builder.AddCssClass("featured");

    output.MergeAttributes(builder);
  }
}

Теперь можем поменять код разметки.

Было:

@foreach (var item in items) {
  var cl = new List<string>();
  if (item.IsActive)
    cl.Add("active");

  if (item.IsFeatured)
    cl.Add("featured");

  <li string.empty)="" :="" &quot;\&quot;&quot;="" +="" cl.toarray()="" &quot;,="" string.join(&quot;="" &quot;class="\&quot;&quot;" ?="" @(cl.any()="">@item.name
}</li></string>

Стало:

@foreach (var item in items) {
  <li asp-featured="&quot;@item.IsFeatured&quot;" asp-active="&quot;@item.IsActive&quot;">@item.name
}</li>

Асинхронность

В зависимости от ситуации, может потребоваться использовать паттерн async/await внутри метода Process. Для этих случаев можно реализовать метод ProcessAsync() вместо Process():

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
  // ...
}

Использовать или нет?

С точки зрения чистоты разметки тег-хелперы выглядят привлекательно. Многие части разметки сразу так и хочется вынести в тег хелперы. Но подойти к этому вопросу стоит осознанно, иначе проект рискует превратиться в кашу из тег-хелперов.

Я бы посоветовал выделить наиболее встречающиеся блоки и обернуть их в тег-хелперы, а специфичные случаи оставить в разметке. Вот хорошие кандидаты на вынесение в тег хелперы:

  • атрибуты валидации
  • генерация URL для SPA-приложений
  • управление стилями элемента (указание CSS-класса)
  • заготовки элементов формы (label + input)
  • заготовки часто повторяющихся конструкций из CSS-фреймворков
  • и т.д.