Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Creare un controllo TagCloud sfruttando l'eredità di CompositeControl

Creare un nuovo controllo che ci permetta di navigare i tag nei nostri siti
Creare un nuovo controllo che ci permetta di navigare i tag nei nostri siti
Link copiato negli appunti

Con l'avvento del Web 2.0 e del Web sociale i tag, letteralmente "etichette", sono diventati via via sempre più importanti per l'identificazione e l'organizzazione dei contenuti. Uno dei sistemi per visualizzare in modo ordinato una lista di tag consiste nella "tag cloud", una distribuzione a "nuvola" in cui le dimensioni di ogni elemento variano a seconda della quantità di contenuti catalogati.

In questo articolo vedremo come creare un controllo ASP.NET per visualizzare una tag cloud. Per rendere il controllo utilizzabile nel maggior numero di situazioni possibili verrà implementato anche il supporto ai template e a tutti i diversi tipi di fonte di dati (datasource), siano questi database o oggetti.

Per quanto riguarda il codice HTML ci atterremo ad un definizione semantica della nuvola attraverso l'utilizzo di una lista non ordinata e dei CSS, secondo il markup presentato in un articolo di Alessandro Fulciniti.

Cenni sulla struttura dei controlli

Prima di procedere con la realizzazione pratica, è bene fare un ripasso sull'architettura dei controlli in ASP.NET 2.0. Il Framework .Net ci mette a disposizione diverse classi (tutte reperibili nel namespace System.Web.UI.WebControls) da cui ereditare per creare diversi tipi di controlli:

  • Control: è la classe di base. In generale non avremo bisogno di derivare direttamente da questa classe, fatta eccezione per alcuni casi particolari
  • WebControl: è una delle classi più utilizzate, alla base di molti controlli come Label o Panel. Non fornisce supporto alle fonti di dati
  • DataBoundControl: classe astratta, utilizzata come base per tutti quei controlli che necessitano di accedere ad un datasource. Non supporta i template
  • CompositeControl: classe astratta, fornisce supporto ai template ma non ai datasource
  • CompositeDataBoundControl: classe astratta che implementa tutte le caratteristiche delle due precedenti. Sicuramente una delle più versatili, è alla base di controlli come GridView e FormView. La utilizzeremo nella realizzazione della tag cloud
  • HierarchicalDataBoundControl: classe molto particolare che permette la gestione di fonti di dati con struttura ad albero. Molto comoda per i menu, praticamente inutile per altri scopi

Occupiamoci adesso delle funzioni principali di CompositeDataBoundControl, e del loro ruolo all'interno del ciclo di vita di un controllo ASP.NET. Come già detto, CompositeDataBoundControl verrà utilizzato come base per il nostro controllo personalizzato. Molte delle considerazioni fornite di seguito possono tuttavia essere applicate anche alle altre classi.

Cominciamo dalla funzione CreateChildControl(), una delle più importanti, dato che ha il compito di costruire la struttura interna del controllo: i template vengono inizializzati all'interno dei loro contenitori e tutti i controlli figli vengono creati (ma non renderizzati, più informazioni su questa differenza di seguito) ed aggiunti alla collection Controls. Viene inoltre effettuato il databind dei dati ad ogni elemento interno.

È bene distinguere subito tra la fase di creazione dei controlli e quella di rendering: al contrario di quanto comunemente si è portati a pensare queste due fasi sono distinte tra loro ed avvengono in due diversi momenti di vita del controllo.

Il rendering del controllo avviene quando la pagina ha completato l'inizializzazione ed esegue un ciclo sui controlli figlio, richiamando per ognuno il metodo Render(), proprio della classe Control.

Questo metodo ne richiama a sua volta tre distinti: RenderBeginTag, RenderContents e RenderEndTag. Il primo e l'ultimo creano un contenitore per il controllo, di default un tag HTML span. Più interessante è il secondo che si occupa di generare il codice HTML per il contenuto del controllo. Nella costruzione del nostro controllo non avremo bisogno di sovrascrive questo metodo, lasciando fare tutto il lavoro ad ASP.NET.

