Inleiding
In dit 4e en laatste deel over het bouwen van ASP.NET server controls wil ik verder ingaan op de integratie van custom controls en Visual Studio.NET. Het is mogelijk server controls extra mogelijkheden te geven tijdens het ontwerpen van de webpagina’s. Hierbij valt te denken aan interactie van de control met andere controls op de pagina of het anders tonen van de control in de Visual Studio designer dan in de run-time applicatie. Denk bijvoorbeeld aan een tabel die run-time geen border heeft, maar waarvan de border design-time wel zichtbaar dient te zijn om zo de pagina correct te kunnen opmaken.
Om een goed begrip te krijgen van de mogelijkheden wil ik eerst kort ingaan op de designer architectuur die beschikbaar is in Visual Studio. Vervolgens zal ik beschrijven hoe je zelf designers kunt maken en gebruiken voor zelfgemaakte controls. Als voorbeeld gaan we een imagelist control maken die alleen design-time gedrag heeft en passen we de eerder gemaakte label control zodanig aan dat het gebruik maakt van de imagelist om een image te selecteren. Het image zal zowel run-time als design-time voor de tekst van het label worden getoond.
Designer architectuur
Als we het hebben over het ontwerpen van componenten, dan gaat het feitelijk over het met elkaar kunnen laten samenwerken van componenten zonder daar code voor te schrijven. Een voorbeeld daarvan is het samenstellen van een webpagina waarbij diverse controls moeten samenwerken om een pagina te vormen. Hierbij willen we het liefst geen code schrijven, maar dit laten regelen door de designer. Voor het samenstellen van webpagina’s is daarvoor in Visual Studio.NET de “design”-view beschikbaar (zie figuur 1) .

Fig. 1: Design-time view in Visual Studio.NET
Visual Studio.NET fungeert in dit geval als een zogenaamde designer-host applicatie. In een designer-host wordt een representatie van een te ontwerpen component visueel weergegeven door de samenwerking van “designers”. Voor het ontwerpen van een webpagina zal de webpagina in de designer-host worden gerepresenteerd als een “design surface” waarop men controls kan plaatsen. Deze “design surface” is een designer die voor webforms als “root designer” fungeert. Iedere control die op het “design surface” wordt gesleept, zal worden getoond door middel van zijn geassocieerde designer. Deze designer is gekoppeld aan de control klasse door middel van een “Designer” attribuut. De designer zal de gegenereerde HTML representeren in de designer-host waardoor we het bekende WYSYWIG (What You See Is What You Get) gedrag krijgen.
In figuur 2 wordt de relatie weergegeven tussen de verschillende objecten die verantwoordelijk zijn voor design-time gedrag:

