Fluent interface for rendering HTML
Asked Answered
K

4

10

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.

Keg answered 5/1, 2009 at 21:33 Comment(7)
This is cool. I want to ask you something before I respond ... why are you using the indexer syntax? I don't see a benefit to it, and clearly it is intrusive into your syntax. I think you probably have a good reason for it that I'm overlooking.Sewerage
It was the best I could come up with, if I didn't use it the syntax would be something like "t => t.Attribute(key, value).Attribute(key, value)" which grows quite big. Do you have another idea for how to do this, suggestions are more than welcome.Sian
Was there a reason you couldn't do this: .Tag(HtmlTextWriterTag.Select).Attribute("Value", "1").Attribute("Title", "Selects the number 1").EndTag()Sewerage
This is what I touch upon in the "... 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 ..." edit i previously made. Possibly it is a better way to go but then it wouldn't strictly be extension methods.Sian
OK, so you didn't do that because you want to write out the text at each point, which lets you avoid constructing any in-memory dom representation. Is that right? If so, I will see if I can come up with a way to achieve that. It's an interesting challenge to me and I have some time to kill :)Sewerage
Charlie - That's exactly right.Sian
legend.codeplex.com has the project Legend.Web that corresponds to this question.Fault
C
3

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;
        }
Cyrus answered 5/1, 2009 at 21:33 Comment(0)
H
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 “elegant”).

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

Honest answered 5/1, 2009 at 22:13 Comment(5)
Actually I have extension methods for the most common tag-names, so I think that's a great idea. I've played around with the lambda approach, but it can't be nested is one deal breaker, that it doesn't play with VB is actually another since I work on some large apps written in VB.Sian
Actually, I've just noticed that my idea has several serious problems that break it in practice. I'm currently trying to work around some of them and will update my answer accordingly in due course.Honest
@Konrad, I stole your using idea cause I also think its much more intuitive than foreachEminence
@Konrad, could you give some pointers on how the second approach would be implemented? Thanks.Fault
@Daniel: All it needs to do is implement IDisposable and build a hierarchy of node classes. I’ve put a q’n’d example code together at pastie.org/1166790. Notice that it’s got zero error checking, is incomplete, doesn’t HTML escape arguments or text, doesn’t do pretty printing or tons of other sensible things. But it compiles the example from above and gives meaningful output.Honest
E
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.

Eminence answered 5/1, 2009 at 22:16 Comment(4)
Yes, you're right it does look much prettier. The interface I'm working on is for use in situations where you have no choice.Sian
I agree that the code in your second example reads well, however, do you think it reads better than my approach? This also introduces some slight overhead (memory and time for construction of the objects) that I want to avoid. Maybe I shouldn't be so careful with that very slight overhead.Sian
The downer with this is that if the underlying HTMLTextWriter throws an exception, then all the nested enclosing usings will be Disposed of - should they attempt to write a closing tag to the stream? And so should they throw another exception? And if they should catch it, won't that hide exceptions?Saliferous
@Earwicker, good point if you are in a finally clause you can not figure out that an exception is being thrown so you will blindly try to close the tags... I guess its just one of those caveats with this approachEminence
S
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();
            }));
      }
    }
    
Smelt answered 23/10, 2009 at 12:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.