10

在我看来,使用 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);
}

希望您能够“破译”此代码段输出的 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();
}

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

4

4 回答 4

3

There are two issues that I see:

  • Repeated use of Tag(Tagname, …). Why not offer extension methods for each tag name? Admittedly, this bloats the interface and is quite a lot to write (=> code generation!).
  • The compiler/IDE doesn't assist you. In particular, it doesn't check indentation (it will even destroy it when you indent your automatically).

Both problems could perhaps be solved by using a Lambda approach:

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

This is just one basic approach. The API certainly would need a lot more work. In particular, nesting the same tag name won't work because of argument name conflicts. Also, this interface wouldn't work well (or at all) with VB. But then, the same is unfortunately true for other modern .NET APIs, even the PLINQ interface from Microsoft.

Another approach that I've thought about some time ago actually tries to emulate Markaby, like sambo's code. The main difference is that I'm using using blocks instead of foreach, thus making use of 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.");
}

This code doesn't have the problems of the other approach. On the other hand, it provides less type safety for the attributes and a less elegant interface (for a given definition of “<em>elegant”).

I get both codes to compile and even produce some more or less meaningful output (i.e.: HTML!).

于 2009-01-05T22:13:28.173 回答
3

我希望能够拥有这种语法:

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 添加了扩展方法,尽管容器可能更合适(首先我更感兴趣的是让它工作!)感觉很懒,我不想为每个可用的标签,所以我通过迭代 System.Web.UI.HtmlTextWriterTag 枚举使用 t4 模板对方法进行编码。标签属性使用匿名对象进行管理;代码基本上反映了匿名类型,提取属性并将它们转换为属性,我认为这使结果语法看起来非常干净。

代码生成结果:

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;
        }
于 2010-04-22T17:18:03.707 回答
1

If you need to do lots of this kind of stuff have you considered some sort of template engine like NHaml?

In Ruby/Markaby this would look so much prettier.

    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

You can port a similar approach to .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"); 
        }
    } 

I think it reads quite will and supports nesting.

EDIT I stole the using from Konrad cause it reads much better.

I have the following issues with the original proposal

  1. You must remember to call EndTag otherwise your HTML goes Foobar.
  2. Your namspace is too polluted HtmlTextWriterTag is repeated a ton of times and its hard to decipher the content from the overhead.

My suggested approach is potentially slightly less efficient, but I think it addresses these concerns and would be very easy to use.

于 2009-01-05T22:16:52.130 回答
0

This is what I came up with, taking care of the following considerations:

  1. I save some typing with T.Tag after using T = HtmlTextWriterTag;, which you might like or not
  2. I wanted to get at least some safety for the sequence of the invocation chain (the Debug.Assert is just for brevity, the intention should be clear)
  3. I didn't want to wrap the myriad of methods of the 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();
            }));
      }
    }
    
于 2009-10-23T12:26:30.487 回答