TagCloud: struttura

Come già anticipato, il nostro controllo offrirà supporto ai template ed alla definizione dichiarativa dei tag, secondo lo schema riassunto in figura.

Figura 1. Schema dei template
Schema dei template

I template di cui avremo bisogno sono quindi tre: <HeaderTemplate> per l'intestazione (utile ad esempio per un titolo), <FooterTemplate> per il piede ed <EmptyDataTemplate> per visualizzare un messaggio nel caso in cui datasource sia vuoto.

La collection Items rappresenta invece la lista dei tag da visualizzare, che può essere riempita, come succede ad esempio con il controllo DropDownList, in modo dichiarativo, da codice e attraverso un datasource.

Per i template e le proprietà ho scelto di utilizzare nomi in inglese per mantenere maggiore affinità con gli altri controlli di ASP.NET che offrono funzionalità simili.

Le proprietà che verranno implementate nel controllo sono le seguenti:

  • AppendDataBoundItems: simile all'omonima proprietà di DropDownList. A seconda del suo valore sovrascriveremo o meno la lista dei tag dopo un databind
  • DataTextField: il nome del campo del datasource da utilizzare per il nome dei tag
  • DataTextFormatString: stringa di formattazione del nome dei tag
  • DataSizeField: nome del campo del datasource che contiene la "dimensione" del tag, ovvero il numero dei contenuti contrassegnati con il tag. Da questo valore dipenderà la dimensione visiva del tag

TagCloud: template e proprietà

Iniziamo la scrittura del controllo creando una nuova classe TagCloud (l'esempio allegato può servire come riferimento immediato e completo al codice che analizzeremo):

Decorazione della classe TagCloud

[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal),
AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal),
ToolboxData("<{0}:TagCloud runat="server" />"),
Description("Displays a tag cloud.")]
public sealed class TagCloud : CompositeDataBoundControl 
{ 
  ... 
}

Soffermiamoci un attimo sugli attributi assegnati alla classe, in particolare su AspNetHostingPermission che indica il livello di sicurezza richiesto dal codice del controllo per funzionare: assegnando come livello AspNetHostingPermissionLevel.Minimal specifichiamo che il codice non fa uso di funzionalità che hanno bisogno di trust più elevato (come, ad esempio, la Reflection). In questo modo il controllo potrà essere utilizzato con qualsiasi livello di protezione.

I rimanenti attributi ToolboxData e Description rappresentano, rispettivamente, il codice che viene inserito nella pagina quando trasciniamo il controllo dalla barra degli strumenti di Visual Studio (o Visual Web Developer Express) e la descrizione del controllo.

Occupiamoci adesso dei template. Per implementare questa funzionalità avremo bisogno di definire un campo ed una proprietà di tipo ITemplate per ciascun template che vogliamo utilizzare:

private ITemplate headerTemplate;

[Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty),
DefaultValue(typeof(ITemplate), ""),
Description("Header Template"),
TemplateContainer(typeof(TagCloudTemplateContainer))]
public ITemplate HeaderTemplate
{
  ... 
}

Anche in questo caso soffermiamoci sugli attributi:

  • Browsable con argomento false indica che la proprietà del controllo non sarà visibile nella finestra proprietà di Visual Studio;
  • PersistenceMode con valore PersistenceMode.InnerProperty riguarda invece il modo in cui la proprietà sarà visualizzata nel server tag, ovvero come proprietà interna e non come attributo del tag;
  • DefaultValue indica il valore di default del template, in questo caso vuoto;
  • Description è lo stesso attributo di descrizione visto per la definizione della classe;
  • infine, TemplateContainer indica il tipo di contenitore in cui il template dovrà essere inizializzato.

