ASPNL logo (1 kb)
zondag 6 juli 2008




Microsoft MVP

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

The Vision Column - Custom controls in ASP.NET: de TabControl (deel 1)

The Vision Column wordt verzorgd door ontwikkelaars van The Vision Web (tegenwoordig Ordina).

Door Arnold Jan van der Burg
5 oktober 2007

Iedere ontwikkelaar in ASP.NET kent de standaard ASP.NET webserver controls wel. Meestal is deze set van controls voldoende om de functionaliteit te bouwen die je voor ogen hebt. Soms kan het echter zo zijn dat deze tekort schieten of niet precies doen wat je als ontwikkelaar zou willen, of het kan zijn dat je een bepaald type control nodig hebt dat eenvoudigweg niet beschikbaar is. Gelukkig is het in ASP.NET niet heel ingewikkeld om een custom control te bouwen, of toch wel?

User controls vs. custom controls

Binnen ASP.NET zijn er twee typen "custom" control. Je hebt het UserControl type en het Custom WebControl type. Het verschil tussen deze twee vormen staat in de volgende tabel:

User control Custom control
Visuele ondersteuning bij aanmaken + -
Scheiding code en HTML + -
Precompiled code - +
Visuele ondersteuning bij toevoegen in pagina - +
Herbruikbaarheid - +

User controls zijn gemakkelijk als je snel iets specifieks voor één applicatie wilt maken, custom controls zijn echter weer makkelijker te delen over meerdere applicaties, omdat ze gecompileerd worden naar een DLL. Vooral als je dynamische content moet verwerken in je control, kun je beter een custom control gebruiken.

TabControl

Ik heb voor een klein project binnen T-Mobile een Tab control gebouwd voor ASP.NET. Deze control moest aan de volgende eisen voldoen:
  • Het aantal tabs kan variëren.
  • Een tab kan andere ASP.NET controls en HTML bevatten.
  • ASP.NET controls binnen een tab zijn in de pagina beschikbaar als schijnbaar losse controls.
  • Event afhandeling voor het wisselen van tab (zowel server-side als client-side). De control moet kunnen onthouden welke tab geselecteerd is.
  • Het moet mogelijk zijn om tabs uit te schakelen of onzichtbaar te maken.
  • Validatie controls moeten per tab kunnen worden in- of uitgeschakeld worden, om te voorkomen dat deze per ongeluk worden afgevuurd.
  • Visueel onderscheid tussen de actieve en inactieve tabs (voor- en achtergrond kleuren).
  • De tab moet zowel horizontaal als verticaal kunnen worden getoond.
  • Gebruik van additionele CSS styles moet worden ondersteund.

Uit bovenstaande eisen kan logischerwijs worden afgeleid dat het hier een custom control betreft. Het is uiteindelijk een redelijk complex control geworden, waarvan ik de werking in deze en komende column zal uitleggen. Daarnaast ik zal proberen een schets te geven van de mogelijkheden die je hebt bij het bouwen van een custom ASP.NET control.

De basis

In feite wordt mijn tab control niets anders dan een HTML tabel, waarbij de bovenste regel cellen de tab headers vormen. De rijen cellen daaronder vormen de inhoud van de tabs. Door slim gebruik te maken van styles wordt een 3D effect gesuggereerd (zie Afbeelding 1).


Afbeelding 1

Telkens als een tab wordt geselecteerd, zal de bijbehorende tabel rij (met daarin de controls van de geselecteerde tab) getoond worden. Om ervoor te zorgen dat de juiste controls bij de juiste tab getoond worden en dit te kunnen onthouden in de control heb ik de volgende oplossing bedacht:

    1. Er bestaat een collectie van tabs. Iedere tab vormt weer de parent control voor de controls binnen de tab zelf.
    2. Iedere control binnen een tab moeten worden gepersisteerd naar de pagina. Op deze manier is iedere control binnen een tab los bereikbaar.

De basis van de tab control wordt dus gevormd door de collectie van tabs. Deze TabCollection class is eigenlijk niets meer dan een proxy class die de tabs zal toevoegen aan de Controls collectie van de TabControl class zelf. Op deze manier hoeft de control niet zelf een aparte collectie te bevatten (zie Afbeelding 2).


Afbeelding 2

Om ervoor te zorgen dat alleen tabs kunnen worden toegevoegd aan deze collectie, zal dit een custom ControlCollection class zijn. Op deze manier kan worden afgevangen of de toegevoegde control wel daadwerkelijk een control van het type Tab is. Zoals in de eisen vermeldt staat, moeten de controls binnen een tab zichtbaar zijn in de code behind. Dit is in te stellen door gebruik te maken van zogenaamde "class attributes".

Class attributes