Fig. 2: Designer architectuur
De designer-host is de visual editing omgeving zoals Visual Studio.NET of WebMatrix. Een designer-host kan diverse interfaces implementeren die kunnen worden gebruikt door de designers om hun design-time gedrag te implementeren. De designer-host heeft ten minste één referentie naar de Root control en de daarbij behorende Root Designer. De koppeling van een control en zijn designer gebeurt altijd via de tussenkomst van een “site”. De root designer is verantwoordelijk voor het tekenen van de “design surface”. Alle afzonderlijke designers zijn alleen verantwoordelijk voor het representeren van de control waarmee ze zijn geassocieerd.
In het geval van webforms heeft iedere control die is afgeleid van System.Web.UI.Control een gekoppelde designer. Default zal deze designer zal de control aanspreken alsof deze run-time wordt benaderd. Dit houdt dus in dat deze designer alle events laat afgaan op de control in dezelfde volgorde als run-time het geval is (met uitzondering van PreRender()).
Er is gekozen om de koppeling van een designer met zijn component te maken door middel van het designer attribuut. Zo is het mogelijk de designers volledig los te koppelen van de control implementatie. Het is dus prima mogelijk een control uit te leveren en zijn bijbehorende designer in een andere assembly uit te leveren. Dit zorgt ervoor dat het design-time gedrag geen extra resources kost, zodra de componenten ontworpen zijn en in de run-time omgeving het echte werk moeten gaan verrichten.
Imagelist control
Voor onze eerder gemaakte label control gaan we nu extra functionaliteit inbouwen om een image te selecteren uit een imagelist control die op de pagina aanwezig is.
Allereerst beginnen we met het maken van de imagelist control.
De imagelist control zal design-time een lijst met images tonen in de designer die vervolgens kunnen worden geselecteerd door andere controls op de pagina. De imagelist zal zelf run-time niets doen. Deze control heeft dus alleen maar design-time gedrag en geen run-time representatie.
We maken hiervoor eerst een control klasse die afgeleid is van Systsem.Web.UI.Control en implementeren één property die de lijst met images bevat.
Zie hieronder het codevoorbeeld voor de implementatie van de control:
[ToolboxData("<{0}:ImageList runat=server>"),
Designer(typeof(ImageListDesigner))]
public class ImageList : System.Web.UI.Control
{
// the list is only manipulated by its designer
// therefore its internal so the designer can access it
internal string[] _imageList = null;
[
Bindable(true),
Category("Data"),
DefaultValue(""),
TypeConverter(typeof(StringArrayTypeconverter)),
PersistenceMode(PersistenceMode.Attribute)
]
public string[] List
{
get
{
return _imageList;
}
set
{
_imageList = value;
}
}
}
In de implementatie is te zien dat er alleen een property wordt gemaakt van het type string[] (een array van strings). Omdat de implementatie van het serialiseren van een string[] default hetzelfde is als een ToString() functie aanroepen op het array, moeten we zelf een type-converter aanleveren om alle items uit het array komma gescheiden weg te schrijven en ook weer in te lezen in het array. Deze conversie zal worden aangeroepen bij het starten van de designer als de pagina voor het eerst wordt getoond in de designer en als de designer wordt afgesloten en de property is veranderd.
N.B.: De implementatie van de type-converter valt buiten de scope van dit artikel maar is wel in de begeleidende sourcecode meegeleverd in de file StringArrayTypeConverter.cs.
Imagelist designer
We willen design-time een lijst met images op de pagina getoond zien en hiervoor gaan we nu zelf een designer maken. Hiervoor maken we een klasse die afgeleid is van System.Web.Design.ControlDesigner. ControlDesigner biedt ons de basisimplementatie van een designer die het run-time gedrag van een webpage simuleert. Hierdoor zal standaard de run-time inhoud van de control worden getoond. We dienen nu alleen zelf een aantal methodes te overriden om het design-time gedrag naar wens aan te passen.
Het is mogelijk om het context menu van een control, dat wordt getoond als er met de rechtermuis wordt geklikt op de control, aan te passen en hier zelf items aan toe te voegen. Dit wordt gedaan door de Verbs collection te overriden en hier zelf entries aan toe te voegen. Zorg er daarbij voor dat er maar éénmaal een toevoeging wordt gedaan, anders zal de lijst steeds verder groeien na iedere aanroep van het menu. We maken hiervoor een instance variabele van het type DesignerVerb die ons menu-item vasthoudt.
De implementatie voor de Verbs property override wordt hieronder weergegeven.
// voeg menu items toe aan de control
// bij een rechter muisclick op de designer
public override
System.ComponentModel.Design.DesignerVerbCollection Verbs
{
get
{
System.ComponentModel.Design.DesignerVerbCollection
baseVerbs = base.Verbs;
if(_bitmapSelector == null)
{
_bitmapSelector = new DesignerVerb(
"Add bitmaps", new EventHandler(this.HandleAddBitmaps));
baseVerbs.Add(_bitmapSelector );
}
return baseVerbs;
}
}
Eerst wordt gecontroleerd of onze instance variabele al is gevuld. Is dit niet het geval dan vragen we aan de baseclass alle standaard verbs op en breiden we deze lijst zelf uit met ons eigen menu item “Add bitmaps”. We geven mee dat, indien dit menu item wordt geselecteerd, de eventhandler “HandleAddBitmaps” moet worden aangeroepen op onze klasse.
Bij het afhandelen van het menu-item willen we een standaard browse dialoog tonen die in web projecten wordt gebruikt voor het aanduiden van een resource in de website. Deze dialoog ziet er uit zoals aangegeven in figuur 3.

