问题
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.
回答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 => 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!).
回答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>
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;
}
回答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" }
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
- You must remember to call EndTag otherwise your HTML goes Foobar.
- 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.
回答4:
This is what I came up with, taking care of the following considerations:
- I save some typing with
T.Tag
afterusing T = HtmlTextWriterTag;
, which you might like or not - 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) 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(); })); } }
来源:https://stackoverflow.com/questions/414605/fluent-interface-for-rendering-html