Da programmatore ASP.NET quale sono c'è una cosa che invidio a Windows Presentation Foundation. Non si tratta certo di animazioni, 3D e quant'altro che sono lontane anni luce da quel poco che si può fare con ASP.NET e con le recenti librerie AJAX e quindi non vale nemmeno la pena di invidiarle. Piuttosto se guardiamo al DataBinding, laddove ci sono delle profonde somiglianze, vi è anche una marcata differenza per quanto riguarda l'uso che se ne può fare in WPF quando ad esempio si collegano più controlli tra di loro. La prima volta che ho visto uno dei tanti esempi di DataBinding usato per questo scopo, sono rimasto letteralmente affascinato pensando a quanto codice mi avrebbe risparmiato il poter usare una sintassi simile in ASP.NET. Nulla di trascendentale, magari un checkbox che abilita o disabilita un pannello, un porzione di pagina che cambia colore in accordo con il valore di una DropDowList e così via, tutte cose che grazie al BindingEngine di WPF sono di una banalità impressionante ma che richiedono molto codice ridondante in ASP.NET.

Beh, la bella notizia è che con un po' di astuzia e con una buona conoscenza del funzionamento del modello di compilazione di ASP.NET (detto con tutta la modestia possibile, sia chiaro) la cosa si può fare eccome, e le differenze con WPF sono davvero minime, dettate soprattutto dal flusso di eventi che porta al rendering della pagina più che da altre sottigliezze. Ma prima di entrare nel merito di questa proof-of-concept che ho realizzato nelle serate della recente vacanza in Svizzera e che potete scaricare in coda a questo post, vale la pena di approfondire meglio con un piccolo esempio il funzionamento di WPF.

Binding, ElementName & Path

In WPF per realizzare il DataBinding si fa uso di quella che comunemente viene definita una Markup Extension. Le Merkup Extension che ora non è il caso di approfondire sono delle estensioni che permettono di semplificare la sintassi di XAML per introdurre referenze ed estensioni che arricchiscono il suo significato. "Binding" è la markup extension che si incarica di collegare i dati agli elementi compositivi dell'interfaccia. La cosa curiosa è che mediante Binding è possibile specificare come sorgente dati un elemento dell'interfaccia stessa.

<Border Margin="{Binding ElementName=sldDistance, Path=Value}"></Border>

Con una sintassi come quella specificata nel riquadro è possibile ad esempio specificare uno slider come sorgente dati di un Border. Il risultato sarà che spostando lo slider varia il margine del bordo. ElementName infatti specifica il legame con un ipotetico controllo Slider che porta questo nome mentre path referenzia la sua proprietà Value.

Penso sia evidente a tutti la potenza di una sintassi del genere che nel caso di WPF consente di applicare delle logiche anche molto complesse grazie anche all'uso di Converter che nella fase di binding sono in grado di applicare delle conversioni ai dati che transitano in una direzione e nell'altra. Per un esempio completo di quanto descritto vi invito ad approfondire un tempo fa.

Do it in ASP.NET

E' giunto il momento di mettere assieme i tasselli della soluzione che ho architettato per mimare in ASP.NET questo stesso comportamento. Il primo di essi si chiama ExpressionBuilder. Il più usato e famoso degli ExpressionBuilder è quello che in ASP.NET consente di specificare le risorse localizzate in una pagina. Si tratta semplicemente di un codeblock il cui prefisso è il simbolo del dollaro ($) che si comporta in modo molto particolare. Esso infatti viene valutato al momento della compilazione della pagina e da adito ad una piccola porzione di codice che assegna una attributo di un server control. Questo pezzetto di codice, generato per mezzo di CodeDOM viene inserito nella pagina compilata dal runtime ed è in grado di valorizzare la proprietà dui è associato applicando delle logiche particolari. Per una spiegazine estensiva vi invito a leggere un .

L'idea che mi è venuta è quella di sfruttare un ExpressionBuilder per simulare la markup extension di WPF. Una cosa che occorre tenere presente è che la stringa che funge da parametro di un ExpressionBuilder viene passata per intero al metodo responsabile di generare il codice perciò è molto semplice introdurre una sintassi simile, se non identica a quella delle markup extension.

BackColor="<%$ Binding: ElementName=ddlColor, Path=SelectedValue %>"

L'idea era buona, ma il problema è che il codice generato dalla ExpressionBuilder deve condurre all'assegnazione della proprietà cui è associata. Per questo mi ci è voluto un po' per arrivare ad un metodo alternativo, ma poi, meditando sull'uso degli Anonymous Methods ho trovato la giusta soluzione che ora provo ad illustrare.

Il primo passo è stato creare un metodo statico, nella classe che rappresenta il Builder, il quale si occupi di raccogliere le informazioni dei controlli specificati nel Binding e le utilizzi nel metodo anonimo associato all'evento load della pagina. Ecco il metodo in questione:

