I controlli compositi ci permettono di riutilizzare set di controlli definendone uno solo. Possiamo definire i "controlli figli" per la costruzione di interfacce e funzionalità avanzate, e richiamare tutto il "blocco" da un unico oggetto in qualunque pagina delle nostre applicazioni Web.
Questo tipo di controlli fa parte della famiglia dei "Web Server Controls", che abbiamo visto in un articolo precedente, e sono largamente utilizzati all'interno dell'intera architettura di ASP.NET, ma tornano anche molto utili allo sviluppatore nel caso in cui si voglia incapsulare diversi tipi di logica in un unico contenitore.
Possono essere anche visti come delle vere e proprie collezioni di controlli, i quali però non combinano funzionalità e comportamenti tra di loro, ma lavorano assieme per creare nuove funzionalità più complesse.
Le stesse pagine ASP.NET, come pure gli User Control (.ascx), possono essere considerati controlli compositi, in quanto contengono una quantità di sotto-controlli per definire l'interfaccia del sito. In questi casi la "composizione" avviene attraverso il markup e non implementando direttamente la classe del controllo via
codice.
Confronto con .NET 1.x
È possibile creare controlli compositi sin dalle prime versioni del .NET Framework; prima dell'arrivo della 2.0 però, la procedura era più complessa in quanto bisognava ereditare dalla classe base WebControl
e implementare l'interfaccia INamingContainer. Tale interfaccia, tuttora in uso nel framework, garantisce l'univocità degli attributi ID di tutti i controlli figlio, perché questi possano essere possano essere richiamati direttamente dall'ID oppure con il metodo FindControl()
, ereditato dalla classe Control
(che, se non viene implementata l'interfaccia INamingContainer
, ritorna sempre null).
È sempre stato importantissimo implementare questa interfaccia, anche perché si evitano tranquillamente i conflitti di nomi una volta che i vari controlli vengono inseriti all'interno di pagine .aspx complesse. Grazie a questo meccanismo, ogni controllo, una volta "renderizzato" all'interno di una pagina ASP.NET, risulta avere un ID univoco, diverso da quello iniziale, e composto da tutti gli ID dei controlli contenitori, separati dal simbolo '$' (dollaro), secondo la gerarchia stabilita in fase di "composizione".
Esempio di ID
IDControlloPadre$IDControlloFiglio
Nella versione 2.0 di ASP.NET, è stata aggiunta una classe apposita che implementa già l'interfaccia INamingContainer
e che fornisce le funzionalità base per la creazione di controlli che ne contengono altri. Si tratta della classe astratta CompositeControl, compresa nel namespace System.Web.UI.WebControls
. grado di contenere la funzionalità di controllo della presenza di controlli figli prima di accedervi (cosa questa, che prima doveva essere fatta a mano).
Ereditando da questa classe quindi, possiamo creare i nostri controlli compositi personalizzati in grado di incapsulare logiche diverse all'interno di un unico oggetto. All'interno del Framework possiamo già trovare componenti che ereditano da questa classe: sono il controllo Login
e il controllo Wizard
.
Un primo esempio di controllo composito
Come esempio d'uso, creaimo un controllo per l'immissione ed il salvataggio dei dati di un cliente su database, con la relativa intefaccia grafica. Iniziamo, quindi, ereditando dalla classe CompositeControl
:
Ereditare la classe CompositeControl
namespace HTMLit.Web.UI.WebControls
{
public class DettagliUtente : CompositeControl
{
//...
}
}
Fatto questo, dobbiamo preoccuparci di svolgere tre operazioni fondamentali:
- creare le istanze dei Web Server Control che abbiamo deciso di utilizzare per l'interfaccia del controllo;
- aggiungere alla collezione di controlli le istanze dei controlli figli appena creati;
- decidere come renderizzare i controlli figli;
Le prime due operazioni le svolgiamo implementando il metodo CreateChildControls() ereditato da WebControl
. Questo metodo risulta fondamentale durante la costruzione e il rendering dei controlli compositi: viene richiamato sia in fase di caricamento del controllo, sia in fase di "databind", sia a fronte di postback della pagina, in modo tale da permettere al motore di ASP.NET di (ri)generare i vari controlli figlio presenti all'interno del nostro contenitore.
Siccome si è deciso di creare un form di inserimento, all'interno del metodo CreateChildControls()
dovremo quindi creare tutte le istanze dei controlli che vogliamo inserire all'interno del nostro controllo personalizzato, quindi avremo bisogno di TextBox
, si Label
, si RequiredFieldValidator
per aggiungere funzionalità di validazione al nostro controllo e di un Button
, che permetta di inviare le informazioni inserite.
Una volta valorizzate le proprietà utili dei controlli figlio (specificare l'ID è d'obbligo), li aggiungiamo alla collezione dei controlli; tale collezione è rappresentata dalla proprietà Controls
del nostro controllo composito.
Definire i componenti di base
protected override void CreateChildControls()
{
this.Controls.Clear();
// lblTitolo
lblTitolo = new Label();
lblTitolo.ID = "lblTitolo";
lblTitolo.Style.Add(HtmlTextWriterStyle.FontSize, "large");
this.Controls.Add(lblTitolo);
// txtNome
txtNome = new TextBox();
txtNome.ID = "txtNome";
txtNome.ToolTip = "Nome";
this.Controls.Add(txtNome);
// rfvNome
rfvNome = new RequiredFieldValidator();
rfvNome.ID = "rfvNome";
rfvNome.ControlToValidate = txtNome.ID;
rfvNome.ErrorMessage = "*";
rfvNome.Display = ValidatorDisplay.Static;
rfvNome.ValidationGroup = "customerDetailValdationGroup";
this.Controls.Add(rfvNome);
// l'implementazione degli altri controlli figlio
// è omessa, ma presente nell'allegato da scaricare
// btnSalva
btnSalva = new Button();
btnSalva.ID = "btnSalva";
btnSalva.ValidationGroup = "customerDetailValdationGroup";
btnSalva.Click += new EventHandler(btnSalva_Click);
this.Controls.Add(btnSalva);
}
Ora dobbiamo decidere come questi elementi saranno visualizzati. La classe astratta prevede, per questo, che venga implementato il metodo Render()
(dall'inglese "to render", ovvero mostrare o "costruire l'aspetto visuale, e da cui il verbo italianizzato "renderizzare").
Con Render()
possiamo definire l'aspetto che avranno i singoli "controlli figlio" e il controllo in generale, applicando del semplice markup XHTML. Tutto il codice generato poi sarà inviato nel "flusso in uscita" del del controllo, e utilizzato da altri controlli, o inviato direttamente al browser (è per questo che dobbiamo definire come parametro uno stream di output, contenuto nell'HtmlTextWriter
)
Per inserire i tag HTML nel flusso ci possiamo avvalere dei metodi RenderBeginTag()
e RenderEndTag()
della classe HtmlTextWriter
, mentre per il rendering dei vari controlli bisogna utilizzare il metodo RenderControl()
, ereditato dalla classe base Control
, che inserisce il codide del controllo nella posizione scelta dello stream di output.
Definizione dell'aspetto (rendering)
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
lblTitolo.RenderControl(writer); // titolo del form di inserimento
writer.RenderBeginTag(HtmlTextWriterTag.Table); // apro <table>
// Nome
writer.RenderBeginTag(HtmlTextWriterTag.Tr); // apro <tr>
writer.RenderBeginTag(HtmlTextWriterTag.Td); // apro <td> label
writer.Write(txtNome.ToolTip);
writer.RenderEndTag(); // chiudo <td>
writer.RenderBeginTag(HtmlTextWriterTag.Td); // apro <td> controllo
txtNome.RenderControl(writer);
rfvNome.RenderControl(writer);
writer.RenderEndTag(); // chiudo <td>
writer.RenderEndTag(); // chiudo <tr>
// il rendering delle altre righe della tabella
// è stato omesso, ma presente nell'allegato da scaricare
// bottone
writer.RenderBeginTag(HtmlTextWriterTag.Tr); // apro <tr>
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Align, "right", false);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
btnSalva.RenderControl(writer);
writer.RenderEndTag();
writer.RenderEndTag(); // chiudo <tr>
writer.RenderEndTag(); // chiudo <table>
}
L'interfaccia grafica è completa. In questo caso, a titolo di esempio, abbiamo optato per un layout a tabelle, ma naturalmente si può implementare una soluzione tableless. Abbiamo 3 textbox con le relative label di descrizione, i validatori e il bottone per il salvataggio, tutti inseriti all'interno di una tabella HTML.
Per far si che il nostro controllo composito risulti completamente funzionante e facilmente ridistribuibile, abbiamo bisogno però di altri accorgimenti:
- creare delle proprietà per gestire le etichette e le informazioni inserite;
- creare un evento che può essere gestito a livello di pagina, che viene scatenato al click sul bottone inserito tra i controlli figli.
Per quanto riguarda le proprietà, a differenza di quanto visto per i controlli utente, non abbiamo più la necessità di salvare le informazioni all'interno del ViewState
della pagina, ma possiamo direttamente scrivere e leggere dalle proprietà dei controlli figlio. Per esempio, abbiamo una proprietà Nome
che preleva e scrive informazioni nella proprietà Text
del controllo txtNome
, oppure una proprietà Titolo che imposta il testo della Label
che fa da titolo al form di inserimento.
Impostare le proprietà del controllo
[Browsable(true), Bindable(true), Category("Data")]
public string Nome
{
get
{
EnsureChildControls();
return txtNome.Text;
}
set
{
EnsureChildControls();
txtNome.Text = value;
}
}
Le proprietà che esponiamo danno la possibilità, a cui usa il nostro controllo, di modificarne aspetto e comportamento, e di leggere i dati che rappresenta. Ad esempio, è possibile leggere il contenuto delle TextBox
dopo il postback.
Vale la pena soffermarci sull'inserimento del metodo EnsureChildControls
. Questo metodo verifica che i controlli figlio siano stati creati e, in caso contrario, si preoccupa di chiamare il metodo CreateChildControls()
per crearli. Inserirlo prima di manipolare i controlli figli ci assicura di non aver a che fare con oggetti nulli.
La gestione degli eventi
All'interno del metodo CreateChildControls()
, è stato definito l'handler per l'evento Click
del bottone, uno dei controlli figlio del nostro controllo composito.
btnCommand.Click += new EventHandler(btnCommand_Click);
Questo evento viene scatenato correttamente, ma sarà possibile gestirlo solo all'interno del controllo. Una volta inserito il controllo in una pagina ASP.NET, non possiamo più assegnargli un delegato. Possiamo dire che siamo andati "al di fuori del contesto" (scope) dell'evento.
Per questo motivo, abbiamo la necessità di aggiungere un evento al nostro controllo composito che possa essere gestito dalla pagina Web che lo ospita e che venga scatenato al click del bottone. Per farlo, creiamo un evento e lo aggiungiamo alla collezione di eventi del controllo (rappresentata dalla proprietà Events
). Poi creiamo il relativo gestore.
Definire un evento
private static readonly object eventKey = new object();
[Browsable(true), Category("Action")]
public event EventHandler Save
{
add
{
Events.AddHandler(eventKey, value);
}
remove
{
Events.RemoveHandler(eventKey, value);
}
}
protected virtual void OnSave(EventArgs e)
{
EventHandler handler = (EventHandler)Events[eventKey];
if (handler != null)
{
handler(this, e);
}
}
Infine, all'interno del gestore dell'evento Click
del nostro bottone, va scatenato il nostro evento custom.
protected void btnCommand_Click(object sender, EventArgs e)
{
OnSave(EventArgs.Empty);
}
Come ultimo accorgimento da prendere, possiamo impostare l'evento Save
come predefinito aggiungendo l'attributo DefaultEvent
tra quelli che già decoravano la classe.
[DefaultEvent("Save"), DefaultProperty("Caption"), ToolboxData("<{0}:CustomerDetail runat="server"> </{0}:DettagliUtente>")]
public class DettagliUtente : CompositeControl
A questo punto, lo sviluppo del nostro controllo composito può considerarsi concluso.
Utilizzo
Per inserire il nuovo controllo nelle pagine dobbiamo, come sempre, registrare il controllo. Possiamo poi modificarne le proprietà e gestirne gli eventi.
Inserire il controllo in una pagina Web
<%@ Register Namespace="HTMLit.Web.UI.WebControls" TagPrefix="HTMLit" %>
<script runat="server">
protected void SalvaDatiCliente(object sender, EventArgs e)
{
// eventuale salvataggio di dati nel database
lbl.Text = String.Format("Nome: {0}<br />" + "Cognome: {1}<br />" + "Telefono: {2}",
detail.Nome,
detail.Cognome,
detail.Telefono);
}
Il controllo risulta semplice da usare, per merito dell'incapsulamento: abbiamo racchiuso diversi comportamenti in un unico oggetto, per creare una nuova, unica, funzionalità avanzata.
Conclusioni
Lo sviluppo di controlli compositi è un campo in cui è un piacere addentrarsi, in quanto è possibile creare oggetti unici che racchiudono funzionalità e comportamenti nuovi rispetto a quelli offerti dai controlli già presenti all'interno del .NET Framework. Il tutto poi, può essere reso personalizzabile, in modo da poter distribuire il controllo ed utilizzarlo in più contesti possibili tra loro differenti. Siamo limitati solamente dalla nostra fantasia.