ASPNL logo (1 kb)
zaterdag 17 mei 2008




Microsoft MVP

.NET Codewise Community
<< vorige | overzicht | volgende >>

ASP.NET custom controls maken met stijl

Door Michiel van Otegem
30 maart 2006

De weergave van een custom control verzorgen is een uitdaging op zichzelf. Ervoor zorgen dat een custom control doet wat je wilt is maar de helft van het werk. Als je aan de visuele kant van de zaak begint moet je nog allerlei logica verzorgen die de HTML genereert die naar de browser gestuurd wordt. Als je dit echt goed in de hand wil hebben kan dit een saai en vervelend werkje zijn, zeker als je ook nog wilt dat de control er goed uit ziet in verschillende browsers.

De HTML van een control wordt gegenereerd in de laatste stap van de levenscyclus van een control, net voordat de control opgeruimd wordt. Het genereren van HTML wordt verzorgd door de Render-metjode, die je kunt overschrijven om custom HTML te genereren. Als je een nieuwe Web Custom Control maakt in Visual Studio .NET (VS.NET), is het skelet van de code al aanwezig waarin de Render-methode wordt overschreven. De rest is niet meer dan een invuloefening. De enige parameter van de Render-methode is een HtmlTextWriter-object. Dit is een speciaal soort TextWriter-object dat HTML naar de output stream schrijft. Het HtmlTextWriter-object biedt een verzameling Write-methodes die je kunt gebruiken om HTML te maken. De meest basale methodes zijn Write en WriteLine die geërfd worden van het TextWriter-object. Als je de Text-eigenschap van je control in rood zou willen tonen, dan zou je deze methodes kunnen gebruiken zoals in de code hieronder:

   1:  protected override void    Render(HtmlTextWriter output)
   2:  {
   3:      output.WriteLine("<p>");
   4:   
   5:      // HTML netjes maken door een tab in te voegen
   6:      output.Write(HtmlTextWriter.DefaultTabString);
   7:   
   8:      // Zet een font-tag om de tekst voor de kleur
   9:      output.Write("<font color=\"red\">");
  10:      output.Write(this.Text);
  11:      output.WriteLine("</font>");
  12:   
  13:      output.WriteLine("</p>");
  14:  }
De code hierboven levert de HTML hieronder op, die equivalent is aan het HTML 3.2 resultaat dat uit de andere code voorbeelden (mits de browser CSS ondersteunt).

<p>
   <font color="red">My Text</font>
</p>

Met de verscheidene Write-methodes kun je ieder HTML of niet-HTML resultaat maken dat je wilt. De volgende stap is onderscheid kunnen maken tussen de verschillende browsers zodat je zonodig verschillende HTML naar verschillende browsers kunt sturen. Je kunt het type browser dat de pagina opvraagt controleren met het HttpBrowserCapabilites-object. Dit object, dat je kunt benaderen via Page.Request.Browser, bevat allerlei eigenschappen van over de client browser. Je kunt de naam en versie van de browser opvragen, en of deze ondersteuning biedt voor frames, tabellen, Java-applets, enzovoort. Vreemd genoeg ontbreekt van de lijst met eigenschappen of de browser CSS ondersteunt (en zo ja, welke versie), ondanks dat deze informatie in machine.config staat, waar alle browser eigenschappen staan. Je kunt op twee manieren om deze beperking heen, maar beide zijn niet wenselijk. De eerste mogelijkheid is om je kennis van bepaalde browsers te gebruiken om te bepalen of je CSS kunt gebruiken, bijvoorbeeld als volgt:

   1:  HttpBrowserCapabilities client;
   2:  client = Page.Request.Browser;
   3:   
   4:  if(client.MajorVersion > 3 &&
   5:     (client.Browser == "IE" ||
   6:      client.Browser == "Netscape"))
   7:  {
   8:      //Render CSS
   9:  }
  10:  else
  11:  {
  12:      //Render HTML 3.2
  13:  }

