Свободный интерфейс для рендеринга 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();
            }));
      }
    }