Свободный интерфейс для рендеринга 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, который возвращает экземпляр, который он принимает, чтобы вызовы могли быть скованы. Аргумент, переданный лямбде, используемой в перегрузке, используемой при первом вызове "Тег", представляет собой "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();
}
EDIT:
Вероятно, я должен быть немного более откровенным в том, что одна из целей заключается в том, что он должен понести как можно меньше накладных расходов, поэтому я ограничил использование лямбда. Кроме того, сначала я использовал класс, который представлял тег, так что что-то похожее на DOM-дерево было создано синтаксисом перед рендерингом, однако синтаксис был очень похож. Я отказался от этого решения для небольшого объема памяти, который он несет. Есть еще кое-что из этого в использовании класса HtmlAttributeManager, я думал об использовании методов расширения для добавления атрибутов, но я не могу использовать синтаксис индексатора, также он раздувает интерфейс HtmlTextWriter даже больше.
Ответы
Ответ 1
Есть две проблемы, которые я вижу:
- Повторное использование
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. Но тогда, к сожалению, это относится и к другим современным .NET API, даже к интерфейсу 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!).
Ответ 2
Я хотел иметь такой синтаксис:
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. Атрибуты тегов управляются с использованием анонимных объектов; код в основном отражает анонимный тип, вытаскивает свойства и превращает их в атрибуты, которые, как я думаю, дают результирующий синтаксис очень чистый вид.
Результат codegend:
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;
}
Ответ 3
Если вам нужно делать много такого рода вещей, вы считали какой-то механизм шаблонов, например, 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");
}
}
Я думаю, что он читает вполне воля и поддерживает вложенность.
EDIT Я украл использование у Konrad, потому что он читает намного лучше.
У меня есть следующие проблемы с оригинальным предложением
- Вы должны помнить, что вы вызываете EndTag, иначе ваш HTML-код будет Foobar.
- Ваше namspace слишком загрязнено. HtmlTextWriterTag повторяется много раз, и его трудно расшифровать содержимое из служебных данных.
Мой предложенный подход потенциально немного менее эффективен, но я думаю, что он решает эти проблемы и будет очень прост в использовании.
Ответ 4
Это то, что я придумал, учитывая следующие соображения:
- Я сохраняю некоторую типизацию с
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();
}));
}
}