Een class attribute is een handige een manier om metadata over een class op te slaan. Een aantal ervan zijn bijzonder handig als het aankomt op het bouwen van een custom control. De belangrijkste attribute classes voor het bouwen van een custom control zijn de volgende:
  • ParseChildrenAttribute class. Dit attribute wordt gebruikt om aan te geven dat XML elementen, die binnen het server control gedeclareerd zijn, moeten worden behandeld als eigenschappen of childcontrols van die control.
  • PersistChildrenAttribute class. Dit attribuut geeft aan of de child controls moeten worden gedeclareerd als geneste controls. Dit houdt in dat de controls ieder apart zullen worden gedeclareerd als protected members van de pagina.
  • DesignerAttribute class. Met dit attribuut kan worden verwezen naar de designer class die de custom control in design-time modus laat zien (meer over designers in deel 2 van deze column).
  • PersistenceModeAttribute class. Dit attribuut wordt toegepast op properties van het control en bepaalt hoe de eigenschappen van de control worden gepersisteerd in de pagina.
  • DesignerSerializationVisibilityAttribute class. Dit attribuut wordt vaak in combinatie met het PersistenceMode attribuut gebruikt en bepaalt hoe de inhoud van een property moet worden geserialiseerd.

Het gaat hier te ver om de attributen uitgebreid te behandelen. Je kunt de MSDN library raadplegen voor een uitgebreid overzicht van deze attributen.

Zoals eerder aangegeven moeten de controls van een tab zichtbaar zijn in de code behind. Dit betekent dat bij het parsen van de tabcontrol, de controls binnen een tab moeten niet moeten worden geïnterpreteerd. Hierdoor kan een tab zelf alle mogelijke controls bevatten. Om dit te regelen, kan gebruik worden gemaakt van de bovengenoemde class attributes. Ter illustratie hieronder de Tab class:

   1:  [ToolboxItem(false),
   2:     3:        ParseChildren(false),
   4:        PersistChildren(true),
   5:        DefaultProperty("Text"),
   6:        Serializable()]
   7:  public class Tab : Control, INamingContainer

Door het ParseChildren attribuut op false te zetten, zal de parser de inhoud van de tab negeren en interpreteren als child controls van deze tab. Door het PersistChildren attribuut op true te zetten, zullen deze controls worden toegevoegd aan de pagina class als protected members.

Events en postback

Om ervoor te zorgen dat de juiste tab getoond wordt, zal het tab control een property SelectedIndex hebben. Met deze property wordt onthouden welke tab uit de collectie op dat moment geselecteerd is. De eindgebruiker kan een andere tab aanklikken en daardoor de waarde van deze property wijzigen. Om dit mogelijk te maken zal de control een extra event bevatten: OnSelectedIndexChanged. In dit event wordt de geselecteerde tab gewijzigd en kan de inhoud van de nieuw geselecteerde tab worden getoond. Om de control te laten reageren op het wijzigen van een de geselecteerde tab wordt gebruik gemaakt van de IPostBackDataHandler en IPostBackEventHandler interfaces. Deze interfaces brengen de control ervan op de hoogte dat er een postback naar de server heeft plaatsgevonden.

   1:  void IPostBackDataHandler.RaisePostDataChangedEvent()
   2:  {
   3:        // Stub Implementation, Required for IPostBackDataHandler
   4:        //though it doesn't use its methods.
   5:        this.OnSelectedIndexChanged(EventArgs.Empty);
   6:  }
   7:                    
   8:  bool IPostBackDataHandler.LoadPostData(string postDataKey,
   9:        System.Collections.Specialized.NameValueCollection postDataCollection)
  10:  {
  11:        return true;
  12:  }                 
  13:  void IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
  14:  {
  15:        if (eventArgument == null)
  16:              return;   
  17:               
  18:        this._selectedIndex = Int32.Parse(eventArgument);  
  19:                       
  20:        EventArgs e = new EventArgs();
  21:        this.OnSelectedIndexChanged(e);
  22:  }

Om de veranderde index op te slaan na een postback moeten wel de LoadViewState() en SaveViewState() methodes aangepast worden om de gewijzigde index op te slaan. Om de dingen gegroepeerd op te slaan, maak ik hier gebruik van een Triplet, een class die exact drie dingen op kan slaan. Op deze manier is het gemakkelijk drie waardes weer uit de viewstate te kunnen halen.

   1:  protected override void LoadViewState(object viewState)
   2:  {
   3:   
   4:        Triplet currentState;
   5:        if (viewState != null && this.EnableViewState)
   6:        {
   7:              currentState = (Triplet) viewState;
   8:              this._selectedIndex = (int) currentState.First;
   9:    10:        }
  11:        else
  12:        {
  13:              base.SaveViewState();
  14:        }
  15:  }             
  16:   
  17:  protected override object SaveViewState()
  18:  {
  19:        Triplet currentState;
  20:   
  21:        currentState = new Triplet();
  22:                             
  23:        if (this.EnableViewState)
  24:        {
  25:              currentState.First = this.SelectedIndex;
  26:    27:              return currentState;                
  28:        }
  29:        else
  30:        {
  31:              return base.SaveViewState();
  32:        }
  33:  }

Tot zover het eerste deel van de TabControl. In deel 2 zal ik ingaan op het renderen van de control en design-time ondersteuning.

Referenties:

MSDN:
Recommendations for Web User Controls vs. Web Custom Controls:
http://msdn2.microsoft.com/en-us/library/aa651710(vs.71).aspx
Maintaining State in a Control
http://msdn2.microsoft.com/en-us/library/aa720269(vs.71).aspx
Processing Postback Data
http://msdn2.microsoft.com/en-us/library/aa720471(vs.71).aspx
<< vorige | ^ naar boven | overzicht | volgende >>
copyright 2000-2007 ASPNL