Het is duidelijk dat de bovenstaande code nooit alle browsers kan dekken, dus zou het kunnen zijn dat je HTML 3.2 stuurt aan een browser die CSS ondersteunt. Als de stijlen die je gebruikt complex zijn, zoals een achtergrondkleur op een label, zal het resultaat in HTML 3.2 niet geheel overeenkomen met de gewenste opmaak, omdat sommige zaken nu eenmaal niet ondersteund worden door alleen HTML 3.2.
De tweede mogelijkheid is te controleren welk type HtmlTextWriter meegegeven is aan de Render-methode. Er zijn er twee: de HtmlTextWriter en de Html32TextWriter. De eerste wordt gebruikt voor browsers die HTML 4.0 en CSS ondersteunen, de tweede voor browsers die HTML 3.2 zonder ondersteuning voor CSS. Je zou derhalve met de volgende code kunnen bepalen wat voor browser je het mee te stellen hebt:

   1:  if(output.GetType().ToString()== "HtmlTextWriter")
   2:  {
   3:      //Render CSS
   4:  }
   5:  else
   6:  {
   7:      //Render HTML 3.2
   8:  }

Het nadeel van het controleren van het type HtmlTextWriter is dat voor vrijwel alle niet-Microsoft browsers, inclusief versies van Netscape die weldegelijk CSS ondersteunen, de Html32TextWriter aan de Render-methode wordt meegegeven. Dus zelfs naar Netscape 6 die CSS prima ondersteunt wordt in dat geval gewoon HTML 3.2 gestuurd. De enige manier om dat op te lossen is om machine.config aan te passen en het volgende toe te voegen bij browsers die CSS ondersteunen:

tagwriter=System.Web.UI.HtmlTextWriter

Dit is echter wel riskant, want dit heeft niet alleen gevolgen voor de CSS ondersteuning. Er kunnen nog andere redenen zijn waarom de Html32TextWriter gebruikt wordt voor een bepaalde browser. Het is dus verstandig om je site te controleren nadat je dit doet. Eventueel kun je dit overigens ook overschrijven in de web.config van je site, zodat andere sites op dezelfde machine hierdoor niet beïnvloed worden.

ASP.NET de HTML laten genereren

Hoewel wat je tot nog toe gezien hebt werkt, is het niet de meest aantrekkelijke manier om HTML te genereren. Het is vervelend en zelfs met methodes als WriteBeginTag kan er nog van alles fout gaan. Als alternatief kun je ASP.NET de tags, attributen en opmaak laten verzorgen. Dat is veel eenvoudiger en sneller dan alles met de hand te moeten doen. Je zult het echter wel moeten stellen met de eigenaardigheden die ASP.NET heeft bij het genereren van HTML. Je hebt namelijk geen controle over ieder karakter dat naar de browser gestuurd wordt, maar ASP.NET verzorgt automatisch HTML 3.2 of HTML 4.0 met CSS, afhankelijk van het type HtmlTextWriter dat toegepast wordt. Dit heeft hetzelfde probleem als wanneer je zelf controleert welk type HtmlTextWriter gebruikt wordt, maar zoals gezegd kun je dit beïnvloeden door machine.config of web.config aan te passen. De code hieronder verzorgt rode tekst in het lettertype Verdana.

   1:  protected override void    Render(HtmlTextWriter output)
   2:  {
   3:      // Begin met de te gebruiken stijlen
   4:      output.AddStyleAttribute(HtmlTextWriterStyle.Color, "Red");
   5:      output.AddStyleAttribute(HtmlTextWriterStyle.FontFamily, "Verdana");
   6:   
   7:      // Begin tag schrijven NA de stijl
   8:      output.RenderBeginTag(HtmlTextWriterTag.P);
   9:   
  10:      // Tekst schrijven
  11:      output.Write(this.Text);
  12:   
  13:      // Eind tag schrijven
  14:      output.RenderEndTag();
  15:  }

Wanneer de Html32TextWriter doorgegeven wordt aan de Render-methode zou het resultaat van de bovenstaande code als volgt zijn:

Netscape

Anders is het resultaat HTML 4.0 met CSS, zoals hieronder:

My Text

Omdat je niet hoeft na te denken over de verschillende browsers, scheelt dit enorm veel werk. Merk op dat je de HTML tags ook niet meer expliciet schrijft, maar dat je de HtmlTextWriter vertelt welke tag te schrijven (uit een verzameling tags die bekend zijn voor de HtmlTextWriter). Bij het schrijven van de eind-tag hoef je niet op te geven welke tag er geschreven moet. De HtmlTextWriter houdt bij welke eind tag geschreven moet worden, naar aanleiding van de geschreven begin-tags. Zo ben je er zeker van dat tags nooit verkeerd genest worden. Ook van belang is dat de opmaak die je wilt toekennen aangegeven moet worden voordat je de begin-tag schrijft, zodat de opmaak dan toegekend kan worden.

De Style-class gebruiken

