Runtime GUI’s met Runtime databound ASP.NET Templates (ITemplate en IBindableTemplate)
In ASP.NET v2.0 zijn heel wat verbeteringen uitgevoerd die het leven van een ontwikkelaar veraangenamen. Zo is two-way databinding mogelijk.
one- en two-way databinding
One-way databinding verzorgt het koppelen van data aan controls (ASP.NET v1 functionaliteit). Two-way databinding verzorgt eveneens het koppelen van data aan controls maar ook de terugkoppeling van de data in de controls naar de datalaag. Dus naast het tonen van data kan data na bewerking zonder tussenkomst van custom code weer worden opgeslagen.
Normaal gesproken wordt databinding in designtime geregeld. Men plaatst bijvoorbeeld een FormView op de pagina en past de templates daarvan zodanig aan dat deze overeenkomen met de op de betreffende pagina te bewerken entiteit. Bij grote websites is dit vaak veel van hetzelfde, neem bijvoorbeeld een CRM pakket. Het wordt al gauw een tijdrovende en foutgevoelige klus. Want al snel staat hier een spatie te veel en daar weer een dubbele punt te weinig, etc… In sommige gevallen kan het dus bijzonder handig zijn als pagina’s op basis van de betrokken entiteit automatisch wordt opgebouwd. Het komt de eenduidigheid en de snelheid waarmee pagina’s kunnen worden ontwikkeld ten goede.
In dit artikel wil ik aan de hand van een voorbeeld laten zien hoe onder andere runtime one- en two-way binded templates kunnen worden opgebouwd op basis van een willekeurige data klasse (entiteit). In ons geval een persoon. De belangrijkste klasse in het voorbeeld is de klasse die de koppeling van een FormView control aan een GridView control regelt. Daarnaast regelt deze klasse het koppelen van de custom FormView templates tijdens runtime. Voor het koppelen van data maken we in het voorbeeld gebruik van ObjectDataSource controls. Het is handig als je wat kennis hebt van reflection, binnenkort wil ik daar overigens ook een artikeltje aan weiden.
Omdat het hier slechts om een voorbeeld gaat zijn alle klassen opgenomen in een enkel webproject. Het project bestaat globaal uit een ListDetail klasse die de koppeling en inrichting van GridView en FormView controls verzorgt, de templates voor de FormView voor Read, Insert en Edit mode, een Persoon klasse die we gebruiken om de overige code te testen en natuurlijk een webpagina.
De sourcecode is van uitgebreidt commentaar voorzien en kan worden gedownload met behulp van de link bovenin dit artikel.
De ListDetail klasse
De ListDetail klasse heeft 3 properties. Er kan een GridView en FormView worden opgegeven. Daarnaast moet een entitytype worden opgegeven, in dit geval van het type Persoon. Verder is er een Init methode opgenomen deze dient te worden aangeroepen in de onInit methode van de webpagina.
OnInit of PageLoad
Voeren we de Init van de ListDetail uit in de pageload dan zijn we te laat om bijv. events te kunnen koppelen.
De Init methode is opgedeeld in 3 delen:
1 2 3 4 5 6 7 8 |
public void Init() { InitObjectDataSources(); InitGridView(); InitFormView(); } |
Voor de ObjectDataSources geldt dat zij niet op de webpagina hoeven te worden geplaatst zoals de GridView en de FormView. De ObjectDataSources worden namelijk gecreëerd in de ListDetail. Voor de IDs van de datasources wordt een vaste benaming aangehouden, mocht je echter meerdere ListDetail objecten op 1 pagina willen gebruiken dan is het beter de ObjectDataSource-ID te baseren op de gekoppelde GridView- of FormView-ID en dan met name de uniqueID van beidden. In de Init methode van de ObjectDataSources worden de ID’s van ObjectDataSources gekoppeld aan de GridView en FormView.
ID of Control
We koppelen niet de ObjectDataSource controls zelf. Dit kan overigens ook maar zal niet tot het gewenste resultaat leidden. Op een of andere manier komt de databinding dan niet correct tot stand. Dit heeft hoogstwaarschijnlijk te maken met state, in de state zal men geen ObjectDataSource control op willen slaan maar wel de ID daarvan.
Vervolgens worden aan de ObjectDataSources het entitytype meegegeven (in ons geval typeof(Persoon)
). Daarna moeten we opgeven met welke methoden binnen de Persoon klasse Personen kunnen worden opgehaald (Get
voor de FormView, All
voor de GridView), opgeslagen en verwijderd. Voor de Get methode is een parameter vereist, de ID van de op te halen persoon. Deze moet worden gekoppeld aan het geselecteerde item van de gridview. Hiervoor voegen we aan de ObjectDataSource van de FormView een ControlParameter toe de waarde van de parameter is gebaseerd op de SelectedValue property van de GridView. Daarmee is de koppeling tussen Grid- en FormView tot stand gebracht.
In de Init methode van de GridView wordt aangegeven dat de kolommen voor de GridView en een Select button automatisch mogen worden gegenereerd. Voor een echte toepassing moet hier natuurlijk nog het een en ander gebeuren.
In de Init methode van de FormView zetten we de defaultmode, in ons geval zie je de entity Persoon eerst ReadOnly. Het is belangrijk de propertyna(a)m(en) met de primaire sleutel van de Persoon klasse mee te geven aan zowel de FormView als de GridView. Dit doen we doormiddel van de DataKeyNames property. Voor Persoon is de property “ID” uniek.
Als er een modificatie van de data plaats vindt binnen de FormView moeten deze ook leiden tot een tot een update van de GridView. Vandaar dat in het ItemInserted, -Deleted, -Updated event, binding van de GridView opnieuw plaatsvind. In het laatste gedeelte van de FormView Init methode worden templates gekoppeld voor ReadOnly-, Edit- en Insert-mode.
De Templates
De templates moeten we zelf bouwen. Normaal gesproken worden deze gedefinieerd in designtime op de webpagina. Nu we dit runtime doen zullen we in code zorg moeten dragen voor de 3 werkende templates (ReadOnly, Insert en Edit).
De ReadOnly Template
Voor de ReadOnly template hoeven we enkel one-way databinding te implementeren. De data hoeft namelijk enkel maar getoond te worden. Hiervoor dienen we een klasse te definieren die ITemplate
implementeert, de door ons gedefinieerde FormViewReadOnlyTemplate
. De ITemplate
interface implementeerd 1 methode, de methode InstantiateIn(Control container)
. Naast deze methode definiëren we een property waarmee we op kunnen geven dat het om een Persoon klasse gaat. Deze geven we door via de ListDetail klasse.
De InstantiateIn(Control container)
methode
In deze methode wordt de opbouw verzorgd van de template op de container. Container is de plek waar we controls in kunnen plaatsen. Doormiddel van reflection kunnen we in de InstantiateIn methode alle properties van de Persoon klasse uitvragen en op basis daarvan per property een veld tonen. De waarde wordt binnen de InstantiateIn methode nog niet gevuld er wordt echter een Label control per property opgenomen met een herkenbare ID: _bound_[PropertyName]
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
foreach (PropertyInfo pi in EntityType.GetProperties())<br /> {<br />   if (pi.Name != "ID")<br />   {<br />      htmlhelper.AddHTMLCode(container, "<tr>");<br />      htmlhelper.AddHTMLCode(container, <br />         string.Format("<td><b>{0}</b></td>", pi.Name));<br />      htmlhelper.AddHTMLCode(container, "<td>");<br />      Label lbl = new Label();<br />      lbl.ID = "_bound_" + pi.Name;<br />      lbl.DataBinding += new EventHandler(lbl_DataBinding);<br />      container.Controls.Add(lbl);<br />      htmlhelper.AddHTMLCode(container, "</td>");<br />      htmlhelper.AddHTMLCode(container, "</tr>");<br />   }<br /> } |
Vervolgens wordt er een EventHandler aan DataBinding event van de labels gekoppeld. Op het moment dat een waarde noodzakelijk is zal de EventHandler de koppeling regelen. De EventHandler implementeren we dus ook.
1 2 3 4 5 6 7 8 9 10 11 |
void lbl_DataBinding(object sender, EventArgs e)<br /> {<br />   if (sender is Label)<br />   {<br />     FormView fv = (FormView)((Label)sender).NamingContainer;<br />     ((Label)sender).Text = DataBinder.Eval(fv.DataItem, <br />           ((Label)sender).ID.Replace("_bound_", "")).ToString();<br />   }<br /> } |
Door in de EventHandler de NamingContainer van de sender
(label) op te vragen krijgen we de FormView control terug. De FormView bevat een DataItem dit is een object dat in ons geval een Persoon object is. Op basis van de opgebouwde Label ID kunnen we achterhalen welke gegevens in de betreffende label moet worden geplaatst. De ID van het Label is bijvoorbeeld “_bound_Voornaam� we weten dan dus dat de waarde van de Voornaam property van het Persoon object in de Text property van de Label moet worden geplaatst. Dit doen we doormiddel van de functie DataBinder.Eval. In designtime zouden we in de webpagina binnen het Text attribute van de label
opnemen, runtime werkt dit echter niet.
<label … text=’<%# Bind("Voornaam") %>’ runat=�server�/>
In de InstantiateIn methode nemen we verder nog 3 LinkButtons op die het mogelijk maken een nieuwe Persoon aan te maken en de huidige persoon te bewerken of te verwijderen. We hoeven hier feitelijk niets speciaals mee te doen. Belangrijk om de linkbuttons juist te laten functioneren is de CommandName property op te geven: “New�, “Edit� en “Delete�. Om te zorgen dat bij verwijderen om een bevestiging wordt gevraagd wordt de OnClientClick
property gevuld van de Delete LinkButton.
De Edit- & Insert-Template
Om te zorgen dat we two-way databinding kunnen toepassen hebben we niet genoeg aan de ITemplate
interface maar dienen we de IBindableTemplate
te implementeren. IBindableTemplate
implementeert op zijn beurt ITemplate
. Omdat de Insert- en Edit-template nauwelijks van elkaar verschillen qua layout overerven deze van FormViewEditableTemplate
. Waarbij in de laatstgenoemde template de opbouw van het scherm wordt verzorgd binnen de InstantiateIn
methode. De implementatie verschilt nauwelijks van de ReadOnlyTemplate
het enige verschil is dat er geen labels maar TextBoxen op het scherm moeten worden getoond. Naast de InstantiateIn
methode implementeerde IBindableTemplate
ook de IOrderedDictionary ExtractValues(Control container)
methode. Deze verzorgt het terugschrijven van de controlwaarden naar een object die IDictionary
implementeert.
Een Dictionary bestaat uit name-value pairs. In het “name�-deel dient de naam van de property te worden opgegeven. In het “value�-deel de waarde van het Control die de property representeerd. In de code zie je dat als eerste de gegevens uit het DataItem in de Dictionary worden geplaatst. Dit laatste is belangrijk, mochten namelijk niet alle properties van, in dit geval het Persoon object, op het scherm getoond worden dan worden de niet getoonde properties niet juist teruggeschreven naar het Persoon object. Zouden we dit dus niet doen en hebben we bijvoorbeeld een Persoon met de Voornaam “Ton� maar tonen we Voornaam niet in de template en gaan we deze Persoon bewerken en opslaan dan is de Voornaam null of “� geworden.
Het DataItem is niet altijd beschikbaar dus dit moet ook worden gecontroleerd op null-waarde. Vervolgens worden alle properties van de betrokken entity afgelopen en wordt de Control die deze representeert erbij gezocht en de waarde in de dictionary geschreven. Nadat dit is gebeurd is de two-way databinding rond. In de 2 overerfden van de FormViewEditableTemplate
verzorgen we enkel de opbouw van de footer LinkButtons die specifiek zijn voor edit en insert.
De Persoon klasse
In dit voorbeeld wordt een Persoon klasse gebruikt. Voor de ListDetail is het echter niet noodzakelijk dat we een Persoon klasse hebben het mag ook elke andere zelfgedefinieerde entiteit zijn. De eis is wel dat de klasse de volgende methoden (static of non-static bevat, Get(int ID)
, All()
, Store([EntiteitType] o)
en Delete([EntiteitType] o)
. In deze methoden kan dataaccess van welk soort dan ook worden geïmplementeerd. Voor het gemak heb ik hier een static generic List gebruikt. Leuk voor testdoeleinden maar praktisch niet handig. Hier ligt dus nog wat werk bij het ontwikkelen van “eigen� entiteiten. Verder is het van belang dat de klasse properties bevat (met getters en setters). Op zijn minst dient de klasse een ID property te bevatten van het type Int.
De webpagina
Op de webpagina zijn een FormView en een GridView control geplaatst. In de OnInit
methode van de pagina wordt de ListDetail gecreëerd en geinitaliseerd.
De oplossing is klaar en we kunnen nu personen zien en bewerken op 1 enkele pagina zonder voor een andere entiteiten design van een GridView of FormView designtime arbeid noodzakelijk is.
Conclusie
De ListDetail voorziet in een basis voor het tonen en bewerken van klassen van welk soort dan ook. Het voorziet echter nog niet in elke type property bijvoorbeeld Booleans, entiteiten die gekoppeld zijn met andere entiteiten en zo meer. Het gaat dan ook om het idee en de techniek. Voor mijn werk ben ik op dit moment bezig met een uitgebreide implementatie van deze materie. We hebben hiervoor een presentatie-framework opgezet. Op deze manier is een 2-tier of 3-tier koppeling volgens de regels der kunst snel en elegant opgelost.
Kortom ik zou zeggen download de sourcecode en kijk er eens naar. Binnenkort een artikeltje over reflection. Op het moment dat je deze techniek zou willen adopteren is kennis van reflection een must.
Download source code