Quando inizi a lavorare con WPF, ti capita fin da subito di dover avere a che fare con le DependencyProperties. Sono molto simili alle proprietà che normalmente utilizziamo sotto .Net ma un po’ più complesse e potenti.

La differenza principale è che il valore di una proprietà è direttamente leggibile da un membro privato della classe o dalla classe stessa che la implementa, mentre il valore di una DependencyProperty viene risolto dinamicamente quando viene chiamato il metodo GetValue che viene ereditato da DependencyObject.

Quando il valore di una DependencyProperty viene settato, questo non viene memorizzato subito nell’oggetto contenitore ma in un dizionario di chiavi e valori che viene fornito da DependencyObject. La chiave corrisponde proprio al nome della proprietà ed il valore corrisponde al contenuto di questa.

I vantaggi che si possono ottenere utilizzando le dependency properties sono molti:

  • Si eliminano Memory Footprint: se pensiamo che nella maggior parte dei casi il valore delle proprietà dei nostri oggetti rimane invariato per tutta l’esecuzione del programma aumentando in ogni caso l’utilizzo di memoria per l’applicazione, le dependency properties risolvono questo problema memorizzando solo le proprietà che vengono modificate. I valori di default vengono memorizzati una sola volta all’interno delle proprietà di dipendenza.
  • Ereditarietà dei valori: quando si accede ad una proprietà, questa viene letta utilizzando una strategia di risoluzione secondo la quale se si verifica la situazione in cui nessun valore è impostato, la proprietà di dipendenza continua a cercare seguendo uno schema ad albero fin quando non trova un valore. Se per esempio in un controllo imposto la proprietà FontWeight, questa viene impostata anche per tutti gli elementi figlio eccetto quelli per cui non predisponiamo personalmente un comportamento diverso.
  • Meccanismo di notifica: le dependency properties hanno un meccanismo automatico di notifica. In pratica è possibile impostare una dependency property in modo tale da poter ricevere una notifica nel caso in cui questa cambi.
Strategie di risoluzione dei valori

Ogni volta che accediamo ad una dependency property, internamente si scatena un meccanismo automatico in grado di risolvere il valore della proprietà, questo meccanismo segue uno schema logico secondo cui prova a cercare un certo tipo di valore fino ad arrivare ad un Default, lo schema di seguito chiarisce meglio il concetto:

ResolvePropertyStrategy

Ogni controllo registra un set di DependencyProperty nella classe statica. Ognuna di queste consiste in una chiave che deve essere univoca per tipo oltre ad una serie di metadati che contengono le chiamate di callbacks ed i valori di Default. Tutti i tipi che vogliono utilizzare le DependencyProperties devono derivare da DependencyObject. Questa classe base definisce un dizionario di chiavi – valore che contengono le proprietà della classe che eredita. Una chiave in ingresso è una chiave specificata con una DependencyProperty.

Quando viene effettuata una chiamata ad una DependencyProperty, .Net esegue dietro le quinte una chiamata GetValue(DependencyProperty) per accedere al valore. Questo metodo utilizza una strategia di risoluzione che segue a grandi linee lo schema esposto sopra. Se un valore locale è presente, viene letto questo direttamente dal dizionario, al contrario, se non è impostato, viene effettuata una ricerca ad albero in tutte i controlli ereditati. Se nessun valore viene trovato, viene impostato il valore di Default descritto nei metadati.

Questo schema può chiarire meglio l’idea di ciò che accade:

image

Come si crea una DependencyProperty

Per creare una DependencyProperty è necessario aggiungere un campo statico di tipo DependencyProperty sul tipo di riferimento e successivamente chiamare DependencyProperty.Register() per creare un istanza dell’oggetto. Il nome della DependencyProperty deve avere come suffisso Property, è una convenzione di WPF. Per renderlo accessibile come una normale proprietà .Net è necessario creare un Property Wrapper. Questo non fa altro che settare e prelevare internamente il valore della proprietà utilizzando rispettivamente GetValue() e SetValue() dal DependencyObject passando la DependencyProperty come chiave.

