2009-01-05 6 views
10

Le rendu HTML avec HtmlTextWriter n'est pas incroyablement intuitif à mon avis, mais si vous mettez en place des contrôles Web dans des formulaires Web, c'est ce que vous devez travailler. J'ai pensé qu'il serait peut-être possible de créer une interface fluide pour cela, qui ressemble un peu plus au HTML qu'elle produit. J'aimerais savoir ce que les gens pensent de la syntaxe que j'ai trouvée jusqu'à présent.Interface fluide pour le rendu HTML

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 », « Texte » et « EndTag » sont des méthodes d'extension pour la classe HtmlTextWriter qui retourne l'instance prend en sorte que les appels peuvent être chaînés. L'argument passé au lambda utilisé dans la surcharge utilisée par le premier appel à "Tag" est un "HtmlAttributeManager", qui est une simple classe qui enveloppe un HtmlTextWriter pour fournir un indexeur qui prend un HtmlTextWriterAttribute et une valeur de chaîne et retourne l'instance que les appels peuvent être enchaînés. J'ai aussi avoir des méthodes de cette classe pour les attributs les plus communs, tels que le « Nom », « classe » et « Id » afin que vous puissiez écrire le premier appel ci-dessus comme suit:

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

Un petit exemple plus:

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

Espérons que vous serez en mesure de « déchiffrer » ce HTML cette sorties de snippet, au moins c'est l'idée. S'il vous plaît donnez-moi des idées sur la façon dont la syntaxe peut être améliorée, peut-être de meilleurs noms de méthodes, peut-être une autre approche tous ensemble.

Edit: Je pensais que ce serait peut-être intéressant de voir ce que le même extrait ressemblerait sans l'utilisation de l'interface fluide, à titre de comparaison:

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: Je devrais probablement être un peu plus explicite en ce sens que l'un des objectifs est que cela devrait avoir le moins de frais possible, c'est pourquoi j'ai limité l'utilisation de lambdas. Aussi au début j'ai utilisé une classe qui représentait une balise pour que quelque chose de similaire à un arbre DOM ait été construit par la syntaxe avant le rendu, la syntaxe était cependant très similaire. J'ai abandonné cette solution pour la légère surcharge de mémoire qu'elle encourt. Il y a encore une partie de ceci présent dans l'utilisation de la classe HtmlAttributeManager, j'ai réfléchi à l'utilisation de méthodes d'extension pour l'ajout d'attributs, mais je ne peux pas utiliser la syntaxe indexer, aussi cela gonfle l'interface du HtmlTextWriter encore plus.

+0

C'est cool. Je veux vous demander quelque chose avant de répondre ... pourquoi utilisez-vous la syntaxe de l'indexeur? Je ne vois aucun avantage à cela, et il est clair que cela est intrusif dans votre syntaxe. Je pense que vous avez probablement une bonne raison pour cela que je néglige. –

+0

C'était le meilleur que je pouvais trouver, si je ne l'utilisais pas la syntaxe serait quelque chose comme "t => t.Attribute (clé, valeur) .Attribute (clé, valeur)" qui grandit assez grand. Avez-vous une autre idée de comment faire cela, les suggestions sont plus que bienvenues. –

+0

Y avait-il une raison pour laquelle vous ne pouviez pas faire ceci: .Tag (HtmlTextWriterTag.Select) .Attribute ("Value", "1"). Attribut ("Title", "Sélectionne le numéro 1"). EndTag() –

Répondre

3

Il y a deux questions que je vois:

  • L'utilisation répétée de Tag(Tagname, …). Pourquoi ne pas proposer des méthodes d'extension pour chaque nom de tag? Certes, cela bloque l'interface et est beaucoup à écrire (=> génération de code!).
  • Le compilateur/IDE ne vous aide pas. En particulier, il ne vérifie pas l'indentation (il va même le détruire lorsque vous indentez votre automatiquement).

Les deux problèmes pourraient être résolus en utilisant une approche 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") 
    }) 
}); 

Ceci est juste une approche de base. L'API aurait certainement besoin de beaucoup plus de travail. En particulier, l'imbrication du même nom de tag ne fonctionnera pas à cause des conflits de noms d'arguments. En outre, cette interface ne fonctionnerait pas bien (ou pas du tout) avec VB. Mais alors, la même chose est malheureusement vraie pour les autres API .NET modernes, même l'interface PLINQ de Microsoft.

