Costruzione di custom control per ASP.NET: parte 3

Di Daniele Bochicchio

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 Analisi dei composite control
Come creare un composite control Come creare un composite control
Il primo controllo Composite Il primo controllo Composite
Event bubbling: propagare all’esterno l’evento di un controllo contenuto Event bubbling: propagare all’esterno l’evento di un controllo contenuto
Quando utilizzare INamingContainer Quando utilizzare INamingContainer
Conclusioni Conclusioni
Approfondimenti 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

Raccolta di script ed articoli sui custom control da ASPItalia.com