ASPNL logo (1 kb)
zaterdag 10 mei 2008




Microsoft MVP

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

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

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