Tot nog toe is alle opmaak in de code gedefinieerd. Je kunt dat verhelpen door publieke eigenschappen toe te voegen aan de control waarnee de gebruiker van de control de kleur, het lettertype, enzovooorts kan instellen. Als je voor ieder type opmaak een eigenschap aan de control toe zou moeten voegen, zouden dat behoorlijk wat eigenschappen zijn. Bovendien zou je er ook voor moeten zorgen dat deze op de juiste plaats aan tags toegevoegd worden, en dat is veel werk. Door gebruik te maken van de Style-class in de System.Web.UI.WebControls namespace kun je jezelf een hoop werk besparen. De Style-class bevat alle eigenschappen die in een Label-control gebruikt worden: achtergrondkleur, tekstkleur, lettertype, randen, etc. Sterker nog, als je een control zoals de Calendar-control gebruikt, zijn de afzonderlijke stijlen die je in kunt stellen, zoals de SelectedDayStyle, allemaal gebaseerd op de Style-class. Er zijn meerdere afgeleide classes, zoals de TableStyle, die additionele opmaakeigenschappen aanbieden die van toepassing zijn op bepaalde HTML-elementen. Je kunt elk van deze classes om stijlen toe te voegen aan een control door deze als eigenschappen aan te bieden, zoals in de code hieronder:

   1:  private Style _myStyle = new Style();
   2:      
   3:  public Style MyStyle
   4:  {
   5:      get {return _myStyle;}
   6:      set {_myStyle = value;}
   7:  }
   8:   
   9:  protected override void    Render(HtmlTextWriter output)
  10:  {
  11:      // Stijl aan tag toekennen
  12:      this.MyStyle.AddAttributesToRender(output);
  13:   
  14:      // Begin tag schrijven NA de stijl
  15:      output.RenderBeginTag(HtmlTextWriterTag.P);
  16:   
  17:      // Tekst schrijven
  18:      output.Write(this.Text);
  19:   
  20:      // Eind-tag schrijven
  21:      output.RenderEndTag();
  22:  }

Noot: de code hierboven slaat de stijl niet op in de ViewState zodat veranderingen eraan tijdens het uitvoeren van de pagina bewaard worden. Dat vergt extra enkele extra acties die buiten het bestek van dit artikel vallen.

In de code hierboven wordt de AttributesToRender-metode aangeroepen op MyStyle om de opmaak daarvan toe te kennen aan de tag die geschreven wordt. De toegekende opmaak wordt door de HtmlTextWriter naar beste kunnen verzorg. Dit betekent dat de achtergrondkleur voor de tekst niet getoond wordt als er naar HTML 3.2 geschreven wordt. Voor de meeste complexe control is het niet verstandig om te proberen hier binnen je control een oplossing voor te vinden. In het kader “Templates afhankelijk van de browser” wordt een oplossing besproken die een de meeste gevallen handiger is.

Als de control die je maakt erft van een bestaande control, of van System.Web.UI.WebControl, is de kans groot dat de control al een Style-class bevat voor de algehele opmaak van de control. Dit is de zogenaamde ControlStyle, maar gebruikers van de control kunnen de eigenschappen daarvan direct benaderen alsof het eigenschappen van de control zelf zijn. Ze kunnen dus bijvoorbeeld het volgende gebruiken:

Font-Names="Verdana,Arial"

In plaats van een specifieke stijl aanwijzen, zoals de SelectedDayStyle in het voorbeeld hieronder:

SelectedDayStyle-Font-Names="Verdana,Arial"

Binnen de Render-methode kun je deControlStyle toekennen aan een tag door de volgende code te gebruiken:

this.ControlStyle.AddAttributesToRender(output);

Iets anders dat je kunt doen is de RenderContents-methode overschrijven, in plaats van de Render-method. Hierdoor weet je zeker dat de buitenste tag van je control automatisch de opmaak vande ControlStyle mee krijgt. In RenderContents verzorg je vervolgens alles dat binnen de buitenste tags moet staan.

Composite controls