// Dependency Property
public static readonly DependencyProperty CurrentTimeProperty = 
     DependencyProperty.Register( "CurrentTime", typeof(DateTime),
     typeof(MyClockControl), new FrameworkPropertyMetadata(DateTime.Now));
 
// .NET Property wrapper
public DateTime CurrentTime
{
    get { return (DateTime)GetValue(CurrentTimeProperty); }
    set { SetValue(CurrentTimeProperty, value); }
}

Ogni DependencyProperty permette di impostare un evento di callback per notifiche di cambiamento, validazione etc. Queste chiamate vengono registrate come viene mostrato di seguito:

new FrameworkPropertyMetadata( DateTime.Now, 
                       OnCurrentTimePropertyChanged, 
                       OnCoerceCurrentTimeProperty ),
                       OnValidateCurrentTimeProperty );

Nella chiamata di callback si verifica se il valore è valido, se questa chiamata restituisce false viene sollevata un eccezione.

private static bool OnValidateTimeProperty(object data)
{
    return data is DateTime;
}
Chiamate di Callback

La chiamata di callback è un metodo statico che viene chiamato ogni qual volta il valore della proprietà cambia. Il nuovo valore viene passato tramite EventArgs, l’oggetto di cui questo valore è cambiato, viene passato come source:

private static void OnCurrentTimePropertyChanged(DependencyObject source, 
        DependencyPropertyChangedEventArgs e)
{
    MyClockControl control = source as MyClockControl;
    DateTime time = (DateTime)e.NewValue;
    // Put some update logic here...
}
Readonly DependencyProperties

Alcune dependency property in WPF sono in sola lettura. Sono utilizzate spesso per riportare lo stato di un controllo come IsMouseOver, si può dedurre che in questi casi non ha alcun senso impostare un setter.

Ma la domanda principale, che molte volte, durante lo studio di questo argomento, mi sono posto è: perché utilizzare dependency properties al posto delle normali proprietà che è possibile utilizzare con .Net?

La prima importante ragione è che con .Net non è possibile impostare dei trigger sulle proprietà.

Create una proprietà in sola lettura è molto simile a creare una proprietà in .Net. All’interno della chiamata DepedencyProperty.Register() si può chiamare DependencyProperty.RegisterReadonly(). Viene restituita una DependencyPropertyKey, questa dovrebbe essere memorizzata in una variabile privata o protetta (Statica) ed in sola lettura. La chiave ti permette di avere accesso al valore e settarlo come normalmente si farebbe con una Property. La seconda cosa da fare è impostare una DependencyProperty pubblica assegnata a DependencyPropertyKey.DependencyProperty. Questa proprietà è in sola lettura e può essere richiamata dall’esterno.

// Register the private key to set the value
private static readonly DependencyPropertyKey IsMouseOverPropertyKey = 
      DependencyProperty.RegisterReadOnly("IsMouseOver", 
      typeof(bool), typeof(MyClass), 
      new FrameworkPropertyMetadata(false));
 
// Register the public property to get the value
public static readonly DependencyProperty IsMouseoverProperty = 
      IsMouseOverPropertyKey.DependencyProperty;    
 
// .NET Property wrapper
public int IsMouseOver
{
   get { return (bool)GetValue(IsMouseoverProperty); }
   private set { SetValue(IsMouseOverPropertyKey, value); }
}

Per concludere, l’argomento può sembrare di difficile comprensione nei primi momenti (tristissimi momenti) ma subito dopo, se si ha modo di poter vedere in azione queste funzionalità la comprensione risulta molto più facile. Spero che questa guida possa essere di aiuto a qualcuno che come me, alle prime esperienze con nuove tecnologie, si trovi spiazzato e disorientato!

Per qualsiasi domanda ci sono i commenti!