【问题标题】:Fluent interface for rendering HTML用于呈现 HTML 的流畅界面
【发布时间】:2010-09-29 16:48:13
【问题描述】:

在我看来,使用 HtmlTextWriter 渲染 HTML 并不是非常直观,但是如果您在 Web 表单中实现 Web 控件,则必须使用它。我认为有可能为此创建一个流畅的界面,该界面看起来更像它输出的 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”使用的重载中使用的传递给 lambda 的参数是一个“HtmlAttributeManager”,它是一个简单的类,它包装了一个 HtmlTextWriter 以提供一个索引器,该索引器接受一个 HtmlTextWriterAttribute 和一个字符串值并返回实例,所以可以链接调用。我还为这个类提供了最常见属性的方法,例如“Name”、“Class”和“Id”,以便您可以将上面的第一个调用编写如下:

.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);
}

希望您能够“破译”这个 sn-p 输出的 HTML,至少是这样的想法。

请给我一些关于如何改进语法的想法,也许是更好的方法名称,也许是其他一些方法。

编辑: 我想看看相同的 sn-p 在不使用 fluent 接口的情况下会是什么样子可能会很有趣,以进行比较:

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();
}

编辑: 我可能应该更明确一点,其中一个目标是它应该产生尽可能少的开销,这就是我限制使用 lambdas 的原因。同样,起初我使用了一个表示标签的类,以便在渲染之前通过语法构建类似于 DOM 树的东西,尽管语法非常相似。我放弃了这个解决方案,因为它会产生轻微的内存开销。在使用 HtmlAttributeManager 类时仍然存在一些这种情况,我一直在考虑使用扩展方法来附加属性,但是我不能使用 indexer-syntax,而且它会使 HtmlTextWriter 的接口膨胀更多。

【问题讨论】:

  • 这很酷。在我回复之前我想问你一些事情......你为什么使用索引器语法?我看不出它有什么好处,而且显然它会侵入您的语法。我认为您可能有充分的理由忽略我。
  • 这是我能想到的最好的,如果我不使用它,语法将类似于 "t => t.Attribute(key, value).Attribute(key, value) “它变得相当大。您对如何执行此操作有其他想法吗?非常欢迎提出建议。
  • 是否有你不能这样做的原因:.Tag(HtmlTextWriterTag.Select).Attribute("Value", "1").Attribute("Title", "Selects the number 1" ).EndTag()
  • 这就是我在“...”中提到的内容。起初,我使用了一个表示标签的类,以便在渲染之前通过语法构建类似于 DOM 树的东西。 ." 我之前所做的编辑。可能这是一种更好的方法,但严格来说它不是扩展方法。
  • 好的,所以你没有这样做,因为你想在每个点写出文本,这样你就可以避免构造任何内存中的 dom 表示。那正确吗?如果是这样,我会看看我是否能想出一种方法来实现这一目标。这对我来说是一个有趣的挑战,我有一些时间可以消磨:)

标签: .net html render web-controls htmltextwriter


【解决方案1】:

我看到了两个问题:

  • 重复使用Tag(Tagname, …)。为什么不为每个标签名称提供扩展方法?诚然,这会使界面臃肿,并且需要编写大量代码(=> 代码生成!)。
  • 编译器/IDE 无法帮助您。特别是,它不检查缩进(当你自动缩进时它甚至会破坏它)。

这两个问题都可以通过使用 Lambda 方法来解决:

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 也是如此,甚至是 Microsoft 的 PLINQ 接口。

我前段时间考虑过的另一种方法实际上是在尝试模仿 Markaby,例如 sambo 的代码。主要区别在于我使用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.");
}

此代码没有其他方法的问题。另一方面,它为属性提供了较少的类型安全性和不太优雅的接口(对于“elegant”的给定定义)。

我可以编译这两个代码,甚至生成一些或多或少有意义的输出(即:HTML!)。

【讨论】:

  • 其实我有最常见的标签名称的扩展方法,所以我认为这是一个好主意。我玩过 lambda 方法,但它不能嵌套是一个交易破坏者,它不能与 VB 一起使用实际上是另一个问题,因为我正在处理一些用 VB 编写的大型应用程序。
  • 实际上,我刚刚注意到我的想法有几个严重的问题在实践中打破了它。我目前正在尝试解决其中的一些问题,并将在适当的时候相应地更新我的答案。
  • @Konrad,我偷了你的使用想法,因为我也认为它比 foreach 更直观
  • @Konrad,您能否就如何实施第二种方法提供一些指导?谢谢。
  • @Daniel:它所要做的就是实现IDisposable 并构建节点类的层次结构。我在pastie.org/1166790 上放了一个 q'n'd 示例代码。请注意,它的错误检查为零,不完整,没有 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 枚举来对方法进行代码生成。标签属性使用匿名对象进行管理;代码基本上反映了匿名类型,提取属性并将它们转换为属性,我认为这使生成的语法看起来非常干净。

代码生成结果:

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"); 
            }
        } 
    

    我认为它读起来很会并且支持嵌套。

    编辑我从 Konrad 那里偷了 using,因为它读起来好多了。

    我对原始提案有以下问题

    1. 您必须记住调用 EndTag 否则您的 HTML 会变为 Foobar。
    2. 您的 namspace 污染太严重了 HtmlTextWriterTag 重复了很多次,很难从开销中破译内容。

    我建议的方法可能效率稍低,但我认为它解决了这些问题并且非常易于使用。

    【讨论】:

    • 是的,你是对的,它看起来确实更漂亮。我正在开发的界面是在你别无选择的情况下使用的。
    • 我同意您的第二个示例中的代码读起来不错,但是,您认为它比我的方法读起来更好吗?这也引入了一些我想避免的轻微开销(构建对象的内存和时间)。也许我不应该对那非常轻微的开销如此小心。
    • 如果底层 HTMLTextWriter 抛出异常,那么所有嵌套的封闭 usings 都将被处理掉 - 他们是否应该尝试将结束标记写入流?那么他们应该抛出另一个异常吗?如果他们应该抓住它,那不会隐藏异常吗?
    • @Earwicker,很好,如果你在 finally 子句中,你无法弄清楚正在抛出异常,所以你会盲目地尝试关闭标签......我猜它只是其中之一这种方法的注意事项
    【解决方案4】:

    这是我想出的,考虑到以下几点:

    1. 我在using T = HtmlTextWriterTag; 之后用T.Tag 保存了一些输入, 您可能喜欢或不喜欢哪个
    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();
              }));
        }
      }
      

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-06
      相关资源
      最近更新 更多