Fluent interface for rendering HTML

前端 未结 4 1344
不思量自难忘°
不思量自难忘° 2020-12-30 09:21

Rendering HTML with the HtmlTextWriter isn\'t incredibly intuitive in my opinion, but if you\'re implementing web controls in web forms it\'s what you have to work with. I t

相关标签:
4条回答
  • 2020-12-30 09:51

    I wanted to be able to have this kind of syntax:

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

    In order to acheive this I've added extension methods to the HtmlTextWriter although a container would probably be more appropriate (I was more interested in getting it to work first of all!) Feeling lazy, I didn't want to write a method for each of the available tags, so I codegend the methods using a t4 template by iterating through the System.Web.UI.HtmlTextWriterTag enum. Tag attributes are managed using anonymous objects; the code basically reflects on the anonymous type, pulls out the properties and turns them into attributes which I think gives the resultant syntax a very clean appearance.

    The codegend result:

    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;
            }
    
    0 讨论(0)
  • 2020-12-30 09:53

    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.

    0 讨论(0)
  • 2020-12-30 09:53

    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();
              }));
        }
      }
      
    0 讨论(0)
  • 2020-12-30 10:04

    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 “elegant”).

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

    0 讨论(0)
提交回复
热议问题