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

Supporto desing-time dei controlli ASP.NET

Permettere che un nostro controllo sia manipolabile in modo visuale in Visual Studio e affini
Permettere che un nostro controllo sia manipolabile in modo visuale in Visual Studio e affini
Link copiato negli appunti

Il meccanismo alla base della gestione dei controlli server in ASP.NET 2.0, è lo stesso in tutto il .NET Framework: lo ritroviamo praticamente identico anche per le Windows Form. Questo meccanismo prevede che i componenti, a partire da quelli di base, possano essere rappresentati anche in modo visuale all'interno degli ambienti di sviluppo.

Sono infatti previste funzionalità sia per la fase di runtime (che abbiamo passato in rassegna in un articolo precedente), sia per la fase di design/progettazione del controllo, fase questa che non va assolutamente trattata con leggerezza, soprattutto nel caso in cui i nostri controlli lato server siano stati pensati anche in termini di distribuzione e riutilizzo.

Quello che vogliamo ottenere è la possibilità di impostare le proprità dei nostri controlli direttamente in modo visuale, come avviene per i controlli standard forniti dal .NET Framework. Queste funzionalità di modellazione visuale sono sfruttabili poi con ambienti di sviluppo che rispondono ai requisiti di design richiesti dal .NET Framework 2.0; Visual Studio è il più famoso e il più usato, ma ce ne sono degli altri.

Ogni controllo personalizzato supporta già delle funzionalità "visuali" di base, perchè le eredita dalla classe Control. Queste funzionalità prevedono che il controllo possa essere contenuto all'interno di un designer visuale, che possa essere trascinato all'interno dell'area di progettazione dalla toolbox e che le sue proprietà siano modificabili all'interno della finestra delle proprietà.

La classe Control, offre queste caratteristiche in quanto a sua volta eredita dalla classe Component, classe base di tutti i componenti presenti all'interno del CLR. Un componente, nel .NET Framework, non è altro che una classe che implementa l'interfaccia IComponent e che abilita lo scambio e la condivisione di oggetti tra differenti applicazioni. Infine un'applicazione è in grado di fare da "host" per un componente se questa implementa l'interfaccia IContainer. Questo spiega anche la forte similitudine tra i "controlli Web" e i "controlli WinForm".

Gli attributi di progettazione

Noi sviluppatori, abbiamo a disposizione varie modalità con cui modificare il comportamento in fase di design/progettazione dei nostri controlli lato server. Il più immediato ed intuitivo è sicuramente quello di decorare, con attributi di progettazione, classi, proprietà o eventi. Tali attributi, che possono tranquillamente essere trattati come veri e propri metadati, sono poi interpretati dall'IDE. Elenchiamo di seguito gli attributi di progettazione più utilizzati.

Attributi di classe
Attributo Descrizione
DefaultProperty Definisce la proprietà di default del controllo. Tale proprietà viene evidenziata all'interno della finestra delle proprietà quando il controllo ottiene il focus all'interno del designer
DefaultEvent Definisce l'evento di default del controllo. Quando lo sviluppatore effettua un doppio click sul controllo posto all'interno della pagina, viene creato il gestore dell'evento di default
ParseChildren Attributo di tipo boolean che indica alla pagina che funge da "host" per il nostro controllo, se questo contiene o meno dei controlli annidati al suo interno
ToolBoxData Definisce il codice di markup ASP.NET che l'editor deve generare una volta che il controllo viene trascinato all'interno del designer
ToolboxBitmap Definisce l'immagine che andrà a rappresentare l'icona del controllo all'interno della ToolBox
Attributi di proprietà
Attributo Descrizione
Bindable Attributo di tipo boolean che indica se la proprietà può essere valorizzata tramite un binding di dati
Browsable Attributo di tipo boolean che indica se la proprietà può essere visualizzata all'interno della finestra delle proprietà
Category Definisce la categoria d'appertenenza della proprietà
DefaultValue Definisce un eventuale valore di default per la proprietà
Description Definisce la descrizione testuale di una proprietà. Tale descrizione viene visualizzata sia nella finestra delle proprietà di Visual Studio che nel tooltip dell'IntelliSense
Themeable Attributo di tipo boolean che indica se la proprietà può essere gestita attraverso uno skin posto all'interno di un tema