Wanneer je composite controls, controls met sub-controls, maakt is de functie van de Render-methode iets anders. Omdat de control-tree al opgebouwd is op het moment dat de Render-methode aangeroepen wordt, kun je alleen nog opmaak verzorgen om de sub-controls heen en stijlen toepassen op de controls in de control-tree. Om de HTML voor de sub-controls te genereren doe je gebruik te maken van de functionaliteit die de sub-control daarvoor biedt. Als je dat niet doet is de sub-control niet zichtbaar voor de gebruiker en waarschijnlijk nutteloos binnen de control. Om opmaak rond de sub-controls te verzorgen kun je de mogelijkheden gebruiken die eerder besproken zijn. De sub-controls opmaken kun je expliciet doen door eigenschappen van de sub-controls in te stellen, of door stijlen toe te passen op de sub-controls. Om een stijl toe te passen heb je wederom een Style-obecjt nodig. Dit kan de ControlStyle zijn als je de algemene stijl door wilt geven aan de sub-controls, of een die beschikbaar is al publieke eigenschap van de control.
De code hieronder laat de relevante code zien voor een composite control die een TextBox met een titel laat zien. De titel neemt de opmaak van de control over, maar de TextBox wordt opgemaakt met een aparte stijl.

   1:  public class CaptionedTextBox : WebControl
   2:  {
   3:      private string caption;
   4:      private TextBox _textbox;
   5:      private Style _textBoxStyle = new Style();
   6:      
   7:      // Kop die weergegeven moet worden bij de TextBox
   8:      public string Caption
   9:      {
  10:          get {return caption;}
  11:          set {caption = value;}
  12:      }
  13:   
  14:      // De tekst in de TextBox
  15:      public string Text 
  16:      {
  17:          get {
  18:              // Zorg dat child controls toegankelijk zijn
  19:              this.EnsureChildControls();
  20:              return _textbox.Text;
  21:          }
  22:   
  23:          set {
  24:              this.EnsureChildControls();
  25:              _textbox.Text = value;
  26:          }
  27:      }
  28:      
  29:      // Stil voor de TextBox
  30:      public Style TextBoxStyle
  31:      {
  32:          get {return _textBoxStyle;}
  33:          set {_textBoxStyle = value;}
  34:      }
  35:   
  36:   
  37:      protected override void CreateChildControls()
  38:      {
  39:          // TextBox aan de control tree toevoegen
  40:          _textbox = new TextBox();
  41:          this.Controls.Add(_textbox);
  42:      }
  43:   
  44:      protected override void RenderContents(HtmlTextWriter output)
  45:      {
  46:          // Inhoud van de control schrijven
  47:          output.Write(this.Caption + " ");
  48:   
  49:          // Stijl toekennen aan TextBox en TextBox schrijven
  50:          _textbox.ApplyStyle(this.TextBoxStyle);
  51:          _textbox.RenderControl(output);
  52:      }
  53:  }

Merk op dat in de code hierboven de TextBox gedefinieerd is als globale member variabele binnen de control, omdat deze door meerdere methodes gebruikt wordt. De eerste keer wanneer de TextBox aan de control-tree toegevoegd wordt, en later als de stijl toegepast wordt en de HTML gegenereerd wordt. Je zou de stijl toe kunnen passen in de CreateChildControls-methode, maar dat zou tot gevolg hebben dat de stijl van de TextBox-control altijd in de ViewState opgeslagen wordt, terwijl het Style-object zelf ook al in de ViewState opgeslagen wordt (ervan uitgaande dat je daarvoor de benodigde zaken geregeld hebt). Dit betekent uiteraard dat de ViewState groter is, zonder dat daar een goede reden voor is. De ApplyStyle-methode kopieert alle eigenschappen van de bron stijl naar de ControlStyle van de sub-control, waarbij het alle bestaande instellingen overschrijft. Als je alleen de eigenschappen wilt overschrijven die nog geen waarde hebben, dan kun je de MergeStyle-methode gebruiken.

Tot slot

Opmaak verzorgen voor controls kan op verschillende manieren gedaan worden. Wat de beste manier is hangt af van de control die je aan het maken bent, maar in de meeste gevallen kun je het beste de buitenste tag van de control waarvan je geërfd hebt intact laten. Dit betekent dat je het beste de RenderContents-methode kunt overschrijven, in plaats van de Render-methode. Wanneer je composite controls maakt, houd dan in je achterhoofd dat je opmaak van sub-controls nog enigszins kunt beïnvloeden tijdens de fase waarin de HTML gegenereerd wordt. Verder kun je in deze fase extra HTML toevoegen, zodat je een control kunt maken met weinig functionele sub-controls, waarbij je de HTML om deze controls heen verzorgt in de Render- of RenderContents-methode. Dit is efficiënter dan meer controls toevoegen aan de control-tree om de opmaak te verzorgen. Dat laatste brengt namelijk een extra overhead met zich mee.

<< vorige | ^ naar boven | overzicht | volgende >>
copyright 2000-2007 ASPNL