Costruzione di custom control per ASP.NET: parte 3
Dopo i precedenti articoli della serie, dedicati ad un’introduzione circa la creazione ed al mantenimento dello stato nei custom control, in questo nuova puntata ci soffermeremo in maniera specifica su un particolare tipo di controlli, che sono in realtà composti da più controlli e prendono pertanto il nome di composite control.
L’argomento è molto stuzzicante per chi costruisce GUI mediamente complesse, perchè affronta un caso tipico dello sviluppo, cioè l’incapsulamento in un solo controllo di funzionalità già presenti, o da sviluppare, su più controlli, unite insieme per comodità e per garantire una implementazione rapida e semplice di uno specifico caso di sviluppo.
In questa pagina
Analisi dei composite control
Come creare un composite control
Il primo controllo Composite
Event bubbling: propagare all’esterno l’evento di un controllo contenuto
Quando utilizzare INamingContainer
Conclusioni
Approfondimenti
Analisi dei composite control
Un composite control è dunque semplicemente un controllo composto a partire da altri controlli, che ha la caratteristica di avere un modello di sviluppo che è tipico degli user control, cioè di quei frammenti di pagine racchiusi in file .ascx e programmati quasi come se fossero controlli.
Questa tipologia di controllo è dunque in realtà formato unendo le caratteristiche di controlli già esistenti, ma in maniera programmatica, cioè scrivendo codice all’interno di una classe in luogo della modalità dichiarativa, come è tipico degli user control, analizzati nella prima puntata di questa serie.
I composite control dunque rispondo ad esigenze particolari e che si ripetono all’interno di un sito. Ne sono un esempio una textbox con calendario collegato in popup, piuttosto che una dropdownlist con textbox associata per aggiungere un eventuale valore mancante, e più in generale qualsiasi necessità che all’interno del vostro progetto dovesse ripetersi più di una volta.
In caso di ripetizioni, infatti, vale la pena racchiudere le funzionalità all’interno di un controllo, per favorire sia l’implementazione che l’eventuale correzione di bug o aggiunta di comportamenti non inizialmente previsti.
Il vero vantaggio dei composite control è che consentono dunque di evitare di ripetere codice, centralizzando la defizione delle funzionalità e consentendone il riutilizzo in maniera più facile e produttiva.
In realtà la distinzione tra un composite control ed un custom control è molto sottile, dato che molto spesso nella creazione di un controllo si fa uso di funzionalità già offerte da altri, però è utile trattarli come caso a parte, poichè hanno delle caratteristiche e necessitano di alcune accortezze particolari da tenere in considerazione.
Come creare un composite control
ASP.NET prevede una classe specifica da utilizzare quando è necessario implementare questo tipo di controlli, presente nel namespace System.Web.UI.WebControls e chiamata CompositeControl.
Si tratta di una classe molto semplice, che eredita dalla classe WebControl ed offre un supporto minimo per il design all’interno di Visual Studio, più alcune funzionalità tipiche appunto dei WebControl, come il supporto per stili e personalizzazioni grafiche, oltre al DataBinding.
Non necessita di particolare approfondimento, ma è utile sottolineare che non è l’unica via per implementare un composite control.
Qualora se ne voglia implementare uno partendo da zero, è possibile utilizzare direttamente la classe Control, base di tutti i controlli inseriti all’interno di una pagina ASP.NET, potendo contare su una maggiore possibilità di personalizzazione in fase di definizione delle caratteristiche.
In questo caso è necessario effettuare l’override del metodo protetto CreateChildControls, derivato dalla classe Control utilizzata come base. All’interno di questo override vanno creati i controlli di cui sarà composto, assegnandone le proprietà e, nel caso, provvedendo al subscribe degli eventi.
Non è necessario invece toccare la logica di rendering e quindi non serve fare l’ovveride del metodo Render, perchè già di default è previsto un meccanismo che si occupa di renderizzare tutti i controlli figli contenuti all’interno di un controllo.
All’atto pratico, infatti, non c’è nessuna differenza tra l’aggiungere in una pagina un PlaceHolder con una Label ed una TextBox rispetto a creare un composite control che fornisca gli ultimi due controlli incapsulati all’interno di un solo contenitore: in entrambi i casi si tratta di rendere gli ultimi due controlli figli di quello genitore.
Il primo controllo Composite
Il tipico esempio da cui si parte è proprio questo di un controllo che mostri già, incolonnati, un testo ed una Texbox, da utilizzare per rendere più semplice l’inserimento di una maschera di richiesta dati.
In questo caso optiamo per partire dalla creazione di un controllo composite senza sfruttare la classe base, ereditando direttamente da Control.
Si procede dunque effettuando l’override del metodo CreateChildControls e creando i controlli (2 Literal, 1 Label ed un TextBox) necessari.
C#
using System; using System.Web.UI; using System.Web.UI.WebControls; namespace ASPItalia.com.Web.UI.Controls { public class SuperTextBox: Control { private string _description; public string Description { get { return _description; } set { _description = value; } } public string Text { get { // crea i controlli se non è stato ancora fatto EnsureChildControls(); return ((TextBox)Controls[3]).Text; } set { // crea i controlli se non è stato ancora fatto EnsureChildControls(); ((TextBox)Controls[3]).Text = value; } } protected override void CreateChildControls() { // aggiungo un controllo literal Controls.Add(new LiteralControl("<p>")); // aggiungo una label per la descrizione Label labelText = new Label(); labelText.Text = Description; Controls.Add(labelText); // aggiungo uno spazio Controls.Add(new LiteralControl(": ")); // aggiungo la TextBox TextBox box = new TextBox(); Controls.Add(box); } } }
VB
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Namespace ASPItalia.com.Web.UI.Controls Public Class SuperTextBox Inherits Control Private _description As String Public Property Description() As String Get Return _description End Get Set _description = value End Set End Property Public Property Text() As String Get ' crea i controlli se non è stato ancora fatto EnsureChildControls() Return DirectCast(Controls(3), TextBox).Text End Get Set ' crea i controlli se non è stato ancora fatto EnsureChildControls() DirectCast(Controls(3), TextBox).Text = value End Set End Property Protected Overloads Overrides Sub CreateChildControls() ' aggiungo un controllo literal Controls.Add(New LiteralControl("<p>")) ' aggiungo una label per la descrizione Dim labelText As New Label() labelText.Text = Description Controls.Add(labelText) ' aggiungo uno spazio Controls.Add(New LiteralControl(": ")) ' aggiungo la TextBox Dim box As New TextBox() Controls.Add(box) End Sub End Class End Namespace
Si può notare la differenza di approccio per quanto riguarda le due proprietà. Quella da utilizzare come descrizione, infatti, è una proprietà “normale”, con un campo di appoggio privato che viene caricato ed assegnato alla creazione del controllo Label. Viceversa, per la TextBox si è optato per una scelta diversa, che ha il vantaggio di riflettere immediatamente il valore contenuto nella TextBox, sfruttando i meccanismi di persistenza che quest’ultimo ha nel ViewState o nella richiesta, sfruttando il metodo POST.
Si può notare, a tal proposito, l’aggiunta della chiamata al metodo EnsureChildControls(), necessaria per essere sicuri che in quel particolare momento i controlli siano effettivamente creati ed evitare riferimenti ad oggetti che altrimenti poi sarebbero nulli.
Aggiungendo un sito web alla soluzione, come nell’esempio che si può scaricare tra gli approfondimenti, è possibile poi utilizzare il controllo, ricordandosi di aggiungere un riferimento al progetto di tipo Class Library creato in precedenza.
La defizione del controllo nella pagina, codice di esecuzione a parte, è il seguente:
<%@ Register TagPrefix="aspitalia" Namespace="ASPItalia.com.Web.UI.Controls" Assembly="ASPItalia.com.CustomControls3" %> <form id="form1" runat="server"> <div> <aspitalia:SuperTextBox id="BoxFirstName" runat="server" Description="Inserisci il tuo nome" Text="Daniele" /> <br /><asp:Button ID="SubmitButton" runat="server" OnClick="GetName" Text="Conferma" /> <p><asp:Literal ID="Result" runat="server" /></p> </div> </form>
Event bubbling: propagare all’esterno l’evento di un controllo contenuto
La definizione di un event handler per un evento di un controllo è generalmente utilizzata, da chi scrive custom control, per consentire allo sviluppatore che li andrà ad utilizzare la possibilità di associare comportamenti personalizzati da associare allo stato del controllo.
Nel caso di composite control, questa necessità si tramuta quasi sempre nella creazione di un evento che in realtà effettui il bubbling, cioè la propagazione dell’evento di un controllo contenuto nel composite control all’esterno dello stesso, tipicamente nella pagina che andrà ad ospitarlo.
In questi casi si procede con la defizione di un evento nel custom control, usando il pattern per la definizione degli eventi utilizzato con il .NET Framework, che prevede la dichiarazione dell’evento stesso e poi un metodo protected che lo scateni, contraddistinto dall’avere il prefisso “On” nel nome dell’evento per il quale viene creato.
C#
public event EventHandler SelectedValueChanged; protected void OnSelectedValueChanged (EventArgs e) { SelectedValueChanged(this, e); }
VB
Public Event SelectedValueChanged As EventHandler Protected Sub OnSelectedValueChanged(ByVal e As EventArgs) RaiseEvent SelectedValueChanged(Me, e) End Sub
Una volta fatto questo, è necessario agganciare l’evento vero e proprio sul controllo che lo scatenerà, con la solita sintassi di VB, o nel caso di C# 2.0, potendo sfruttare una delle novità, ovvero la possibilità di definire un anonymous method, che richiami il metodo protetto direttamente in linea al scatenarsi dell’evento e consenta quindi al nostro codice di entrare in funzione.
C#
listControl.SelectedIndexChanged += delegate(object sender, EventArgs e) { OnSelectedValueChanged(e); };
VB
AddHandler listControl.SelectedIndexChanged, AddressOf listControl_SelectedIndexChanged Private Sub listControl_SelectedIndexChanged(sender As Object, e As EventArgs) OnSelectedValueChanged(e) End Sub
A questo punto è sufficiente compilare il tutto, così da avere l’assembly con il controllo pronto, inserirlo nella pagina e sfruttare il meccanismo di AutoPostBack della DropDownList per avere la possibilità di intercettare, nella pagina, il cambio di selezione del controllo contenuto e mostrarne a video il valore:
<aspitalia:SuperDropDownList id="List" runat="server" Description="Seleziona una città" AutoPostBack="true" OnSelectedValueChanged="GetChanges" />
C#
protected void GetChanges(object sender, EventArgs e) { Result.Text = "Hai scelto " + List.SelectedValue + " cambiando la selezione"; }
VB#
Protected Sub GetChanges(ByVal sender As Object, ByVal e As EventArgs) Result.Text = "Hai scelto " & List.SelectedValue & " cambiando la selezione" End Sub
L’effetto che si ottiene è quello di poter ora intercettare, se è necessario, un evento di un controllo contenuto all’interno del nostro composite control come si trattasse di un normale evento che il nostro controllo stesso mostra all’esterno.
C#
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.Collections; namespace ASPItalia.com.Web.UI.Controls { public class SuperDropDownList: Control, INamingContainer { private string _description; public string Description { get { return _description; } set { _description = value; } } public IList DataSource { get { // crea i controlli se non è stato ancora fatto EnsureChildControls(); return ((DropDownList)Controls[3]).DataSource as IList; } set { // crea i controlli se non è stato ancora fatto EnsureChildControls(); ((DropDownList)Controls[3]).DataSource = value; } } public string SelectedValue { get { // crea i controlli se non è stato ancora fatto EnsureChildControls(); return ((DropDownList)Controls[3]).SelectedValue; } set { // crea i controlli se non è stato ancora fatto EnsureChildControls(); ((DropDownList)Controls[3]).SelectedValue = value; } } public bool AutoPostBack { get { // crea i controlli se non è stato ancora fatto EnsureChildControls(); return ((DropDownList)Controls[3]).AutoPostBack; } set { // crea i controlli se non è stato ancora fatto EnsureChildControls(); ((DropDownList)Controls[3]).AutoPostBack = value; } } // definizione dell'evento public event EventHandler SelectedValueChanged; protected void OnSelectedValueChanged(EventArgs e) { SelectedValueChanged(this, e); } protected override void CreateChildControls() { // aggiungo un controllo literal Controls.Add(new LiteralControl("<p>")); // aggiungo una label per la descrizione Label labelText = new Label(); labelText.Text = Description; Controls.Add(labelText); // aggiungo uno spazio Controls.Add(new LiteralControl(": ")); // aggiungo la DropDownList DropDownList listControl = new DropDownList(); // aggancio dell'evento di cambio della dropdownlist listControl.SelectedIndexChanged += delegate(object sender, EventArgs e) { OnSelectedValueChanged(e); }; Controls.Add(listControl); } } }
VB
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.Collections Namespace ASPItalia.com.Web.UI.Controls Public Class SuperDropDownList Inherits Control Implements INamingContainer Private _description As String Public Property Description() As String Get Return _description End Get Set _description = value End Set End Property Public Property DataSource() As IList Get ' crea i controlli se non è stato ancora fatto EnsureChildControls() Return TryCast(DirectCast(Controls(3), DropDownList).DataSource, IList) End Get Set ' crea i controlli se non è stato ancora fatto EnsureChildControls() DirectCast(Controls(3), DropDownList).DataSource = value End Set End Property Public Property SelectedValue() As String Get ' crea i controlli se non è stato ancora fatto EnsureChildControls() Return DirectCast(Controls(3), DropDownList).SelectedValue End Get Set ' crea i controlli se non è stato ancora fatto EnsureChildControls() DirectCast(Controls(3), DropDownList).SelectedValue = value End Set End Property Public Property AutoPostBack() As Boolean Get ' crea i controlli se non è stato ancora fatto EnsureChildControls() Return DirectCast(Controls(3), DropDownList).AutoPostBack End Get Set ' crea i controlli se non è stato ancora fatto EnsureChildControls() DirectCast(Controls(3), DropDownList).AutoPostBack = value End Set End Property ' definizione dell'evento Public Event SelectedValueChanged As EventHandler Protected Sub OnSelectedValueChanged(ByVal e As EventArgs) RaiseEvent SelectedValueChanged(Me, e) End Sub Protected Overloads Overrides Sub CreateChildControls() ' aggiungo un controllo literal Controls.Add(New LiteralControl("<p>")) ' aggiungo una label per la descrizione Dim labelText As New Label() labelText.Text = Description Controls.Add(labelText) ' aggiungo uno spazio Controls.Add(New LiteralControl(": ")) ' aggiungo la DropDownList Dim listControl As New DropDownList() AddHandler listControl.SelectedIndexChanged, AddressOf listControl_SelectedIndexChanged ' aggancio dell'evento di cambio della dropdownlist Controls.Add(listControl) End Sub Private Sub listControl_SelectedIndexChanged (ByVal sender As Object, ByVal e As EventArgs) OnSelectedValueChanged(e) End Sub End Class End Namespace
Quando utilizzare INamingContainer
Nella creazione di custom control, l’interfaccia INamingContainer contenuta nel namespace System.Web.UI rappresenta una funzionalità che deve essere conosciuta e che nel caso di composite control assume un ruolo se vogliamo ancora più importante.
È una interfaccia vuota, che non prevede metodi o proprietà, ma che funziona come segnaposto per il Page Framework di ASP.NET, che se la trova implementata all’interno di un controllo capisce che quest’ultimo contiene dei controlli che devono avere ID univoco nella gerarchia dell’albero dei controlli di ASP.NET. Questa tecnica è utilizzata quando il composite control è contenuto all’interno di controlli che lo ripetono, come i data control, o più semplicemente perchè ce n’è più di uno all’interno della pagina.
Ebbene, questa interfaccia ha il semplice compito di fare in modo che gli ID siano univoci, anche perchè nel caso di controlli che facciano uso di Javascript, il contrario potrebbe rappresentare un problema di non poco conto.
Conclusioni
Con questa terza puntata dedicata alla costruzione di custom control siamo entrati nel vivo dell’argomento, affrontando tematiche più complesse ed analizzando più da vicino la creazione di alcuni controlli in grado di rispondere alle necessità più diffuse, anche come casistiche che si incontrano durante lo sviluppo.
Il consiglio, quando si intraprende per la prima volta questa attività, è quello di soffermarsi ad analizzare anche controlli esistenti, per trarre spunti di riflessione e capire più da vicino come chi scrive controlli di professione affronti le difficoltà quotidiane ed implementi le funzionalità più diffuse.
Approfondimenti
Libro 'ASP.NET 2.0 per tutti – seconda edizione' – 2007, Daniele Bochicchio, Cristian Civera, Riccardo Golia, Stefano Mostarda
Raccolta di script ed articoli sui custom control da ASPItalia.com