.net - Interfaz fluida para renderizar HTML
web-controls htmltextwriter (4)
Renderizar HTML con HtmlTextWriter no es increíblemente intuitivo en mi opinión, pero si está implementando controles web en formularios web, es con lo que debe trabajar. Pensé que sería posible crear una interfaz fluida para esto que se parece un poco más al HTML que genera. Me gustaría saber qué opina la gente sobre la sintaxis que he presentado hasta ahora.
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();
}
"Etiqueta", "Texto" y "Etiqueta final" son métodos de extensión para la clase HtmlTextWriter que devuelve la instancia que toma para que las llamadas puedan encadenarse. El argumento pasado al lambda utilizado en la sobrecarga utilizada por la primera llamada a "Etiqueta" es un "HtmlAttributeManager", que es una clase simple que envuelve un HtmlTextWriter para proporcionar un indexador que toma un HtmlTextWriterAttribute y un valor de cadena y devuelve la instancia que las llamadas se pueden encadenar También tengo métodos en esta clase para los atributos más comunes, como "Nombre", "Clase" e "Id" para que pueda escribir la primera llamada anterior de la siguiente manera:
.Tag(HtmlTextWriterTag.Div, e => e.Id("id").Name("name").Class("class"))
Un poco más de ejemplo:
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);
}
Con suerte, podrá "descifrar" qué HTML produce este fragmento, al menos esa es la idea.
Por favor, denme algunas ideas sobre cómo se puede mejorar la sintaxis, tal vez mejores nombres de métodos, tal vez algún otro enfoque todos juntos.
Editar: pensé que podría ser interesante ver cómo se vería el mismo fragmento sin el uso de la interfaz fluida, para comparar:
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();
}
EDITAR: Probablemente debería ser un poco más explícito en que uno de los objetivos con esto es que debe incurrir en la menor sobrecarga posible, es por eso que he limitado el uso de lambdas. También al principio usé una clase que representaba una etiqueta para que la sintaxis construyera algo similar a un árbol DOM antes de la representación, aunque la sintaxis era muy similar. Abandoné esta solución por la leve sobrecarga de memoria en la que incurre. Todavía hay algo de este presente en el uso de la clase HtmlAttributeManager, también he estado pensando en usar métodos de extensión para agregar atributos, pero no puedo usar la sintaxis del indexador, también se hincha la interfaz del HtmlTextWriter aún más.
Hay dos problemas que veo:
- Uso repetido de
Tag(Tagname, …)
. ¿Por qué no ofrecer métodos de extensión para cada nombre de etiqueta? Es cierto que esto abota la interfaz y es mucho para escribir (=> generación de código). - El compilador / IDE no te ayuda. En particular, no comprueba la sangría (incluso la destruirá cuando sangres automáticamente).
Ambos problemas podrían quizás resolverse usando un enfoque Lambda:
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")
})
});
Este es solo un enfoque básico. La API ciertamente necesitaría mucho más trabajo. En particular, anidar el mismo nombre de etiqueta no funcionará debido a conflictos de nombre de argumento. Además, esta interfaz no funcionaría bien (o no funcionaría) con VB. Pero luego, desafortunadamente, lo mismo ocurre con otras API .NET modernas, incluso con la interfaz PLINQ de Microsoft.
Otro enfoque en el que he pensado hace un tiempo realmente intenta emular a Markaby, como el código de Sambo. La principal diferencia es que estoy usando bloques en lugar de foreach
, haciendo uso de 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.");
}
Este código no tiene los problemas del otro enfoque. Por otro lado, proporciona menos seguridad de tipo para los atributos y una interfaz menos elegante (para una definición dada de " elegante ").
Obtengo ambos códigos para compilar e incluso produzco un resultado más o menos significativo (es decir, ¡HTML!).
Si necesita hacer muchas cosas de este tipo, ¿ha considerado algún tipo de motor de plantillas como NHaml?
En Ruby / Markaby esto se vería mucho más bonito.
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
Puede portar un enfoque similar a .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");
}
}
Creo que dice bastante voluntad y admite la anidación.
EDITAR Me robó el uso de Konrad porque lee mucho mejor.
Tengo los siguientes problemas con la propuesta original
- Debe recordar llamar a EndTag, de lo contrario su HTML se convierte en Foobar.
- Su namspace está demasiado contaminado HtmlTextWriterTag se repite una tonelada de veces y es difícil descifrar el contenido de la sobrecarga.
Mi enfoque sugerido es potencialmente un poco menos eficiente, pero creo que aborda estas preocupaciones y sería muy fácil de usar.
Esto es lo que se me ocurrió, teniendo en cuenta las siguientes consideraciones:
-
T.Tag
algo de tipeo conT.Tag
después deusing T = HtmlTextWriterTag;
, que te puede gustar o no - Quería obtener al menos algo de seguridad para la secuencia de la cadena de invocación (
Debug.Assert
es solo por brevedad, la intención debería ser clara) No quería envolver la miríada de métodos del 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(); })); } }
Quería poder tener este tipo de sintaxis:
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();
}
Para lograr esto, he agregado métodos de extensión al HtmlTextWriter, aunque un contenedor probablemente sería más apropiado (¡estaba más interesado en hacerlo funcionar antes que nada!) Sintiéndome perezoso, no quería escribir un método para cada una de las etiquetas disponibles, así que codifiqué los métodos usando una plantilla t4 iterando a través de System.Web.UI.HtmlTextWriterTag enum. Los atributos de etiqueta se administran usando objetos anónimos; el código básicamente se refleja en el tipo anónimo, extrae las propiedades y las convierte en atributos que, en mi opinión, dan a la sintaxis resultante una apariencia muy limpia.
El resultado codegend:
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;
}