Свободный интерфейс для рендеринга HTML
Рендеринг HTML с помощью HtmlTextWriter, на мой взгляд, не очень интуитивно понятен, но если вы реализуете веб-элементы управления в веб-формах, это то, с чем вам придется работать. Я подумал, что возможно было бы создать свободный интерфейс для этого, который будет немного больше похож на HTML, который он выводит. Я хотел бы знать, что люди думают о синтаксисе, который я придумал до сих пор.
public void Render(HtmlTextWriter writer)
{
writer
.Tag(HtmlTextWriterTag.Div, e => e[HtmlTextWriterAttribute.Id, "id"][HtmlTextWriterAttribute.Name,"name"][HtmlTextWriterAttribute.Class,"class"])
.Tag(HtmlTextWriterTag.Span)
.Text("Lorem")
.EndTag()
.Tag(HtmlTextWriterTag.Span)
.Text("ipsum")
.EndTag()
.EndTag();
}
"Tag", "Text" и "EndTag" являются методами расширения для класса HtmlTextWriter, который возвращает экземпляр, который он принимает, чтобы можно было объединять вызовы. Аргумент, передаваемый лямбда-выражению, используемому в перегрузке, используемой первым вызовом "Tag", является "HtmlAttributeManager", который представляет собой простой класс, который оборачивает HtmlTextWriter для предоставления индексатора, который принимает HtmlTextWriterAttribute и строковое значение и возвращает экземпляр так, эти звонки могут быть прикованы цепью. У меня также есть методы для этого класса для наиболее распространенных атрибутов, таких как "Имя", "Класс" и "Идентификатор", чтобы вы могли написать первый вызов выше следующим образом:
.Tag(HtmlTextWriterTag.Div, e => e.Id("id").Name("name").Class("class"))
Немного более длинный пример:
public void Render(HtmlTextWriter writer)
{
writer
.Tag(HtmlTextWriterTag.Div, a => a.Class("someClass", "someOtherClass"))
.Tag(HtmlTextWriterTag.H1).Text("Lorem").EndTag()
.Tag(HtmlTextWriterTag.Select, t => t.Id("fooSelect").Name("fooSelect").Class("selectClass"))
.Tag(HtmlTextWriterTag.Option, t => t[HtmlTextWriterAttribute.Value, "1"][HtmlTextWriterAttribute.Title, "Selects the number 1."])
.Text("1")
.EndTag(HtmlTextWriterTag.Option)
.Tag(HtmlTextWriterTag.Option, t => t[HtmlTextWriterAttribute.Value, "2"][HtmlTextWriterAttribute.Title, "Selects the number 2."])
.Text("2")
.EndTag(HtmlTextWriterTag.Option)
.Tag(HtmlTextWriterTag.Option, t => t[HtmlTextWriterAttribute.Value, "3"][HtmlTextWriterAttribute.Title, "Selects the number 3."])
.Text("3")
.EndTag(HtmlTextWriterTag.Option)
.EndTag(HtmlTextWriterTag.Select)
.EndTag(HtmlTextWriterTag.Div);
}
Надеюсь, вы сможете "расшифровать", какой HTML выводит этот фрагмент, по крайней мере, такова идея.
Пожалуйста, дайте мне какие-нибудь мысли о том, как можно улучшить синтаксис, возможно, лучше имена методов, возможно, какой-то другой подход все вместе.
Изменить: я подумал, что было бы интересно посмотреть, как будет выглядеть тот же фрагмент без использования плавного интерфейса, для сравнения:
public void RenderUsingHtmlTextWriterStandardMethods(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Class, "someClass someOtherClass");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.RenderBeginTag(HtmlTextWriterTag.H1);
writer.Write("Lorem");
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Id, "fooSelect");
writer.AddAttribute(HtmlTextWriterAttribute.Name, "fooSelect");
writer.AddAttribute(HtmlTextWriterAttribute.Class, "selectClass");
writer.RenderBeginTag(HtmlTextWriterTag.Select);
writer.AddAttribute(HtmlTextWriterAttribute.Value, "1");
writer.AddAttribute(HtmlTextWriterAttribute.Title, "Selects the number 1.");
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write("1");
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Value, "2");
writer.AddAttribute(HtmlTextWriterAttribute.Title, "Selects the number 2.");
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write("2");
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Value, "3");
writer.AddAttribute(HtmlTextWriterAttribute.Title, "Selects the number 3.");
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write("3");
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
}
РЕДАКТИРОВАТЬ: Я, вероятно, должен быть немного более четким в том, что одна из целей этого заключается в том, что это должно повлечь за собой как можно меньше накладных расходов, поэтому я ограничил использование лямбд. Также сначала я использовал класс, представляющий тег, чтобы синтаксис перед рендерингом создавал нечто похожее на DOM-дерево, хотя синтаксис был очень похожим. Я отказался от этого решения из-за небольших накладных расходов памяти, которые оно несет. Некоторые из них присутствуют в использовании класса HtmlAttributeManager. Я думал о том, чтобы использовать методы расширения и для добавления атрибутов, но я не могу использовать синтаксис indexer, также он раздувает интерфейс HtmlTextWriter. даже больше.
4 ответа
Я вижу две проблемы:
- Повторное использование
Tag(Tagname, …)
, Почему бы не предложить методы расширения для каждого имени тега? По общему признанию, это раздувает интерфейс и довольно много пишет (=> генерация кода!). - Компилятор /IDE не помогает вам. В частности, он не проверяет отступ (он даже уничтожит его, когда вы сделаете отступ автоматически).
Обе проблемы могут быть решены с помощью лямбда-подхода:
writer.Write(body => new Tag[] {
new Tag(h1 => "Hello, world!"),
new Tag(p => "Indeed. What a lovely day.", new Attr[] {
new Attr("style", "color: red")
})
});
Это всего лишь один из основных подходов. API, безусловно, потребуется гораздо больше работы. В частности, вложение одного и того же тега не будет работать из-за конфликтов имен аргументов. Кроме того, этот интерфейс не будет работать (или вообще) с VB. Но, к сожалению, то же самое относится и к другим современным API.NET, даже к интерфейсу PLINQ от Microsoft.
Другой подход, о котором я думал некоторое время назад, на самом деле пытается эмулировать Markaby, как код самбо. Основное отличие в том, что я использую using
блоки вместо foreach
, таким образом, используя RAII:
using (var body = writer.body("xml:lang", "en")) {
using (var h1 = body.h1())
h1.AddText("Hello, World!");
using (var p = body.p("style", "color: red"))
p.AddText("Indeed. What a lovely day.");
}
Этот код не имеет проблем другого подхода. С другой стороны, он обеспечивает меньшую безопасность типов для атрибутов и менее элегантный интерфейс (для данного определения "элегантного").
Я получаю оба кода для компиляции и даже создаю более или менее значимый вывод (например, HTML!).
Я хотел иметь такой синтаксис:
using (var w = new HtmlTextWriter(sw))
{
w.Html()
.Head()
.Script()
.Attributes(new { type = "text/javascript", src = "somescript.cs" })
.WriteContent("var foo='bar'")
.EndTag()
.EndTag()
.Body()
.P()
.WriteContent("some content")
.EndTag()
.EndTag()
.EndTag();
}
Чтобы добиться этого, я добавил методы расширения в HtmlTextWriter, хотя контейнер, вероятно, был бы более подходящим (меня больше интересовало, чтобы он работал в первую очередь!) Чувствуя себя ленивым, я не хотел писать метод для каждый из доступных тегов, поэтому я кодирую методы с помощью шаблона t4, просматривая перечисление System.Web.UI.HtmlTextWriterTag. Атрибуты тегов управляются с помощью анонимных объектов; код в основном отражает анонимный тип, вытягивает свойства и превращает их в атрибуты, которые, я думаю, придают результирующему синтаксису очень чистый вид.
Результат кодовой легенды:
using System;
using System.Web.UI;
using System.Collections.Generic;
/// <summary>
/// Extensions for HtmlTextWriter
/// </summary>
public static partial class HtmlWriterTextTagExtensions
{
static Stack<Tag> tags = new Stack<Tag>();
/// <summary>
/// Opens a Unknown Html tag
/// </summary>
public static HtmlTextWriter Unknown(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("Unknown", null));
return writer;
}
/// <summary>
/// Opens a A Html tag
/// </summary>
public static HtmlTextWriter A(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("a", null));
return writer;
}
/// <summary>
/// Opens a Acronym Html tag
/// </summary>
public static HtmlTextWriter Acronym(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("acronym", null));
return writer;
}
/// <summary>
/// Opens a Address Html tag
/// </summary>
public static HtmlTextWriter Address(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("address", null));
return writer;
}
/// <summary>
/// Opens a Area Html tag
/// </summary>
public static HtmlTextWriter Area(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("area", null));
return writer;
}
/// <summary>
/// Opens a B Html tag
/// </summary>
public static HtmlTextWriter B(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("b", null));
return writer;
}
/// <summary>
/// Opens a Base Html tag
/// </summary>
public static HtmlTextWriter Base(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("base", null));
return writer;
}
/// <summary>
/// Opens a Basefont Html tag
/// </summary>
public static HtmlTextWriter Basefont(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("basefont", null));
return writer;
}
/// <summary>
/// Opens a Bdo Html tag
/// </summary>
public static HtmlTextWriter Bdo(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("bdo", null));
return writer;
}
/// <summary>
/// Opens a Bgsound Html tag
/// </summary>
public static HtmlTextWriter Bgsound(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("bgsound", null));
return writer;
}
/// <summary>
/// Opens a Big Html tag
/// </summary>
public static HtmlTextWriter Big(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("big", null));
return writer;
}
/// <summary>
/// Opens a Blockquote Html tag
/// </summary>
public static HtmlTextWriter Blockquote(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("blockquote", null));
return writer;
}
/// <summary>
/// Opens a Body Html tag
/// </summary>
public static HtmlTextWriter Body(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("body", null));
return writer;
}
/// <summary>
/// Opens a Br Html tag
/// </summary>
public static HtmlTextWriter Br(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("br", null));
return writer;
}
/// <summary>
/// Opens a Button Html tag
/// </summary>
public static HtmlTextWriter Button(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("button", null));
return writer;
}
/// <summary>
/// Opens a Caption Html tag
/// </summary>
public static HtmlTextWriter Caption(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("caption", null));
return writer;
}
/// <summary>
/// Opens a Center Html tag
/// </summary>
public static HtmlTextWriter Center(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("center", null));
return writer;
}
/// <summary>
/// Opens a Cite Html tag
/// </summary>
public static HtmlTextWriter Cite(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("cite", null));
return writer;
}
/// <summary>
/// Opens a Code Html tag
/// </summary>
public static HtmlTextWriter Code(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("code", null));
return writer;
}
/// <summary>
/// Opens a Col Html tag
/// </summary>
public static HtmlTextWriter Col(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("col", null));
return writer;
}
/// <summary>
/// Opens a Colgroup Html tag
/// </summary>
public static HtmlTextWriter Colgroup(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("colgroup", null));
return writer;
}
/// <summary>
/// Opens a Dd Html tag
/// </summary>
public static HtmlTextWriter Dd(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("dd", null));
return writer;
}
/// <summary>
/// Opens a Del Html tag
/// </summary>
public static HtmlTextWriter Del(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("del", null));
return writer;
}
/// <summary>
/// Opens a Dfn Html tag
/// </summary>
public static HtmlTextWriter Dfn(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("dfn", null));
return writer;
}
/// <summary>
/// Opens a Dir Html tag
/// </summary>
public static HtmlTextWriter Dir(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("dir", null));
return writer;
}
/// <summary>
/// Opens a Div Html tag
/// </summary>
public static HtmlTextWriter Div(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("div", null));
return writer;
}
/// <summary>
/// Opens a Dl Html tag
/// </summary>
public static HtmlTextWriter Dl(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("dl", null));
return writer;
}
/// <summary>
/// Opens a Dt Html tag
/// </summary>
public static HtmlTextWriter Dt(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("dt", null));
return writer;
}
/// <summary>
/// Opens a Em Html tag
/// </summary>
public static HtmlTextWriter Em(this HtmlTextWriter writer)
{
WritePreceeding(writer);
tags.Push(new Tag("em", null));
return writer;
}
Если вам нужно делать много такого рода вещей, рассматривали ли вы какой-нибудь шаблонизатор, такой как NHaml?
В Ruby/Markaby это выглядело бы намного красивее.
div :class=>"someClass someOtherClass" do
h1 "Lorem"
select :id => "fooSelect", :name => "fooSelect", :class => "selectClass" do
option :title=>"selects the number 1", :value => 1 { "1" }
option :title=>"selects the number 2", :value => 2 { "2" }
option :title=>"selects the number 3", :value => 3 { "3" }
end
end
Вы можете портировать аналогичный подход к.Net
using(var d = HtmlTextWriter.Div.Class("hello"))
{
d.H1.InnerText("Lorem");
using(var s = d.Select.Id("fooSelect").Name("fooSelect").Class("fooClass"))
{
s.Option.Title("select the number 1").Value("1").InnerText("1");
}
}
Я думаю, что это читает довольно волю и поддерживает вложение.
РЕДАКТИРОВАТЬ Я украл использование от Конрада, потому что он читается намного лучше.
У меня есть следующие проблемы с оригинальным предложением
- Вы должны не забывать вызывать EndTag, иначе ваш HTML будет отображаться как Foobar.
- Ваше пространство слишком загрязнено. HtmlTextWriterTag повторяется тонну раз, и его трудно расшифровать из-за накладных расходов.
Мой предложенный подход потенциально немного менее эффективен, но я думаю, что он решает эти проблемы и будет очень прост в использовании.
Это то, что я придумал, учитывая следующие соображения:
- Я сохраняю набор текста с помощью
T.Tag
послеusing T = HtmlTextWriterTag;
что вам может понравиться или нет - Я хотел получить хоть какую-то безопасность для последовательности цепочки вызовов (
Debug.Assert
это просто для краткости, намерение должно быть ясным) Я не хотел оборачивать множество методов HtmlTextWriter.
using T = HtmlTextWriterTag; public class HtmlBuilder { public delegate void Statement(HtmlTextWriter htmlTextWriter); public HtmlBuilder(HtmlTextWriter htmlTextWriter) { this.writer = htmlTextWriter; } // Begin statement for tag; mandatory, 1st statement public HtmlBuilder B(Statement statement) { Debug.Assert(this.renderStatements.Count == 0); this.renderStatements.Add(statement); return this; } // Attribute statements for tag; optional, 2nd to nth statement public HtmlBuilder A(Statement statement) { Debug.Assert(this.renderStatements.Count > 0); this.renderStatements.Insert(this.cntBeforeStatements++, statement); return this; } // End statement for tag; mandatory, last statement // no return value, fluent block should stop here public void E() { Debug.Assert(this.renderStatements.Count > 0); this.renderStatements.Add(i => { i.RenderEndTag(); }); foreach (Statement renderStatement in this.renderStatements) { renderStatement(this.writer); } this.renderStatements.Clear(); this.cntBeforeStatements = 0; } private int cntBeforeStatements = 0; private readonly List<Statement> renderStatements = new List<Statement>(); private readonly HtmlTextWriter writer; } public class HtmlWriter { public delegate void BlockWithHtmlTextWriter(HtmlTextWriter htmlTextWriter); public delegate void BlockWithHtmlBuilder(HtmlBuilder htmlBuilder); public string Render(BlockWithHtmlTextWriter block) { StringBuilder stringBuilder = new StringBuilder(); using (StringWriter stringWriter = new StringWriter(stringBuilder)) { using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter)) { block(htmlTextWriter); } } return stringBuilder.ToString(); } public string Render(BlockWithHtmlBuilder block) { return this.Render((HtmlTextWriter htmlTextWriter) => block(new HtmlBuilder(htmlTextWriter))); } // small test/sample static void Main(string[] args) { HtmlWriter htmlWriter = new HtmlWriter(); System.Console.WriteLine(htmlWriter.Render((HtmlBuilder b) => { b.B(h => h.RenderBeginTag(T.Div) ) .A(h => h.AddAttribute("foo", "bar") ) .A(h => h.AddAttribute("doh", "baz") ) .E(); })); } }