Un template non è un controllo (abbiamo usato l'interfaccia ITemplate e non una classe derivata da Control) e, come avremo modo di vedere parlando della funzione CreateChildControls, i template hanno bisogno di essere instanziati all'interno di un altro controllo (nel nostro caso una classe creata ad-hoc, TagCloudTemplateContainer) per poter essere visualizzati nella pagina.

La classe TagCloudTemplateContainer è molto semplice:

public sealed class TagCloudTemplateContainer : Control, INamingContainer
{
  ...
}

Implementando l'interfaccia INamingContainer ci assicuriamo che gli ID dei controlli contenuti all'interno del template non interferiscano con quelli di altri controlli della pagina, creando uno spazio a se stante.

Per quanto riguarda la lista dei tag, abbiamo bisogno anche in questo caso di un campo e di una proprietà:

private Collection<TagCloudItem> items = new Collection<TagCloudItem>();

[PersistenceMode(PersistenceMode.InnerProperty)]
public Collection<TagCloudItem> Items { ... }

La lista degli elementi è dichiarata come una Collection generica di oggetti TagCloudItem, definiti come segue:

public sealed class TagCloudItem
{
  private string text;
  private int size;

  public string Text { ... }
  public int Size { ... }
}

Anche nel caso dei tag la proprietà non sarà visualizzata come attributo del server tag ma internamente. Infine, le altre proprietà sono memorizzate nel ViewState del controllo e non necessitano quindi di campi privati:

public bool AppendDataBoundItems
{
  get
  {
    object o = ViewState["AppendDataBoundItems"];
    return (o != null) ? (bool)o : false;
  }
  set { ViewState["AppendDataBoundItems"] = value; }
}

TagCloud: le funzioni

Eccoci finalmente giunti alla realizzazione della logica interna del controllo. Per cominciare effettuiamo l'override della funzione PerformDataBinding, che si occupa di prelevare i dati dal datasource per inserirli nella collection Items:

protected override void PerformDataBinding(IEnumerable dataSource)
{
  if (dataSource != null)
  {
    if (!AppendDataBoundItems) this.Items.Clear();
    
    // Per migliorare le prestazioni copiamo i valori dei campi
    // in modo da non dover scorrere continuamente la collection
    // del ViewState
    string dataTextField = this.DataTextField;
    string dataTextFormatString = this.DataTextFormatString;
    string dataSizeField = this.DataSizeField;

    foreach (object data in dataSource)
    {
      TagCloudItem item = new TagCloudItem();

      if (!string.IsNullOrEmpty(dataTextField))
      {
        item.Text = DataBinder.Eval(data, dataTextField, dataTextFormatString);
      }

      if (!string.IsNullOrEmpty(dataSizeField))
      {
        item.Size = Convert.ToInt32(DataBinder.GetPropertyValue(data, dataSizeField));
      }

      this.Items.Add(item);
    }
  }
  base.PerformDataBinding(dataSource);
}

Il codice è semplice: per prima cosa svuotiamo la lista degli elementi soltanto se AppendDataBoundItems è uguale a false; poi con un ciclo foreach creiamo un oggetto TagCloudItem per ogni elemento presente nel datasource, utilizzando i valori delle proprietà DataTextField e DataSizeField per estrarre i dati richiesti.

Passiamo adesso alla creazione dei controlli figlio. Come già accennato il cuore di questa operazione è la funzione CreateChildControls, propria di CompositeDataBoundControl e che andiamo quindi a sovrascrivere:

protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
  int num = 0;

  // Ci assicuriamo che la lista dei controlli sia vuota
  this.Controls.Clear();

  // Creiamo i nuovi controlli
  // La funzione CreateControlHierarchy serve soltanto per mantenere
  // il codice più leggibile
  num = this.CreateControlHierarchy(dataSource, dataBinding);

  // Assegnamo a ChildControlsCreated il valore true,
  // in modo da evitare che la lista dei controlli sia costruita di nuovo
  ChildControlsCreated = true;

  // Puliamo il viewstate dei controlli
  base.ClearChildViewState();

  return num;
}

Per mantenere il codice più pulito è preferibile spezzare il codice, inserendo la creazione dei controlli in un'altra funzione, CreateControlHierarchy. Si può fare riferimento ai commenti all'interno del codice per maggiori informazioni sulle altre istruzioni.