public static T MakeBinding<T>( Page page, string boundControlId, string boundPropertyName, string targetControlId, string targetPropertyName, string converter, string converterParameter) { page.Load += delegate(object sender, EventArgs e) { Control bound = page.Form.FindControl(boundControlId); if (bound == null) return; Control target = page.Form.FindControl(targetControlId); if (target == null) return; Type boundType = bound.GetType(); PropertyInfo boundProperty = boundType.GetProperty(boundPropertyName); if (boundProperty == null) return; Type targetType = target.GetType(); PropertyInfo targetProperty = targetType.GetProperty(targetPropertyName); if (targetProperty == null) return; object targetValue = targetProperty.GetValue(target, null); IValueConverter converterInstance = CreateConverter(converter); boundProperty.SetValue(bound, converterInstance.Convert( targetValue, boundProperty.PropertyType, converterParameter, CultureInfo.CurrentUICulture), null); }; return default(T); }

Il metodo è un Generico perchè deve restituire un tipo conforme con la proprietà cui è associato il Binding. Esso poi riceve in ingresso i parametri che vengono estratti dalla stringa dell'ExpressionBuilder. In prima istanza non fa altro che agganciare un anonymous method all'evento load della pagina. Al momento in cui l'evento viene sollevato i parametri vengono usati per  trovare i controlli referenziati nella pagina e copiare i valori opportunamente trasformati dal convertitore prescelto oppure da quello di Default. Il convertitore altro non è che una classe che implementa l'interfaccia IValueConverter, la stessa che fa capo ai convertitori di WPF. Infine il metodo ritorna il valore di default per il tipo generico.

L'expressionBuilder non fa altro che forgiare una chiamata al metodo statico MakeBinding<T>() con gli opportuni parametri:

public override CodeExpression GetCodeExpression( BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { Dictionary<string, string> values = Parse(entry.Expression); return new CodeSnippetExpression( string.Format( @"Elite.Web.UIDataBinding.BindingExpressionBuilder.MakeBinding<{0}>" + @"(this, ""{1}"", ""{2}"", ""{3}"", ""{4}"", ""{5}"", ""{6}"")", entry.PropertyInfo.PropertyType.FullName, entry.ControlID, entry.PropertyInfo.Name, values["ElementName"], values["Path"], values.ContainsKey("Converter") ? values["Converter"] : null, values.ContainsKey("ConverterParameter") ? values["ConverterParameter"] : null)); }

Si potrebbe fare meglio, usando bene le classi e i metodi di CodeDOM, ma per questo esempio è sufficiente. Tutto sommato il mio obbiettivo è di provare che il meccanismo funziona a dovere. Scaricando il codice troverete un piccolo esempio che associa ad AJAX l'utilizzo dell'ExpressionBuilder. La cosa ce vi invito a notare è che per realizzarlo non è stato necessario scrivere alcun codice nel codebehind della pagina perchè tutte i comportamenti sono conferiti mediante l'Expression Builder. Chi intendesse utilizzarlo tenga presenta che si tratta di poco più che un test, e che ha alcune limitazioni rispetto a WPF, dovute più che altro alla mancata implementazione:

  1. La proprietà Path non consente di specificare un vero e proprio Path come in WPF ma solo il Control ID.
  2. I controlli vengono cercati solo al primo livello della gerarchia
  3. Specificare un converter è un po' difficile perchè richiede di immettere il namespace e la classe. Sarebbe meglio trovare un metodo per abbreviare la sintassi che altrimenti rischia di essere un po' troppo logorroico.-
  4. Le proprietà composte di ASP.NET non sono supportate (es. Font-Size)

Tutte queste limitazioni sono facilmente superabili e probabilmente un po' alla volta cercherò di pubblicare nuove versioni se il Binder incontrerà il successo che ha avuto ASP.NET. Per il momento vi invito a provare l'esempio di cui potete vedere uno spezzone in questo riquadro:

<div class="panel"> <asp:Panel id="pnlFont" runat="server" CssClass="scroll-panel" Height="100px"> <asp:Image id="imgSpiderman" runat="server" ImageUrl="images/spiderman.jpg" Width="<%$ Binding: ElementName=ddlSize, Path=SelectedValue, Converter=Elite.Web.UIDataBinding.UnitConverter %>" Height="<%$ Binding: ElementName=ddlSize, Path=SelectedValue, Converter=Elite.Web.UIDataBinding.UnitConverter %>" /> </asp:Panel> <p class="control-row"><label>Size:</label> <asp:DropDownList ID="ddlSize" runat="server" AutoPostBack="true"> <asp:ListItem Text="25%" Value="25" /> <asp:ListItem Text="50%" Value="50" /> <asp:ListItem Text="100%" Value="100" Selected="True" /> <asp:ListItem Text="200%" Value="200" /> <asp:ListItem Text="400%" Value="400" /> </asp:DropDownList></p> </div>

Download: Elite.Web.UIDataBinding.zip (23,4 Kb)

Video: Demo (72 KB)


Aggiungi Commento