Свободный интерфейс для рендеринга HTML - PullRequest
10 голосов
/ 06 января 2009

Рендеринг 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 выводит этот фрагмент, по крайней мере, такова идея.

Пожалуйста, дайте мне какие-нибудь мысли о том, как можно улучшить синтаксис, возможно, лучше имена методов, может быть, какой-то другой подход все вместе.

Edit: Я подумал, что было бы интересно посмотреть, как будет выглядеть тот же фрагмент без использования плавного интерфейса, для сравнения:

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. Я думал о том, чтобы использовать методы расширения и для добавления атрибутов, но я не могу использовать синтаксис indexer, также он раздувает интерфейс HtmlTextWriter. еще больше.

Ответы [ 4 ]

3 голосов
/ 22 апреля 2010

Я хотел бы иметь такой синтаксис:

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;
        }
3 голосов
/ 06 января 2009

Я вижу две проблемы:

  • Повторное использование 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!).

1 голос
/ 06 января 2009

Если вам нужно делать много такого рода вещей, рассматривали ли вы какой-нибудь шаблонизатор, такой как 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"); 
        }
    } 

Я думаю, что он читает вполне будет и поддерживает вложение.

РЕДАКТИРОВАТЬ Я украл использование у Конрада, потому что оно читается намного лучше.

У меня есть следующие проблемы с исходным предложением

  1. Вы должны помнить, чтобы вызывать EndTag, иначе ваш HTML будет отображаться как Foobar.
  2. Ваше пространство слишком загрязнено. HtmlTextWriterTag повторяется тонну раз, и его трудно расшифровать из служебной информации.

Мой предложенный подход потенциально немного менее эффективен, но я думаю, что он решает эти проблемы и будет очень прост в использовании.

0 голосов
/ 23 октября 2009

Это то, что я придумал, принимая во внимание следующие соображения:

  1. Я сохраняю набор текста с T.Tag после using T = HtmlTextWriterTag;, что вам может понравиться или нет
  2. Я хотел получить хоть какую-то безопасность для последовательности цепочек вызовов (Debug.Assert только для краткости, намерение должно быть ясным)
  3. Я не хотел оборачивать множество методов 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();
            }));
      }
    }
    
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...