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.
|