Каскадная генерация HTML-тегов посредством C#

в 7:27, , рубрики: .net, Bootstrap, C#, html, HTML generators

В очередном процессе написания веб приложения под ASP.NET MVC с использованием Bootstrap поймал себя на мысли, что неизбежное создание HTML-тэгов можно было бы и подсократить. Речь пойдёт не о наборе пользовательских элементов управления для расширения пространства Html.*, а о том, что лежит немножечко глЫбже. Для торопыг предлагаю глянуть сюда (GitHub), а для остальных добро пожаловать под кат.

Задача

Имеется HTML-тэг, содержащий в себе название, классы, стили, атрибуты и т.д. В .NET для «ручного» создания сей красоты предполагается использование класса TagBuilder и постепенное его заполнение нужными мета и просто данными.

Но!

Регулярное использование этого класса показалось мне слишком муторным. Постоянные *.AddCssClass, *.Attributes.Add, *.MergeAttribute и *.ToString(TagRenderMode.SelfClosing) — в какой-то момент начинают раздражать своей пошаговостью.

Вот к примеру, как выглядит стандартного элемента-кнопки:

<!-- HTML -->
<button type="button" class="btn btn-success">Success</button>

// C#
var tb = new TagBuilder("button");
tb.Attributes.Add("type", "button");
tb.AddCssClass("btn");
tb.AddCssClass("btn-success");
tb.SetInnerText("Success");
var htmlString = tb.ToString(TagRenderMode.Normal);

Добавим сюда то, что порой HTML-тэги требуют вложенности, а значит требуется наличие одного или многих *.InnerHtml с параметром, каждый из которых в свою очередь должен создаваться точно так же — длинно-размеренно-пошагово — то становится понятно, что хочется чего-то менее рутинного.

Вот так и родился класс TagDecorator.

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

Решение

Ссылка: TagDecorator

На начальном этапе решение состояло из двух классов, но к ним впоследствии добавился ещё один:

TagDecorator — основной работающий класс, который ответственнен за превращение обычного текста в класс, представляющий тэг (TagWrapper), к которому могут прицепляться дополнительные extensions. В существующем примере есть как общие функции AddAttribute, AddCss, так и частные функции AddType, AddName, AddId — создающие конкретные атрибуты.

TagWrapper — класс, представляющий тэг. Был создан, чтобы по возможности полностью отойти от TagBuilder и новые extensions в IntelliSense не путались со свойствами класса TagBuilder.

Tags — класс, необходимый для разделения begin/end tags, чтобы сделать возможной реализацию обрамляющих блоков HTML, использующихся в RazorView

@using(Html.SuchExtension) { 
    //...
}.

Примеры

На указанном примере кнопки преимущества результата применения TagDecorator несколько неочевидны:

var htmlString = "button".ToTag()
                    .AddType("button")
                    .AddCss(new[] {"btn", "btn-success"})
                    .SetText("Success")
                    .ToString();

Но вот уже на примере Bootstrap card — всё уже становится намного приятней глазу:

Используя TagBuilder

var divMain = new TagBuilder("div");
divMain.AddCssClass("card");
divMain.Attributes.Add("style", "width: 18rem;");

var img = new TagBuilder("img");
img.AddCssClass("card-img-top");
img.Attributes.Add("src", "...");
img.Attributes.Add("alt", "Card image cap");

var divBody = new TagBuilder("div");
divBody.AddCssClass("card-body");

var h = new TagBuilder("h5");
h.AddCssClass("card-title");
h.SetInnerText("Card title");

var p = new TagBuilder("p");
p.AddCssClass("card-text");
p.SetInnerText("Some quick example text to build on the card title and make up the bulk of the card's content.");

var a = new TagBuilder("a");
a.Attributes.Add("href", "#");
a.AddCssClass("btn");
a.AddCssClass("btn-primary");
a.SetInnerText("Go somewhere");

divBody.InnerHtml += h.ToString(TagRenderMode.Normal);
divBody.InnerHtml += p.ToString(TagRenderMode.Normal);
divBody.InnerHtml += a.ToString(TagRenderMode.Normal);

divMain.InnerHtml += img.ToString(TagRenderMode.Normal);
divMain.InnerHtml += divBody.ToString(TagRenderMode.Normal);

return divMain.ToString(TagRenderMode.Normal);

Версия с TagDecorator

var htmlString = "div".ToTag()
    .AddCss("card")
    .AddAttribute("style", "width: 18rem;")
    .InnerHtml(new []
    {
        "img".ToTag()
            .AddCss("card-img-top")
            .AddAttributes(new[] {new[] {"src", "..."}, new[] {"alt", "Card image cap"}}),
        "div".ToTag()
            .AddCss("card-body")
            .InnerHtml(new []
            {
                "h5".ToTag().AddCss("card-title").SetText("Card title"),
                "p".ToTag().AddCss("card-text").SetText("Some quick example text to build on the card title and make up the bulk of the card's content."),
                "a".ToTag().AddCss(new[] {"btn", "btn-primary"}).AddAttribute("href", "#").SetText("Go somewhere")
            })
    }).ToString();

Результаты

Минусы

Основным из которых я считаю то, что каскадная имплементация порой сильно сдвигает код вправо, часто выводя за пределы видения разработчика, и чем сложнее иерархия — тем сильнее

Плюсы

+ Line of codes — сокращается. В простейших элементах есть выигрыш примерно 1-2 строк, в сложном HTML дереве — примерно на 1/3 по аналогии если использовать TagBuilder.

+ Наглядность — явно видно что где и какая вложенность. Всё иерархически интуитивно и проще понять.

+ Расширяемость — необходим какой-то специфический случай/атрибут — просто добавляем Extension. Необходима проверка на условие — добавляем Extension.

Возможные улучшения

Поначалу я подумывал о том, чтобы на основе данных классов создать полностью специализированные тэги, которые бы допускали бы в подсказке только определённые extensions — к примеру в тэге button убрать подсказку расширения AddReference, однако впоследствии отказался от данных планов в угоду универсальности. Но в общем и целом — данное решение теперь сильно помогает мне в моих проектах.

Предполагалось ещё создание NuGet пакета, но за недостатком времени — всё откладывается.

Автор: White_Scorpion

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js