Questi attributi decorano classi, proprietà o eventi in gruppi:

[ParseChildren(true, "Text"), DefaultProperty("Url"), ToolboxData("<{0}:PopUpLink runat="server"></{0}:PopUpLink>")]
public class PopUpLink : WebControl

oppure singolarmente:

[Browsable(true)]
public string Text

In C# sono definiti all'interno di due parentesi quadre, mentre in VB.NET sono racchiusi tra i simboli minore (<) e maggiore (>), e seguiti dall'underscore (_).

<Browsable(true)> _
Public Property Text As String

Supporto avanzato per la fase di progettazione

Abbiamo visto come modificare il comportamento di un controllo in fase di progettazione in maniera del tutto dichiarativa attraverso l'utilizzo degli attributi di classi, proprietà ed eventi.

L'intera architettura di progettazione inserita all'interno del .NET Framework però, ci offre delle tecniche più avanzate per facilitare l'utilizzo dei nostri controlli a design-time. Queste tecniche prevedono lo sviluppo di classi extra, che devono essere compilate e poste all'interno dello stesso assembly in cui è presente il controllo stesso (o chiaramente, all'interno della directory App_Code della nostra applicazione Web). Tali tecniche sono principalmente tre:

  1. Utilizzo di control designers
  2. Utilizzo di editor di tipi
  3. Utilizzo di convertitori di tipi

Sembrano oggetti sconosciuti, eppure ci si accorge di averli utilizzati in moltissime occasioni con Visual Studio.

Control designers

Un "Designer" provvede a facilitare la modifica dei comportamenti dei nostri controlli tramite un'interfaccia grafica facile e comprensibile. è possibile prima di tutto definire il rendering del controllo in fase di progettazione e inoltre possiamo specificare i valori di particolari proprietà, modificare il markup HTML, scegliere un eventuale DataSource da collegare al controllo, modificare la visualizzazione dei vari template di un templated-control, abilitare l'autopostback (dove supportato) o semplicemente eseguire certi metodi direttamente dal menu contestuale del designer.

Come per qualsiasi altro campo di personalizzazione, anche per quanto riguarda i Control Designers esistono delle classi base da cui ereditare, in base al tipo di controllo che vogliamo arricchire con il supporto per la progettazione:

Classe Descrizione
ControlDesigner Classe base da cui ereditare per la creazione di un control designer
ContainerControlDesigner Designer per controlli contenitori di altri controlli (che implementano l'interfaccia INamingContainer)
CompositeControlDesigner Designer per controlli compositi
DataSourceDesigner Designer per controlli di tipo DataSource
BaseDataBoundControlDesigner Designer per controlli di tipo Data-Bound
HierarchicalDataSourceDesigner Designer per controlli con strutture ad albero

Visual Studio provvede ad utilizzare queste classi quando vengono inseriti all'interno dell'area di progettazione particolari controlli come i controlli compositi, i DataSource o i Data-Bound. Proprio per questo, senza dover inserire l'attributo:

[Designer("System.Web.UI.Design.WebControls.CompositeControlDesigner")]
public class DettagliCliente : CompositeControl

l'IDE ci presenta già il rendering del controllo a design-time.

Per quanto riguarda invece i controlli utente, siamo noi a doverci far carico della visualizzazione in fase di progettazione sviluppando un designer personalizzato ereditando dalla classe ControlDesigner. Tale classe ci permette sia di modificare il rendering del controllo attraverso markup HTML, sia di popolare la lista di azioni legate al controllo stesso.

Sperimentiamo in pratica questo approccio, creando il supporto per il design-time del controllo per la creazione di link con finestre pop-up. Controllo che abbiamo utilizzato come esempio nell'articolo sui controlli server di ASP.NET 2.0;. Per prima cosa, quindi, creiamo la classe che eredita da ControlDesigner.

public class PopUpLinkDesigner : ControlDesigner

Per quanto riguarda l'aspetto grafico in fase di progettazione, dobbiamo sovrascrivere quattro metodi principali:

Metodo Descrizione
Initialize() utilizzato per prendere il riferimento al componente cui il designer è collegato
GetDesignTimeHtml() utilizzato per ritornare il markup HTML che rappresenterà il controllo a design-time
GetEmptyDesignTimeHtml() utilizzato per ritornare del markup HTML nel caso in cui il metodo GetDesignTimeHtml ritorni una stringa vuota
GetErrorDesignTimeHtml() utilizzato per stampare a video il markup HTML nel caso in cui il controllo scateni degli errori in fase di rendering

Nel nostro caso, non dobbiamo far altro che renderizzare un link HTML (costruendo un elemento <a>).

private PopUpLink control = null;

public override void Initialize(IComponent component)
{
  control = (PopUpLink)component;
  base.Initialize(component);
}

public override string GetDesignTimeHtml()
{
  try
  {
    if (control.Text == string.Empty || control.Url == string.Empty)
      return GetEmptyDesignTimeHtml();
    else
      return String.Format("<a href="{0}">{1}</a>", control.Url, control.Text);
    }
  catch (Exception ex)
  {
    return GetErrorDesignTimeHtml(ex);
  }
}

protected override string GetEmptyDesignTimeHtml()
{
  return "Url and Text properties not set";
}

protected override string GetErrorDesignTimeHtml(Exception e)
{
  return CreatePlaceHolderDesignTimeHtml(e.Message);
}

Solo implementando questi quattro metodi, abbiamo aggiunto funzionalità al design-mode del nostro controllo, verifichiamole importando il controllo nella modalità visuale dell'IDE.

Figura 1. Il controllo in modalità visuale
Il controllo in modalità visuale

Per applicare questo Designer al nostro controllo, dobbiamo decorare il controllo di partenza, con l'attributo Designer, in modo tale da poter specificare la classe (e il relativo namespace) che si occupa del rendering del controllo in fase di progettazione:

Applicare il Designer al controllo

[Designer("Peppe.Web.UI.WebControls.Designers.PopUpLinkDesigner")]
public class PopUpLink : WebControl

Un'altra funzionalità molto interessante di cui dotare il nostro controllo in modalità visuale è la "lista di azioni". Le azioni sono un elenco di possibili operazioni sul controllo che appaiono in un menu contestuale posto a fianco al controllo stesso, e permettono di attuare modifiche rapide senza dover aprire finestre aggiuntive.

La maggior parte dei controlli Web è dotata di questa lista di azioni, ognuna chiaramente con le operazioni specifiche per il relativo controllo. Per aggiungere questa funzionalità al nostro designer dobbiamo:

  1. sovrascrivere la proprietà ActionLists;
  2. creare una nuova classe, che erediti da DesignerActionList, utile a definire l'aspetto e le funzionalità del menu contestuale proprio del nostro designer.

Riscrivendo la proprietà ActionLists dobbiamo preoccuparci di creare un oggetto di tipo DesignerActionListCollection da ritornare a qualsiasi richiesta GET effettuata alla proprietà stessa.

public override DesignerActionListCollection ActionLists
{
  get
  {
    DesignerActionListCollection list = new DesignerActionListCollection();
    list.Add(new PopUpLinkActionList(control));
    return list;
  }
}

Questa collezione ha il compito di contenere la lista delle azioni personalizzate che abbiamo deciso di inserire nel menu contestuale.

Per permettere alle nostre azioni custom di modificare il comportamento del controllo dobbiamo ancora creare la classe che faccia da intermediario tra il designer e il controllo. Questa classe dovrà derivare dalla classe DesignerActionList e implementare un costruttore che prenda come parametro il componente di cui vogliamo modificare proprietà e aspetto.

Per ogni proprietà che abbiamo intenzione di modificare dal menu contestuale, dobbiamo creare una rispettiva proprietà "proxy", che faccia cioè da tramite tra il menu e il controllo.

public class PopUpLinkActionList : DesignerActionList
{
  public PopUpLinkActionList(IComponent component) : base(component)
  {
    AutoShow = true;
  }

  public string Text
  {
    get { return ((PopUpLink)this.Component).Text; }
    set
    {
      PropertyDescriptor property = TypeDescriptor.GetProperties(PopUpLinkControl)["Text"];
      property.SetValue(PopUpLinkControl, value);
    }
  }

  public string Url
  {
    get { return ((PopUpLink)this.Component).Url; }
    set
    {
      PropertyDescriptor property = TypeDescriptor.GetProperties(PopUpLinkControl)["Url"];
      property.SetValue(PopUpLinkControl, value);
    }
  }

  //...
}

Infine sovrascriviamo il metodo GetSortedActionItems, per popolare la lista di azioni personalizzate ed abbellire il menu contestuale con le relative descrizioni.

public override DesignerActionItemCollection GetSortedActionItems()
{
  DesignerActionItemCollection items = new DesignerActionItemCollection();

  items.Add(new DesignerActionHeaderItem("Link appearance"));
  items.Add(new DesignerActionTextItem("Cambia l'aspetto del controllo", "Link appearance"));
  items.Add(new DesignerActionPropertyItem("Text", "Text"));
  items.Add(new DesignerActionPropertyItem("Url", "Url"));
  return items;
}

Il risultato finale del nostro designer risulta ora veramente completo.

Figura 2. Lista delle azioni personalizzata
Lista delle azioni personalizzata

Convertitori di tipo

Questa è veramente una finezza. I convertitori di tipo sono delle particolari classi che supportano la finestra delle Proprietà a fronte di tipi di dati personalizzati. Tale finestra infatti, rappresenta correttamente all'utente finale tutte le proprietà che hanno tipo di dato primitivo (string, int, etc.). Senza un convertitore di tipo infatti, la finestra delle proprietà, a fronte di un tipo custom, si limita semplicemente a stampare la stringa contente il namespace e la classe di tale tipo, eliminando quindi ogni possibile interazione dell'utente sulla proprietà tramite interfaccia grafica.

Per fare un esempio pratico, un enum, che non è un tipo di dato primitivo, ha il suo convertitore (definito dalla classe EnumConverter) per essere rappresentata all'interno della finestra delle proprietà.

Pensando quindi, ad una distribuzione del nostro controllo per un ampio pubblico di sviluppatori, è sempre bene creare un convertitore per ogni tipo di dato personalizzato che abbiamo utilizzato all'interno del nostro controllo.

Veniamo ora al controllo di partenza, il controllo per il rendering di link a finestre pop-up, che come prima implementazione vedeva sue le proprietà PopUpWidth, PopUpHeight e Resizable. Proviamo, a puro titolo d'esempio, a creare un nuovo tipo di dato per rappresentare tutte queste tre informazioni in un'unica proprietà; questo nuovo tipo, sarà rappresentato dalla classe PopUp e definirà una nuova proprietà all'interno del nostro controllo web che sostituirà le tre suddette.

[TypeConverter(typeof(PopUpConverter))]
public class PopUp
{

  private bool resizable;
  private Unit width;
  private Unit height;

  public PopUp()
  {
    resizable = false;
    width = new Unit("700px");
    height = new Unit("500px");
  }

  public PopUp(Unit width, Unit height, bool resizable)
  {
    this.resizable = resizable;
    this.width = width;
    this.height = height;
  }

  [NotifyParentProperty(true)]
  public Unit Height
  {
    get { return height; }
    set { height = value; }
  }

  [NotifyParentProperty(true)]
  public Unit Width
  {
    get { return width; }
    set { width = value; }
  }

  [NotifyParentProperty(true)]
  public bool Resizable
  {
    get { return resizable; }
    set { resizable = value; }
  }
}

[Designer("Peppe.Web.UI.WebControls.Designers.PopUpLinkDesigner")]
public class PopUpLink : WebControl
{
  [Category("Appearance")]
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
  public PopUp PopUp
  {
    get
    {
      if (popup == null)
      popup = new PopUp();
      return popup;
    }
  }

  // ...
}

Come si vede, la proprietà PopUp è decorata dall'attributo DesignerSerializationVisibility, impostato al valore Content; quell'attributo è utilizzato per specificare in che modalità, tale proprietà deve essere persistita nel codice. Con il valore Content, abbiamo specificato che la proprietà va serializzata completamente all'interno del markup del nostro controllo.

La classe PopUp invece, contiene tutte le proprietà che abbiamo appena descritto, decorate dall'attributo NotifyParentProperty impostato a true. Tale attributo (definito dalla classe NotifyParentPropertyAttribute) indica che la proprietà padre deve essere avvisata ad ogni cambiamento della proprietà figlia. Mente l'attributo di classe TypeConverter indica il convertitore di tipo associato al tipo PopUp.

I convertitori di tipo, andando sullo specifico, sono delle classe cha derivano dalla classe TypeConverter del namespace System.ComponentModel. Il .NET Framework offre già un'implementazione di un livello superiore, definita dalla classe ExpandableObjectConverter, che raffigura il tipo all'interno della finestra delle proprietà nella modalità espandibile, presentando anche tutte le sotto-proprietà del tipo in questione.

Per creare il convertitore per il nostro tipo personalizzato quindi, dobbiamo creare una classe che derivi da ExpandableObjectConverter ed effettuare l'override di quattro metodi:

Metodi da implementare
Metodo Descrizione
CanConvertFrom Determina se un'istanza della classe PopUp (il nostro tipo da convertire) può essere istanziata a partire da un particolare tipo primitivo (nel nostro caso una semplice stringa)
CanConvertTo Determina se un'istanza della classe PopUp può essere convertita in un tipo primitivo
ConvertFrom Crea una nuova istanza della classe PopUp, valorizzando le varie sotto-proprietà con le informazioni prelevate dal parsing del tipo primitivo consentito
ConvertTo Crea una nuova istanza del tipo primitivo consentito a partire dai valori delle sotto-proprietà dell'istanza della classe PopUp

L'implementazione di questi quattro metodi risulta molto semplice e permette di raggiungere un risultato veramente d'impatto.

public class PopUpConverter : ExpandableObjectConverter
{
  public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  {
    if (sourceType == typeof(string))
      return true;
    return base.CanConvertFrom(context, sourceType);
  }

  public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  {
    if (destinationType == typeof(string))
      return true;
    return base.CanConvertTo(context, destinationType);
  }

  public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  {
    if (value == null) return new PopUp();

    if(value.GetType().Equals(typeof(string)))
    {
      string valore = (string)value;
      if(String.IsNullOrEmpty(valore)) return new PopUp();

      string[] valori = valore.Split(' ');
      if (valori.Length != 3)
        throw new ArgumentException("La proprietà deve avere 3 sotto-valori", "value");

      return new PopUp(new Unit(valori[0]), new Unit(valori[1]), Convert.ToBoolean(valori[2]));
    }
    return base.ConvertFrom(context, culture, value);
  }

  public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
  {
    if (value != null)
    {
      if (!value.GetType().Equals(typeof(PopUp)))
        throw new ArgumentException("La proprietà PopUp non è valida", "value");
    }

    if (destinationType == typeof(string))
    {
      if (value == null) return String.Empty;
  
      PopUp popup = (PopUp)value;
      return String.Format("{0} {1} {2}", popup.Width, popup.Height, popup.Resizable);
    }
    return base.ConvertTo(context, culture, value, destinationType);
  }
}

Ora, la proprietà PopUp, viene visualizzata all'interno della finestra delle proprietà come in figura, e permette allo sviluppatore la modifica visuale di tutte le sue sotto-proprietà.

Inoltre, a livello di markup ASP.NET, tali sotto-proprietà diventano raggiungibili mediante la sintassi <ParentProperty>-<ChildProperty>:

<peppe:PopUpLink ID="PopUpLinkID" runat="server"
      Text="open pop-up" Url="www.peppedotnet.it"
      PopUp-Height="500px" PopUp-Width="700px" PopUp-Resizable="False" />

Editor di tipi

Gli editor di tipi sono delle ulteriori finestre che sono create sempre a supporto della finestra delle proprietà, poiché la maggior parte dei tipi personalizzati hanno bisogno di differenti strutture di input per l'inserimento delle informazioni, in quanto semplici caselle di testo o menu a tendina spesso non possono bastare. Un editor di tipo che avrete sicuramente utilizzato molto spesso è l'editor per la creazione del collegamento tra un controllo di tipo Data-Bound e un DataSource; in un'operazione del genere è infatti necessario che l'interfaccia grafica sia completa sia per la scelta della fonte di dati che per la scelta dei singoli campi di binding.

Nota: per la creazione di un editor di tipo custom, bisogna avere una minima conoscenza della classi utilizzate per la creazione di Windows Form, il che esula da tutto il mondo legato ad ASP.NET; nonostante ciò daremo comunque la basi per la costruzione di questo tipo di personalizzazioni.

Come esempio, proviamo a creare un editor per la proprietà Url del nostro controllo PopUpLink. L'editor dovrà semplicemente far visualizzare, in una finestra modale, l'indirizzo della pagina che si intende inserire nel controllo, attraverso un controllo WebBrowser (controllo proprio del namespace System.Windows.Form).

Ogni editor di tipo, eredita dalla classe UITypeEditor. Tale classe obbliga l'override di due metodi:

Metodi da implementare
Metodo Descrizione
EditValue Utilizzato per visualizzare la form di supporto alla proprietà e per la modifica dei valori legati al controllo padre
GetEditStyle Utilizzato per definire lo stile da applicare alla Windows Form di supporto

Questo il codice del nostro editor personalizzato (lo sviluppo della Windows Form verrà saltato in quanto non rientra nell'argomento dell'articolo; il codice è comunque presente nel file da scaricare):

public class PopUpEditor : UITypeEditor
{
  private IWindowsFormsEditorService service = null;

  public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
  {
    if (context != null && provider != null)
    {
      service = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
      if(service != null && context.Instance != null)
      {
        // prelevo l'istanza del controllo di riferimento
        PopUpLink control = (PopUpLink)context.Instance;

        // apro la Windows Form
        service.ShowDialog(new PopUpFormEditor(control));
      }
    }
    return value;
  }

  public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
  {
    if(context != null)
    {
      if (context.Instance != null) return UITypeEditorEditStyle.Modal;
    }
    return base.GetEditStyle(context);
  }
}

Una volta creato l'editor e la Windows Form associata, non dobbiamo far altro che legarlo alla proprietà che abbiamo intenzione di modificare attraverso questa finestra di supporto.

[Browsable(true), Category("Appearance"), Description("Link url value.")]
[Editor(typeof(PopUpEditor), typeof(UITypeEditor))]
public string Url
{
  get
  {
    object o = ViewState["Url"];
    return (o == null) ? String.Empty : (string)o;
  }
  set { ViewState["Url"] = value; }
}

Il riusltato è d'avvero di forte impatto.

Conclusioni

Se abbiamo intenzione di distribuire le nostre librerie di controlli web, non è assolutamente pensabile non passare per la personalizzazione del supporto di questi ultimi per quanto riguarda la fase di desgin/progettazione. Un primo aiuto lo danno già gli attributi di progettazione, che ci danno la possibilità, in maniera del tutto dichiarativa, di modificare il comportamento dei nostri controlli in tale fase.

Abbiamo visto anche però, che per rendere i controlli il più malleabili possibile è necessario sviluppare un supporto visuale più complesso, avvalendosi di classi extra per la creazione di control designers, editor di tipi e convertitori di tipi, tutti strumenti che aiutano lo sviluppatore a modificare velocemente e con facilità il comportamento dei nostri controlli web.

Ti consigliamo anche