Une autre approche à laquelle j'ai pensé il y a quelque temps essaie d'émuler Markaby, comme le code de sambo. La principale différence est que j'utilise using blocs au lieu de foreach, rendant ainsi l'utilisation 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."); 
} 

Ce code n'a pas les problèmes de l'autre approche. D'autre part, il fournit moins de sécurité de type pour les attributs et une interface moins élégante (pour une définition donnée de "élégant").

Je reçois les deux codes pour compiler et même produire une sortie plus ou moins significative (par exemple: HTML!).

+0

En fait, j'ai des méthodes d'extension pour les noms de tags les plus courants, donc je pense que c'est une bonne idée. J'ai joué avec l'approche lambda, mais il ne peut pas être imbriqué est un briseur de deal, qu'il ne joue pas avec VB est en fait une autre puisque je travaille sur de grandes applications écrites en VB. –

+0

En fait, je viens de remarquer que mon idée a plusieurs problèmes sérieux qui la cassent en pratique. J'essaie actuellement de contourner certains d'entre eux et mettra à jour ma réponse en conséquence. –

+0

J'ai mis à jour le code et ajouté une autre proposition. –

1

Si vous avez besoin de faire beaucoup de ce genre de choses, avez-vous considéré une sorte de moteur de template comme NHaml?

Dans Ruby/Markaby cela serait tellement plus joli.

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 

Vous pouvez le port d'une approche similaire à .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"); 
     } 
    } 

Je pense qu'il lit tout à fait la volonté et prend en charge l'imbrication.

EDIT J'ai volé l'utilisation de Konrad car elle se lit beaucoup mieux.

J'ai les questions suivantes avec la proposition initiale

  1. Vous devez vous rappeler d'appeler EndTag sinon votre HTML va Foobar.
  2. Votre namspace est trop pollué HtmlTextWriterTag est répété une tonne de fois et il est difficile de déchiffrer le contenu de l'en-tête.

Mon approche suggérée est potentiellement légèrement moins efficace, mais je pense qu'elle répond à ces préoccupations et serait très facile à utiliser.

+0

Oui, vous avez raison, il semble beaucoup plus joli. L'interface sur laquelle je travaille est à utiliser dans des situations où vous n'avez pas le choix. –

+0

Je suis d'accord que le code dans votre second exemple se lit bien, cependant, pensez-vous qu'il se lit mieux que mon approche? Cela introduit également une légère surcharge (mémoire et temps pour la construction des objets) que je veux éviter. Peut-être que je ne devrais pas être si prudent avec ces très légers frais généraux. –

+0

@Patrik J'ai élargi ma réponse ... –

0

C'est ce que je suis venu avec, en prenant soin des considérations suivantes:

  1. J'économiser quelques secondes avec T.Tag après using T = HtmlTextWriterTag;, que vous pourriez aimer ou non
  2. Je voulais obtenir au moins une certaine sécurité pour la séquence de la chaîne d'invocation (le Debug.Assert est juste par souci de concision, l'intention doit être clair)
  3. Je ne voulais pas envelopper la myriade de méthodes de 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(); 
         })); 
        } 
    } 
    
3

Je voulais être en mesure d'avoir ce genre de syntaxe:

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

Pour acheive cela, j'ai ajouté des méthodes d'extension du HtmlTextWriter bien qu'un conteneur serait probablement plus approprié (j'étais plus intéressé à le faire fonctionner tout d'abord! Sentant paresseux, je ne voulais pas écrire une méthode pour chacune des balises disponibles, donc je codegend les méthodes en utilisant un template t4 en itérant à travers l'enum System.Web.UI.HtmlTextWriterTag. Les attributs de balise sont gérés à l'aide d'objets anonymes. le code reflète fondamentalement sur le type anonyme, retire les propriétés et les transforme en attributs qui je pense donne à la syntaxe résultante une apparence très propre.

Le résultat 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; 
     } 
+0

+1 pour le funkiness –

+0

Pouvez-vous mettre cela sur GitHub? Ce serait un projet sympa –

+0

Je l'ai publié sur codeplex: http://fluenthtmlwriter.codeplex.com/ –

Questions connexes