The Vision Column - Custom controls in ASP.NET: de TabControl (deel 2)
The Vision Column wordt verzorgd door ontwikkelaars van
The Vision Web (tegenwoordig Ordina).
Door Arnold Jan van der Burg
18 december 2007
In deel 1 is uiteengezet hoe de algemene opzet van de TabControl er uit ziet, hoe je met class
attributes metadata kunt beïnvloeden en tenslotte hoe je events en postback data kunt opvangen. In
deze tweede column zal uiteengezet worden hoe het renderen van een custom control kan worden
geïmplementeerd. Daarnaast zal ik uitvoerig ingaan op design-time ondersteuning in Visual Studio
.NET 2003.
Rendering
Een custom control heeft dankzij de overerving van de WebControl class een aantal methodes en
events die specifiek gericht zijn op het renderen van de control. Een aantal van deze methodes
zullen hier behandeld worden:
- AddAttributesToRender
- Render
- RenderBeginTag
- RenderEndTag
- RenderContents
De belangrijkste methode uit dit rijtje is de Render methode. Deze methode wordt aangeroepen op
het moment dat de control op de pagina getoond moet worden en de HTML waaruit de control bestaat,
gegenereerd moet worden. Er zijn verschillende manieren waarop je als ontwikkelaar dit kunt
aanpakken. Door het ‘overriden’ van deze methode zou je alle HTML (inclusief attributen) in deze
methode kunnen laten genereren. Het is beter en overzichtelijker om hiervoor de specifieke
methodes van de WebControl class te gebruiken; als je met .NET Reflector de Render methode van
de WebControl class bekijkt, zie je dat deze maar uit drie regels code bestaat:
1: protected override void Render(HtmlTextWriter writer)
2: {
3: this.RenderBeginTag(writer);
4: this.RenderContents(writer);
5: this.RenderEndTag(writer);
6: }
Allereerst wordt de RenderBeginTag methode aangeroepen. In deze methode wordt de HTML gegenereerd
voor de begintag (in het geval van de TabControl is dat <table (attributen….)>. Daarna wordt de
RenderContents methode aangeroepen. In deze methode wordt de inhoud tussen begin- en eindtag
gerendered. Tenslotte wordt de RenderEndTag methode aangeroepen om de eindtag te renderen (in dit
geval dus de </table> tag).
Attributen voor de openingstag kunnen worden aangegeven in de RenderBeginTag methode, maar het is
mooier hiervoor de AddAttributesToRender methode te gebruiken en een aanroep naar deze methode op
te nemen in de RenderBeginTag methode. Op deze manier hoef je geen implementatie van de Render
methode van de WebControl class te maken.
Het renderen van de inhoud van een control gebeurt met behulp van de HtmlTextWriter class. Deze
class houdt o.a. een stack bij van gegenereerde HTML tags en een array van attributen. Op deze
manier hoef je je niet druk te maken om het correct sluiten van een HTML tag, dit doet de
RenderEndTag methode van deze class voor je. Waar je wel op moet letten, is het gebruik van de
AddAttribute en AddStyleAttribute methodes van de HtmlTextWriter class. Met behulp van deze
methodes kun je (stijl)attributen aan een HTML tag toekennen, maar dit moet je doen vóórdat je de
RenderBeginTag methode van de HtmlTextWriter class aanroept. De RenderBeginTag methode schrijft
alle attributen, die tot dan toe aan de HtmlTextWriter zijn toegevoegd, weg en zet vervolgens de
index van het interne array, waarin de attributen zijn opgeslagen, op 0. Zie onderstaand voorbeeld:
1: //Add attribute
2: writer.AddAttribute("orientation", "horizontal");
3: writer.AddAttribute("cellpadding", "0");
4: writer.AddAttribute("cellspacing", "0");
5: writer.AddAttribute("border", "0");
6: writer.AddAttribute("bgcolor", "#ff0000");
7:
8: //Style attributes
9: writer.AddStyleAttribute("cursor", "default");
10: //Render table begin tag
11: writer.RenderBeginTag("table")
De output van bovenstaande code zal er als volgt uit zien:
<table orientation="horizontal" cellpadding="0" cellspacing="0"
border="0" gcolor="#ff0000" style="cursor: default">
De TabControl maakt ook gebruik van bovenstaande opzet, zie de source code voor de details.
Design-time support
Als je eenmaal de rendering van je custom control hebt geschreven, zul je zien dat deze ook als
zodanig wordt getoond in de design-time modus. Voor de gevallen dat je in design-time je control
een andere layout wil geven, of bijvoorbeeld de programmeur wilt voorzien van extra commando’s of
functionaliteit, zul je een designer moeten schrijven. Er zijn binnen het .NET framework drie
classes die je kunt gebruiken voor als basis voor je designer. Welke je kiest, hangt af van het
soort control dat de designer moet gaan ondersteunen:
| ReadWriteControlDesigner |
Designer class voor controls die een container vormen voor andere controls, bijv. het Panel control |
| ControlDesigner |
Designer class voor controls die een statische set van eigenschappen en childcontrols hebben en dus geen container controls zijn. |
| TemplatedControlDesigner |
Designer class specifiek voor controls die gebruik maken van templates. |
Bij het maken van een custom designer kun je een van de bovenstaande classes als je base class
gebruiken. Deze kun je vervolgens aan je custom control toevoegen door het DesignerClassAttribute
te gebruiken. Dit doe je door het DesignerAttribute toe te voegen aan de definitie van je custom
control:
1: [DefaultProperty("Tabs"),
2: DefaultEvent("SelectedIndexChanged"),
3: ToolboxData("<{0}:TabControl runat=server></{0}:TabControl>"),
4: ToolboxBitmap(typeof(TabControl), "Icons.TabControlIcon.bmp"),
5: Designer(typeof(TVW.Web.UI.Design.WebControls.TabControlDesigner)),
6: ParseChildren(true, "Tabs"),
7: PersistChildren(true)]
8: public class TabControl : System.Web.UI.WebControls.WebControl,
9: IPostBackDataHandler, IPostBackEventHandler, INamingContainer
10: {
11: ...
12: }
Zoals het voorbeeld laat zien, voeg je een "Designer(<typeaanduiding>)" regel toe aan de
class definitie. Let er wel op dat je bij de typeaanduiding de volledige namespace opgeeft!
In dit artikel zal ik me beperken tot het bespreken van de ControlDesigner class, om dat deze
gebruikt wordt voor de design-time support van de TabControl. Er zijn een aantal methodes die je
kunt gebruiken voor het renderen van een control in design-time:
- GetEmptyDesignTimeHtml. Methode die HTML genereert voor een lege control in design-time.
- GetDesignTimeHtml. Deze methode retourneert de HTML die de control weergeeft in design-time.
- GetPersistInnerHtml. Methode die wordt aangeroepen op het moment dat geswitched wordt tussen design-time en HTML source modus. Genereert de inhoud van een control in de .aspx file.
- GetErrorDesignTimeHtml. Methode die wordt aangeroepen als bij het renderen van de control in design-time fouten zijn opgetreden.
Bovenstaande functies zijn niet bijzonder ingewikkeld. Uitzondering is de GetPersistInnerHtml
methode. Deze methode wordt iedere keer aangeroepen als er geswitched wordt tussen de design view
en HTML view in Visual Studio .NET. Als de switch wordt gemaakt, zal de designer de HTML inhoud
van de control opnieuw wegschrijven naar de .aspx pagina. Op deze manier worden eventuele
aanpassingen aan de control gesynchroniseerd met de declaratie van de control in de .aspx pagina.
De makkelijkste manier om de HTML te genereren is de GetPersistInnerHtml methode van de
ControlDesigner class hiervoor gebruiken. Door gebruik te maken van de IsDirty property van de
ControlDesigner class kun je de inhoud van de control laten persisteren als het alleen noodzakelijk
is:
1: public override string GetPersistInnerHtml()
2: {
3: TabControl tabControl;
4: bool isVisible;
5: string persistedHtml;
6:
7: tabControl = (TabControl) base.Component;
8:
9: //Save visible property to prohibit child controls
10: // from setting this property themselves
11: isVisible = tabControl.Visible;
12: persistedHtml = string.Empty;
13:
14: if (!base.IsDirty)
15: {
16: return null;
17: }
18:
19: try
20: {
21: tabControl.Visible = true;
22: persistedHtml = base.GetPersistInnerHtml();
23: }
24: finally
25: {
26: //Return control to its original visibility state
27: tabControl.Visible = isVisible;
28: }
29: return persistedHtml;
30: }
Design-time class attributes
In het eerste deel van deze column is uiteengezet wat class attributes zijn en hoe je deze kunt
gebruiken. Er zijn een aantal attributes specifiek gericht op design-time gedrag:
- BrowsableAttribute class. Met dit class attribute geef je aan of een property moet worden weergegeven in het Properties window.
- CategoryAttribute class. Met dit class attribute geef je aan in welke propertycategorie de property moet worden ingedeeld.
Designer verbs
Je kunt de gebruiker van je control extra functionaliteit in design-time bieden door gebruik te
maken van de Verbs collectie die in de ControlDesigner class is opgenomen. Deze collectie kun je
vullen met objecten van het classtype DesignerVerb. Dit is een menucommando dat je kunt toevoegen
aan een designer. Zie onderstaand code snippet uit de TabControl class:
1: this._verbAddTab = new DesignerVerb("Add New Tab", new EventHandler(AddTab));
2: this._verbShowPreviousTab =
3: new DesignerVerb("Show Previous Tab", new EventHandler(ShowPreviousTab));
4: this._verbShowNextTab =
5: new DesignerVerb("Show Next Tab", new EventHandler(ShowNextTab));
6: this._verbRemoveTab =
7: new DesignerVerb("Remove Current Tab", new EventHandler(RemoveTab));
6: base.Verbs.Add(this._verbAddTab);
8:
9: base.Verbs.Add(this._verbShowPreviousTab);
10: base.Verbs.Add(this._verbShowNextTab);
11: base.Verbs.Add(this._verbRemoveTab);
De constructor van de DesignerVerb gebruikt twee parameters: een string met de tekst voor het menu,
en de daaraan gekoppelde eventhandler.
In de eventhandler kun je vervolgens de gewenste actie programmeren en eventueel de design time
HTML updaten door de UpdateDesignTimeHtml() methode aan te roepen.
Designer services
De designer class kan naast de DesignerVerb ook services gebruiken die extra design-time ondersteuning
bieden aan een designer class, bijv. wanneer de inhoud van een control is gewijzigd in design-time.
De designer classes in .NET hebben hiervoor één functie: GetService(). Bijvoorbeeld: in de
TabControl designer class wordt gebruik gemaakt van de IComponentChange service om af te vangen of
de control in design-time is gewijzigd. Vervolgens wordt het menu van de designer verbs geupdate:
1: public override void Initialize(IComponent component)
2: {
3: ...
4: IComponentChangeService ccs;
5: ccs = (IComponentChangeService)this.GetService(
typeof(IComponentChangeService));
6:
7: if (ccs != null)
8: {
9: ccs.ComponentChanged +=
10: new ComponentChangedEventHandler(OnComponentChanged);
11: }
12: ...
13: }
Bovenstaand stuk code laat zien hoe je een service kunt opvragen. Zodra de service is opgevraagd
kun je aan een service (afhankelijk van het soort service) weer eventhandlers koppelen (in dit
geval bijv. het OnComponentChanged event). Voor een compleet overzicht van alle services, zie de
MSDN referenties.
Referenties:
MSDN:
Enhancing Design-Time Support
http://msdn2.microsoft.com/en-us/library/37899azc(vs.71).aspx
|