Fig. 3: Standaard url dialoog
In de eventhandler moeten we deze standaard dialoog aanroepen. Dit is mogelijk door een service te gebruiken die wordt aangeboden door de designer-host. De designer-host is altijd vanuit een designer te benaderen via de control waarvoor de designer verantwoordelijk is. De designer heeft hiervoor een public property “Component”. Iedere component heeft design-time een property “Site” beschikbaar die de methode GetService() heeft. Deze GetService() methode biedt de mogelijkheid om te vragen aan de designer-host om een implementatie van een interface. Voor de WebForms designer is een aantal webform specifieke services beschikbaar zoals IWebFormsDocumentService en IWebFormsBuilderUIService, maar ook een generieke service zoals IComponentChangeService.
IWebFormsBuilderUIService biedt de mogelijkheid om de standaard dialoog zoals in figuur 2 is getoond, te activeren en een URL naar een geselecteerde resource binnen de website terug te krijgen.
De IWebFormsDocumentService biedt de mogelijkheid om informatie te vragen over de huidige webpagina. De property die we gebruiken is “DocumentUrl”. Deze geeft een URL naar de huidige page die in de designer wordt getoond. Dit is de locatie die nodig is om de URLBuilder dialoog goed te initialiseren.
In de afhandeling van het toevoegen van een bitmap, passen we de list property op de control aan. Omdat we dit niet via het property window, of via een PropertyDescriptor, doen, is de omgeving niet op de hoogte van de verandering van de property waarde. Dit heeft tot gevolg dat de property niet wordt weggeschreven in de tag van de control; dit is nodig om na het deactiveren van de pagina in de designer de pagina opnieuw te kunnen initialiseren. Om ervoor te zorgen dat de designer-host op de hoogte is van property veranderingen, is het nodig om de IComponentChangeService aan te roepen. Hierop geef je aan dat je van plan bent een property aan te passen met behulp van de methode OnComponentChanging(). Als de property is aangepast, geef je dat aan met de methode OnComponentChanged().
De implementatie van de HandleAddBitmap functie ziet er als volgt uit:
public void HandleAddBitmaps(object sender, EventArgs e)
{
IWebFormsDocumentService docService =
(IWebFormsDocumentService)GetService(typeof(IWebFormsDocumentService));
IWebFormsBuilderUIService urlBuilder = (IWebFormsBuilderUIService)
GetService(typeof(IWebFormsBuilderUIService));
IComponentChangeService changeService =
(IComponentChangeService)GetService(typeof(IComponentChangeService));
string currentLocation =
docService.DocumentUrl.Substring(
0,docService.DocumentUrl.LastIndexOf("/"))+"/*.gif";
string bitmapUrl = urlBuilder.BuildUrl(
null,"",currentLocation,"Select an imagefile","",
UrlBuilderOptions.NoAbsolute);
string [] list = ((ImageList)Component).List;
string [] newlist = null;
if ((((ImageList)Component).List!=null ) &&
(((ImageList)Component).List.Length > 0))
{
// loop de lijst door; als hij er al in zit
// dan niet toevoegen, anders array uitbereiden.
foreach(string resource in list)
{
if (resource == bitmapUrl)
return;
}
// niet gevonden in de lijst dus maak de lijst groter
newlist = new string[list.Length + 1];
list.CopyTo(newlist,0);
}
else
{
newlist = new string[1];
}
// voeg hem toe aan de nieuwe lijst .
newlist[newlist.Length-1] = bitmapUrl;
// zorg er voor dat de designerhost weet heeft
// van een veranderende property
changeService.OnComponentChanging(Component,null);
// vervang de list op het component
((ImageList)Component).List = newlist;
changeService.OnComponentChanged(Component,null,null,null);
// opnieuw renderen
UpdateDesignTimeHtml();
}
Het laatste dat we nog moeten verzorgen is het tonen van een lijst met images in design-time mode. Doordat de control geen run-time gedrag kent, geeft de baseclass implementatie geen representatie van onze control. Deze roept namelijk de render methode aan van de control en die heeft geen implementatie.
Design-time willen we wel een lijst met images laten zien, dus moeten we hiervoor een implementatie maken in de designer. Dit doen we door de Methode GetDesignTimeHTML() te overriden en daarvoor een implementatie te leveren die de lijst met images uit de control leest en deze omzet in een lijst met images die worden getoond in de designer.
public override string GetDesignTimeHtml()
{
if ((((ImageList)Component).List==null ) ""
(((ImageList)Component).List.Length < 1))
{
return GetEmptyDesignTimeHtml();
}
// er zijn images; toon dan een lijstje met images
try
{
StringWriter sw = new StringWriter();
HtmlTextWriter writer = new HtmlTextWriter(sw);
IWebFormsDocumentService docService =
(IWebFormsDocumentService)Component.Site.GetService(
typeof(IWebFormsDocumentService));
foreach(string resource in ((ImageList)Component).List)
{
// get the path to the resource on disk. only required for designer
// run-time relative name is ok
string currentURL = docService.DocumentUrl.Substring(
0, docService.DocumentUrl.LastIndexOf("/")+1);
writer.AddAttribute(HtmlTextWriterAttribute.Src,
currentURL + resource,true);
writer.AddAttribute(HtmlTextWriterAttribute.Width,"32px");
writer.AddAttribute(HtmlTextWriterAttribute.Height,"32px");
writer.RenderBeginTag(HtmlTextWriterTag.Img);
writer.RenderEndTag();
}
return sw.ToString();
}
catch(Exception e)
{
// toon de foutmelding in de designer op de standaard manier
return GetErrorDesignTimeHtml(e);
}
}
In de implementatie wordt eerst gekeken of de property list van de control is gevuld met namen van images die in de website zijn geselecteerd via de url builder. Als de lijst gevuld is, zal deze lijst worden doorlopen om met behulp van een HTMLTextWriter de juiste HTML te genereren voor het tonen van een lijst met images.
Integratie met label control
Nu we de imagelist klaar hebben, gaan we de eerder gemaakte label control aanpassen. We doen dit zodanig dat de control voortaan een image kan selecteren uit een beschikbaar imagelist control op de pagina. Dit geselecteerde image wordt dan vervolgens voor de tekst van het label geplaatst bij het tonen van het label.
Hiervoor gaan we een design-time property toevoegen aan de control - deze is dus run-time niet op de control beschikbaar - die de index aangeeft in de lijst met images van de imagelist control. Verder wordt er een property van het type string toegevoegd aan de control die de naam van de resource bevat na de selectie van een image uit de imagelist. Deze property is wel run-time beschikbaar, zodat deze kan worden gebruikt om run-time het image te tonen.
Eerst maken we een designer aan die gekoppeld wordt aan de control door middel van het designer attribuut, op de zelfde wijze als bij de imagelist control.
Om een design-time only property te maken dient voor de label control een implementatie te worden gemaakt van een designer. Een designer heeft namelijk een methode PreFilterProperties die is bedoeld om een designer de mogelijkheid te geven de properties die een control heeft design-time aan te passen. Dit houdt in dat er properties kunnen worden gefilterd of kunnen worden toegevoegd. Deze methode ontvangt een Dictionary als argument die de lijst met beschikbare properties op de control bevat. Door nu zelf aan deze lijst een property toe te voegen is het mogelijk alleen design-time een property beschikbaar te stellen. De designer wordt immers run-time niet geactiveerd.
De code hieronder geeft de implementatie weer voor de property “ImageIndex”.
protected override void
PreFilterProperties(System.Collections.IDictionary properties)
{
// Voeg een property toe die de index van een image
// uit een image list kiest mits een imagelist is gebonden
properties["ImageIndex"] =
TypeDescriptor.CreateProperty(
typeof(TextLabelDesigner),
"ImageIndex",
typeof(int),
CategoryAttribute.Appearance,
DesignOnlyAttribute.Yes,
DesignerSerializationVisibilityAttribute.Visible);
}
Bij het aanmaken van de property wordt een TypeDescriptor aangemaakt die de property laat verwijzen naar een property die op de designer is geïmplementeerd. Verder worden het type van de property, de categorie en de naam van de property meegegeven als argument. De naam die de property krijgt, dient overeen te komen met de naam van een property (die ook van hetzelfde type is) die is geïmplementeerd op de designer. De designer moet dus een property krijgen met de naam “ImageList”.
De implementatie van de ImageIndex property moet een aantal stappen bevatten te weten:
- Het opslaan van de waarde of teruggeven van de geselecteerde index waarde.
- Zodra de property wordt gevuld, dient te worden gecontroleerd of er een imagelist control op de pagina aanwezig is.
- Indien er een imagelist control aanwezig is, moet worden gecontroleerd of de index een geldige waarde heeft.
- Indien de waarde correct is, dient de imageResource property op de control te worden gevuld, zodat deze het image kan laten zien.
Het zoeken of een contol beschikbaar is op een pagina kan als volgt worden gedaan:
zoek vanuit de designer, via de Site van de control, de Container op (zie figuur 2 m.b.t. relaties). De Container bevat een “Controls” property die een lijst bevat van alle beschikbare controls op de pagina . Door nu deze lijst te doorlopen en te controleren of een control van het type ImageList is kunnen we een beschikbare imagelist control achterhalen op de pagina.
De implementatie voor het ophalen van de imagelist control op de huidige pagina ziet er als volgt uit:
private void SelecteerImageList()
{
// zoek alle controls af die op dit moment aan de page zijn
// gekoppeld in de designer.
foreach (IComponent c in Component.Site.Container.Components)
{
if (c.Site != null)
{
// als we een imagelist control vinden, bind daar dan aan zodat
// we daaruit images kunnen halen die we gaan tonen in het label.
if (c is ImageList)
{
// bewaar de reference in een instance variable
// naar de image list zodat we hieruit
// de image name kunnen halen.
_il = (ImageList)c;
// zet de selected index initieel op 0;
if (ImageIndex == -1)
{
// laat de control zich opnieuw renderen in de
// ImageIndexProperty.
ImageIndex = 0;
}
else
{
// laat de control zich opnieuw renderen.
UpdateDesignTimeHtml();
// we gaan er van uit dat er maar 1 list op de page
// bestaat.
return;
}
}
}
}
}
Er wordt door alle controls op de pagina gelopen. Zodra een control van het type ImageList is gevonden, zal deze worden toegekend aan een instance variabele die verder kan worden gebruikt in de designer. Verder wordt er een UpdateDesignTimeHtml methode aangeroepen. Deze zorgt ervoor dat de designer-host de user-interface opnieuw gaat opbouwen voor deze control. Hierdoor zal de GetDesignTimeHtml methode in de designer worden aangesproken. Deze bevat in het geval van de imagelist het default gedrag (we hebben deze methode niet overridden), dus zal de render methode in de label control het image moeten tonen.
Bij het vullen van de ImageIndex vindt nog een controle plaats of de index binnen de range van images valt die in de image list zitten. Verder wordt hier ook de controle uitgevoerd of er al een imagelist is gekoppeld aan de control. Is dat niet het geval dan zal de hierboven getoonde code worden aangesproken. De implementatie van de ImageList property staat hieronder:
public int ImageIndex
{
get
{
return _imageIndex;
}
set
{
if (_il == null)
{
// zoek een imagelist control op de page. Indien niet gevonden
// vind de afhandeling hieronder plaats
SelecteerImageList();
if (_il == null)
{
MessageBox.Show(
"Er is geen image list Control aanwezig",
"image selectie",
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
return;
}
}
// kunnen we de waarde instellen ?
if (_il != null)
{
if (value > _il._imageList.Length -1)
{
_imageIndex = -1;
int maxLength = _il._imageList.Length -1;
throw new ArgumentException(
"Maximum waarde is " + maxLength.ToString() + "!");
}
// zorg er voor dat de designer host weet dat er properties zijn
// aangepast op de control.
// Hiermee wordt de tagpersistence geactiveerd.
IComponentChangeService ccs=
(IComponentChangeService)GetService(typeof(IComponentChangeService));
ccs.OnComponentChanging(Component,null);
// zet de waarde en check hierna of dat ook is toegestaan.
_imageIndex = value;
// pas de waarde van de resource string aan zodat deze direct
// wordt getoond in de control
((TextLabel)Component).imageResourceName =
(string)_il._imageList[_imageIndex];
ccs.OnComponentChanged(Component, null, null, null);
}
}
}
Het enige dat nu nog rest, is het aanpassen van de render methode van de label control. Deze zal een image tag moeten genereren zodra een image geselecteerd is. In bovenstaande implementatie kun je zien dat de property imageResourceName wordt gevuld zodra een correcte index is geselecteerd. In de render methode moet de property imageResourceName worden opgevraagd. Indien deze een waarde heeft, moet er een extra html tag worden gegenereerd die aan het image refereert.
De implementatie hiervoor is als volgt:
protected override void RenderContents(HtmlTextWriter output)
{
// is er een image geselecteerd dan ook deze renderen
if (imageResourceName != string.Empty)
{
output.AddAttribute(HtmlTextWriterAttribute.Src,
imageResourceName ,true);
output.RenderBeginTag(HtmlTextWriterTag.Img);
output.RenderEndTag();
}
// output de text
output.Write(Text);
}
Nu ook het tonen van het image in het label geïmplementeerd is, zijn we klaar met onze controls. Nadat de code is gecompileerd, kunnen via een nieuwe instantie van Visual Studio.NET in een web project de controls op een pagina worden geplaatst. Hier kun je vervolgens de controls uitproberen en kun je een design maken zoals eerder getoond in figuur 1.
N.B.: De volledige sourcecode is beschikbaar in een zipfile. De zipfile bevat één C# project waar de controls en hun designers in zitten : Vries_AspNetServerControlPart4_sourcefiles.zip (11 kb)
Conclusie
In de afgelopen vier artikelen heb ik je hopelijk een helder beeld kunnen geven van de mogelijkheden die ASP.NET biedt om zelf Server controls te bouwen. De manier waarop de ASP.NET architectuur is opgezet biedt een ongelofelijke flexibiliteit en aanpasbaarheid die je maar weinig terugvindt in software frameworks. ASP.NET is in mijn beleving dan ook een superieure omgeving om je webapplicaties in te ontwikkelen. Als je zelf aan de slag gaat met het bouwen van web applicaties zal de mogelijkheid controls te kunnen schrijven je zeker goed helpen de snelheid en het gemak van ASP.NET nog verder te verbeteren.
Eerdere artikelen in deze serie:
Ing. Marcel de Vries
ICT-Architect Info Support