Skip to content Skip to sidebar Skip to footer

Fluent Interface For Rendering Html

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 thought tha

Solution 1:

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 =>newTag[] {
    newTag(h1 =>"Hello, world!"),
    newTag(p =>"Indeed. What a lovely day.", newAttr[] {
        newAttr("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!).

Solution 2:

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>publicstaticpartialclassHtmlWriterTextTagExtensions
{
    static Stack<Tag> tags = new Stack<Tag>();



        ///<summary>///  Opens a Unknown Html tag///</summary>publicstatic HtmlTextWriter Unknown(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("Unknown",  null));
            return writer;
        }

        ///<summary>///  Opens a A Html tag///</summary>publicstatic HtmlTextWriter A(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("a",  null));
            return writer;
        }

        ///<summary>///  Opens a Acronym Html tag///</summary>publicstatic HtmlTextWriter Acronym(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("acronym",  null));
            return writer;
        }

        ///<summary>///  Opens a Address Html tag///</summary>publicstatic HtmlTextWriter Address(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("address",  null));
            return writer;
        }

        ///<summary>///  Opens a Area Html tag///</summary>publicstatic HtmlTextWriter Area(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("area",  null));
            return writer;
        }

        ///<summary>///  Opens a B Html tag///</summary>publicstatic HtmlTextWriter B(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("b",  null));
            return writer;
        }

        ///<summary>///  Opens a Base Html tag///</summary>publicstatic HtmlTextWriter Base(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("base",  null));
            return writer;
        }

        ///<summary>///  Opens a Basefont Html tag///</summary>publicstatic HtmlTextWriter Basefont(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("basefont",  null));
            return writer;
        }

        ///<summary>///  Opens a Bdo Html tag///</summary>publicstatic HtmlTextWriter Bdo(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("bdo",  null));
            return writer;
        }

        ///<summary>///  Opens a Bgsound Html tag///</summary>publicstatic HtmlTextWriter Bgsound(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("bgsound",  null));
            return writer;
        }

        ///<summary>///  Opens a Big Html tag///</summary>publicstatic HtmlTextWriter Big(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("big",  null));
            return writer;
        }

        ///<summary>///  Opens a Blockquote Html tag///</summary>publicstatic HtmlTextWriter Blockquote(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("blockquote",  null));
            return writer;
        }

        ///<summary>///  Opens a Body Html tag///</summary>publicstatic HtmlTextWriter Body(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("body",  null));
            return writer;
        }

        ///<summary>///  Opens a Br Html tag///</summary>publicstatic HtmlTextWriter Br(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("br",  null));
            return writer;
        }

        ///<summary>///  Opens a Button Html tag///</summary>publicstatic HtmlTextWriter Button(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("button",  null));
            return writer;
        }

        ///<summary>///  Opens a Caption Html tag///</summary>publicstatic HtmlTextWriter Caption(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("caption",  null));
            return writer;
        }

        ///<summary>///  Opens a Center Html tag///</summary>publicstatic HtmlTextWriter Center(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("center",  null));
            return writer;
        }

        ///<summary>///  Opens a Cite Html tag///</summary>publicstatic HtmlTextWriter Cite(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("cite",  null));
            return writer;
        }

        ///<summary>///  Opens a Code Html tag///</summary>publicstatic HtmlTextWriter Code(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("code",  null));
            return writer;
        }

        ///<summary>///  Opens a Col Html tag///</summary>publicstatic HtmlTextWriter Col(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("col",  null));
            return writer;
        }

        ///<summary>///  Opens a Colgroup Html tag///</summary>publicstatic HtmlTextWriter Colgroup(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("colgroup",  null));
            return writer;
        }

        ///<summary>///  Opens a Dd Html tag///</summary>publicstatic HtmlTextWriter Dd(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("dd",  null));
            return writer;
        }

        ///<summary>///  Opens a Del Html tag///</summary>publicstatic HtmlTextWriter Del(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("del",  null));
            return writer;
        }

        ///<summary>///  Opens a Dfn Html tag///</summary>publicstatic HtmlTextWriter Dfn(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("dfn",  null));
            return writer;
        }

        ///<summary>///  Opens a Dir Html tag///</summary>publicstatic HtmlTextWriter Dir(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("dir",  null));
            return writer;
        }

        ///<summary>///  Opens a Div Html tag///</summary>publicstatic HtmlTextWriter Div(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("div",  null));
            return writer;
        }

        ///<summary>///  Opens a Dl Html tag///</summary>publicstatic HtmlTextWriter Dl(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("dl",  null));
            return writer;
        }

        ///<summary>///  Opens a Dt Html tag///</summary>publicstatic HtmlTextWriter Dt(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("dt",  null));
            return writer;
        }

        ///<summary>///  Opens a Em Html tag///</summary>publicstatic HtmlTextWriter Em(this HtmlTextWriter writer)
        {
            WritePreceeding(writer);
            tags.Push(new Tag("em",  null));
            return writer;
        }

Solution 3:

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" } 
        endend

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.

Solution 4:

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;
    
    publicclassHtmlBuilder {
      publicdelegatevoidStatement(HtmlTextWriter htmlTextWriter);
    
      publicHtmlBuilder(HtmlTextWriter htmlTextWriter) {
        this.writer = htmlTextWriter;
      }
      // Begin statement for tag; mandatory, 1st statementpublic HtmlBuilder B(Statement statement) {
        Debug.Assert(this.renderStatements.Count == 0);
        this.renderStatements.Add(statement);
        returnthis;
      }
      // Attribute statements for tag; optional, 2nd to nth statementpublic HtmlBuilder A(Statement statement) {
        Debug.Assert(this.renderStatements.Count > 0);
        this.renderStatements.Insert(this.cntBeforeStatements++, statement);
        returnthis;
      }
      // End statement for tag; mandatory, last statement// no return value, fluent block should stop herepublicvoidE() {
        Debug.Assert(this.renderStatements.Count > 0);
        this.renderStatements.Add(i => { i.RenderEndTag(); });
        foreach (Statement renderStatement inthis.renderStatements) {
            renderStatement(this.writer);
        }
        this.renderStatements.Clear(); this.cntBeforeStatements = 0;
      }
      privateint cntBeforeStatements = 0;
      privatereadonly List<Statement> renderStatements = new List<Statement>();
      privatereadonly HtmlTextWriter writer;
    }
    
    publicclassHtmlWriter {
      publicdelegatevoidBlockWithHtmlTextWriter(HtmlTextWriter htmlTextWriter);
      publicdelegatevoidBlockWithHtmlBuilder(HtmlBuilder htmlBuilder);
    
      publicstringRender(BlockWithHtmlTextWriter block) {
        StringBuilder stringBuilder              = new StringBuilder();
        using (StringWriter stringWriter         = new StringWriter(stringBuilder)) {
            using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter)) {
                block(htmlTextWriter);
            }
        }
        return stringBuilder.ToString();
      }
      publicstringRender(BlockWithHtmlBuilder block) {
        returnthis.Render((HtmlTextWriter htmlTextWriter) => 
                block(new HtmlBuilder(htmlTextWriter)));
      }
      // small test/samplestaticvoidMain(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();
            }));
      }
    }
    

Post a Comment for "Fluent Interface For Rendering Html"