В очередном процессе написания веб приложения под 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