Monday, January 5, 2009

HtmlTextWriter fluent interface

EDIT: The code demonstrated in the article is part of the Legend open source project you can find at http://legend.codeplex.com/.

If you've ever created a web control - and God knows I've created a few of them - you've most definitely come in contact with the HtmlTextWriter. If you have you've probably sworn a couple of times over how clunky it is. The amount of code you have to write to create quite simple pieces of html is just insane.

For example, take this very basic html tag:

<div id="id" name="name" class="class">Lorem ipsum.</div>

To create the same output with the HtmlTextWriter you'd have to write the following:

public void Render(HtmlTextWriter writer)
{
    writer.AddAttribute(HtmlTextWriterAttribute.Id, "id");
    writer.AddAttribute(HtmlTextWriterAttribute.Name, "name");
    writer.AddAttribute(HtmlTextWriterAttribute.Class, "class");
    writer.RenderBeginTag(HtmlTextWriterTag.Div);
    writer.Write("Lorem ipsum.");
    writer.RenderEndTag();
}

Now, you might think that that's a lot of code to accomplish very little, but that's not what's worst, it's utterly unmaintainable. Let's say we have more than one tag to render, and possibly child-tags of these tags, it soon becomes very hard to tell where a tag starts and ends and the structure of the output is hidden behind this procedural code. Well, what can we do to solve this? Extension methods to the rescue, what if we could write the above code in the following manner:

public void Render(HtmlTextWriter writer)
{
    writer
        .Tag(HtmlTextWriterTag.Div, e => e.Id("id").Name("name").Class("class"))
            .Text("Lorem ipsum.")
        .EndTag();
}

The "Tag", "Text" and "EndTag" methods are all extension methods for the HtmlTextWriter-class, this means that they're nothing more than static methods and they do not present any runtime overhead (memory wise at least, they do create an extra call on the stack).

Attributes

One of the most counter intuitive features of the HtmlTextWriter is that you specify attributes of a tag before you create the tag, to remedy that I created an overload of the Tag-extension where the second parameter is a delegate that takes an argument of the type HtmlAttributeManager. An HtmlAttributeManager is just a simple class that wraps an HtmlTextWriter to provide an interface for adding attributes fluently:

public static void Example(HtmlTextWriter writer)
{ 
    // All attributes can be set via the indexer that takes
    // the type of attribute to set and the value for it:
    writer.Tag(HtmlTextWriterTag.Div, e => e[HtmlTextWriterAttribute.Id, "id"]);

    // The indexer returns the HtmlAttributeManager-instance so calls
    // can be chained like this:
    writer.Tag(HtmlTextWriterTag.Div, e => e[HtmlTextWriterAttribute.Id, "id"][HtmlTextWriterAttribute.Name, "name"]);

    // Some common attributes can be set through named methods:
    writer.Tag(HtmlTextWriterTag.Div, e => e.Id("id").Name("name"));

    // Note that those calls can be chained to, you can even mix
    // calls to the indexer and the named methods like this:
    writer.Tag(HtmlTextWriterTag.Div, e => e.Id("id")[HtmlTextWriterAttribute.Title, "Lorem ipsum."].Class("class"));
}

Let's have a look at how the syntax looks for a little longer html-snippet, here I render the same html, first by a static string, and the via the fluent interface:

public void RenderUsingString(HtmlTextWriter writer)
{
    var html = @"
    <div class=""someClass someOtherClass"">
        <h1>Lorem</h1>
        <select id=""fooSelect"" name=""fooSelect"" class=""selectClass"">
            <option value=""1"" title=""Selects the number 1."">1</option>
            <option value=""2"" title=""Selects the number 2."">2</option>
            <option value=""3"" title=""Selects the number 3."">3</option>
        </select>
    </div>
    ";

    writer.Write(html);
}

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

Now that's a lot more readable than the same code would've been using the normal way of rendering with the HtmlTextWriter, actually, let's have a look at how that would look.

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

The code produced when using the fluent interface is a lot closer to the end result, the html code, so it's a lot easier to decipher (read), which means that it's  lot more maintainable.

I've also added some functionality for simple data binding and repeating, more about that next time.

1 comment:

  1. patrick - re HtmlTextWriter fluent interface,

    due to the wonders of google (and the odd keyword juxtapose - plus stackoverflow) i was able to home in on the above article that you did a wee while back.

    this is something that i've been looking to accomplish for quite some time (rather than the concatanation writer methods) and have been researching on/off to find good/great examples of putting this into practice. as you seem to have nailed it pretty well ,(tho i'm sure you'd disagree :)) i'd be interested to review any class libs that you've created as a result in order to drag my 'lazy ass' a step closer to understanding how you've put it all together. I appreciate that this may not be something that you are willing to do, but thought i'd ask anyway.

    you know the old scot's saying 'don't ask, dont get' :)

    thanks for the article - seriously made me realise that i wasn't alone in envisioning this way of approaching this topic.

    cheers

    jimi (#jimibt@dsl.pipex.com#)

    ReplyDelete