В очередном процессе написания веб приложения под ASP.NET MVC с использованием Bootstrap поймал себя на мысли, что неизбежное создание HTML-тэгов можно было бы и подсократить. Речь пойдёт не о наборе пользовательских элементов управления для расширения пространства Html.*, а о том, что лежит немножечко глЫбже. Для торопыг предлагаю глянуть сюда (GitHub), а для остальных добро пожаловать под кат.
Имеется HTML-тэг, содержащий в себе название, классы, стили, атрибуты и т.д. В .NET для «ручного» создания сей красоты предполагается использование класса TagBuilder и постепенное его заполнение нужными мета и просто данными.
Но!
Регулярное использование этого класса показалось мне слишком муторным. Постоянные *.AddCssClass, *.Attributes.Add, *.MergeAttribute и *.ToString(TagRenderMode.SelfClosing) — в какой-то момент начинают раздражать своей пошаговостью.
Вот к примеру, как выглядит стандартного элемента-кнопки:
Добавим сюда то, что порой 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
На указанном примере кнопки преимущества результата применения TagDecorator несколько неочевидны:
Но вот уже на примере Bootstrap card — всё уже становится намного приятней глазу:
Используя TagBuilder
Версия с TagDecorator
Основным из которых я считаю то, что каскадная имплементация порой сильно сдвигает код вправо, часто выводя за пределы видения разработчика, и чем сложнее иерархия — тем сильнее
+ Line of codes — сокращается. В простейших элементах есть выигрыш примерно 1-2 строк, в сложном HTML дереве — примерно на 1/3 по аналогии если использовать TagBuilder.
+ Наглядность — явно видно что где и какая вложенность. Всё иерархически интуитивно и проще понять.
+ Расширяемость — необходим какой-то специфический случай/атрибут — просто добавляем Extension. Необходима проверка на условие — добавляем Extension.
Поначалу я подумывал о том, чтобы на основе данных классов создать полностью специализированные тэги, которые бы допускали бы в подсказке только определённые extensions — к примеру в тэге button убрать подсказку расширения AddReference, однако впоследствии отказался от данных планов в угоду универсальности. Но в общем и целом — данное решение теперь сильно помогает мне в моих проектах.
Предполагалось ещё создание NuGet пакета, но за недостатком времени — всё откладывается.
Задача
Имеется 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 пакета, но за недостатком времени — всё откладывается.