Il codice di CreateControlHierarchy è molto complesso e per motivi di spazio non sarà possibile riportarlo per intero (la versione completa è disponibile nell'allegato). Vediamone i passaggi principali:

private int CreateControlHierarchy(IEnumerable dataSource, bool dataBinding)
{
  // Numero dei controlli creati, verranno conteggiati 
  // solo i tag
  int num = 0;

  // Header
  if (this.headerTemplate != null)
  {
    // È presente un template per l'header
    // Creiamo il contenitore
    TagCloudTemplateContainer header = new TagCloudTemplateContainer();
    
    // Aggiungiamo il contenitore alla lista dei controlli
    this.Controls.Add(header);

    // Inseriamo il contenuto del template nel contenitore
    headerTemplate.InstantiateIn(header);
  }
  
  
  // Contenuto
  // Se stiamo eseguendo il databind e se la lista 
  // degli elementi non è vuota
  if (dataBinding && items.Count > 0)
  {
    // Contenitore
    HtmlGenericControl ul = new HtmlGenericControl("ul");
    this.Controls.Add(ul);

    // Aggiungiamo gli elementi alla lista dei controlli
    foreach (TagCloudItem item in items)
    {
      HtmlGenericControl li = new HtmlGenericControl("li");
      ul.Controls.Add(li);
      
      // Testo
      li.InnerText = item.Text;

      // Dimensione del testo
      if (item.Size > 100)
        li.Attributes["class"] = "size-6";
      else if ...
    }
  }
  else
  {
    // Il datasource è vuoto e non ci sono elementi dichiarati
    // nel codice: uso l'EmptyDataTemplate se presente
    // Il procedimento è identico a quello utilizzato per l'header
    ...
  }
  ...
}

Iniziamo con l'instanziare il template per l'intestazione nel caso in cui questo sia diverso da null (in altri parole, nel caso in cui il template sia stato specificato). Creiamo quindi un nuovo oggetto TagCloudTemplateContainer, lo aggiungiamo alla lista dei controlli e tramite la funzione InstantiateIn, instanziamo il template. Lo stesso sistema verrà utilizzato al termine della funzione anche per il footer.

A questo punto visualizziamo gli elementi nel caso in cui la collection Items, riempita precedentemente da PerformDataBinding, non sia vuota, visualizzando alternativamente il template EmptyDataTemplate. Creiamo quindi la lista non ordinata (<ul>) e per ogni elemento nella collection (foreach) aggiungiamo un elemento <li>.

Il controllo è pronto! Non resta che creare una pagina di prova registrando il namespace che contiene il controllo e collegando quest'ultimo ad un qualsiasi datasource.

<%@ Register TagPrefix="custom" Namespace="Controls" %>

<custom:TagCloud ID="TagCloud1" runat="server" DataTextField="Name" DataSizeField="PostCount"
        CssClass="tag-cloud" DataSourceID="SqlDataSource1" AppendDataBoundItems="true">
  <HeaderTemplate>
    <h1>Tags</h1>
  </HeaderTemplate>
  <EmptyDataTemplate>
    Nessun tag presente!
  </EmptyDataTemplate>
  <Items>
    <custom:TagCloudItem Text="Aggiunto da codice!" Size="6" />
  </Items>
</custom:TagCloud>

<asp:SqlDataSource ID="SqlDataSource1" runat="server" SelectCommand="SELECT * FROM Tags" ConnectionString="<%$ ConnectionStrings: esempio %>" />

Per mostrare le potenzialità del controllo appena realizzato, nell'esempio allegato all'articolo è compreso un piccolo database e una pagina di esempio con un SqlDataSource ed un ObjectDataSource, insieme alle classi necessarie al funzionamento di quest'ultimo.

Figura 2. Risultato finale
Risultato finale

Conclusioni

Dopo una rapida introduzione alla struttura dei controlli di ASP.NET siamo passati a realizzare un controllo personalizzato basato su CompositeDataBoundControl, implementando il supporto ai template e a un datasource generico. Per semplicità alcune funzionalità (ad esempio i link sui tag visualizzato) non sono state incluse nel controllo finale.


Ti consigliamo anche