Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 that it might be possible to create a fluent interface for this that reads a bit more like the HTML it outputs. I would like to know what people think of the syntax that I've come up with so far.

    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" and "EndTag" are extension methods for the HtmlTextWriter class that returns the instance it takes in so that calls can be chained. The argument passed to the lambda used in the overload used by the first call to "Tag" is a "HtmlAttributeManager", which is simple class that wraps an HtmlTextWriter to provide an indexer that takes an HtmlTextWriterAttribute and a string value and returns the instance so that calls can be chained. I also have methods on this class for the most common attributes, such as "Name", "Class" and "Id" so that you could write the first call above as follows:

.Tag(HtmlTextWriterTag.Div, e => e.Id("id").Name("name").Class("class"))

A little longer example:

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

Hopefully you'll be able to "decipher" what HTML this snippet outputs, at least that's the idea.

Please give me any thoughts on how the syntax can be improved upon, maybe better method names, maybe some other approach all together.

Edit: I thought it might be interesting to see what the same snippet would look like without the use of the fluent interface, for comparison:

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: I should probably be a little more explicit in that one of the goals with this is that it should incur as little overhead as possible, this is why I've limited the use of lambdas. Also at first I used a class that represented a tag so that something similar to a DOM-tree was built by the syntax before the rendering, the syntax was very similar though. I abandoned this solution for the slight memory overhead it incurs. There are still some of this present in the use of the HtmlAttributeManager class, I have been thinking about using extension methods for the appending of attributes also, but the I can't use the indexer-syntax, also it bloats the interface of the HtmlTextWriter even more.

like image 491
Patrik Hägne Avatar asked Jan 05 '09 21:01

Patrik Hägne


1 Answers

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!).

like image 177
Konrad Rudolph Avatar answered Oct 06 '22 03:10

Konrad Rudolph