Partiamo da questo semplice test:

[TestMethod]
public void entity_set_property_normal_should_raise_propertyChanged_event()
{
    var expected = 1;
    var actual = 0;

    var target = new MockEntity();
    target.PropertyChanged += ( s, e ) => actual++;

    target.FirstName = "Mauro";

    actual.ShouldBeEqualTo( expected );
}

Proprio triviale, una semplice entità che implementa INotifyPropertyChanged, tipicamente l’implementazione della proprietà FirstName potrebbe essere una cosa del tipo:

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged( String propertyName )
    {
        if( this.PropertyChanged != null )
        {
            this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }

    private String _firstName = String.Empty;
    public String FirstName
    {
        get { return this._firstName; }
        set
        {
            if( value != this.FirstName )
            {
                this._firstName = value;
                this.OnPropertyChanged( "FirstName" );
            }
        }
    }
}

Che ha il solo difetto, oltre al fatto che è da scrivere di non digerire bene il refactoring… a questo c’è comunque un’elegantissima soluzione:

protected virtual void OnPropertyChanged<T>( Expression<Func<T>> property )
{
    if( this.PropertyChanged != null )
    {
        var expression = property.Body as MemberExpression;
        var member = expression.Member;

        this.PropertyChanged( this, new PropertyChangedEventArgs( member.Name ) );
    }
}

private String _firstName = String.Empty;
public String FirstName
{
    get { return this._firstName; }
    set
    {
        if( value != this.FirstName )
        {
            this._firstName = value;
            this.OnPropertyChanged( () => this.FirstName );
        }
    }
}

Resta sempre il fatto che è da scrivere. Scroccando un po’ di sintassi alle Dependency Property di Wpf vorremmo limitarci a scrivere questo:

public String FirstName
{
    get { return this.GetPropertyValue( () => this.FirstName ); }
    set { this.SetPropertyValue( () => this.FirstName, value ); }
}

Che ha il vantaggio:

  • di essere estremamente conciso;
  • viene ben digerito dagli strumenti di refactoring;
  • e implementa “gratis” INotifyPropertyChanged e, volendo, non solo…;

Dopo un po’ di ragionamenti* ci troviamo però di fronte ad un primo problema che le Dependency Property non risolvono: Boxing/Unboxing dei value type. In un dependency object infatti i metodi SetValue e GetValue prendono e tornano “Object” facendo si che i value type vengano tutte le volte boxed e unboxed.

* i ragionamenti da qui in avanti sono tutti frutto di TDD, quindi le feature descritte non feature messe li perchè seg*e mentali, ma piuttosto feature derivanti dall’uso in contesti reali. Scrivo il test che definisce lo scenario e quindi implemento la feature.

Possiamo aggirare il problema? Direi di si, innanzitutto vediamo la firma che vorremmo avere nei nostri metodi Get/Set:

protected void SetPropertyValue<T>( Expression<Func<T>> property, T data );
protected T GetPropertyValue<T>( Expression<Func<T>> property )

Cominciamo quindi da subito a liberarci di Object, se però ci pensiamo è evidente che la classe base dovrà trovare un tipo-minimoComunDenominatore per gestire facilmente tutti i valori che possono arrivare, ed è pure evidente che la scelta più facile sia Object, forse…:

public abstract class PropertyValue
{

}

Perchè no… definiamo un generico tipo che descrive un valore qualsiasi, e poi lo specializziamo così:

public class PropertyValue<T> : PropertyValue
{
    public PropertyValue( T value )
    {
        this.Value = value;
    }

    public T Value { get; private set; }
}

Utilizzando un vero tipo-generico che incapsula il valore che vogliamo memorizzare, questo lo possiamo quindi persistere in una struttura del tipo:

readonly IDictionary<String, PropertyValue> valuesBag;

Detto questo se continuiamo a pensare a come siamo abituati ad utilizzare una classe di business ci scontriamo con questa necessità:

class Person
{
    public Person(String name)
    {
        this.Name = name;
    }

    public String Name { get; set; }
}

Un costruttore che setta un valore di iniziale, o comunque la necessità di inizializzare una proprietà con un valore di default, ma soprattutto la necessità che questa inizializzazione non triggheri tutti gli eventuali meccanismi che stanno dietro il SetPropertyValue<T>; di primo acchito la soluzione che ci inventiamo è probabilmente basata sull’ignobile tentativo di capire, all’interno di SetPpropertyValue<T>(), se siamo o meno in un costruttore, ma è una soluzione pessima perchè porta allo stesso nightmare che saremmo costretti ad affrontare se ci addentrassimo nei meandri dell’implementazione di IQueryable<T>, osservate questo semplice esempio che spiega al volo il problema:

class Person
{
    public Person()
    {
        this.OnInitialize();
    }

    protected virtual void OnInitialize()
    {
        this.Name = "default value";
    }

    public String Name { get; set; }
}

Non dico altro :-), la soluzione come sempre è la più semplice di tutte:

readonly IDictionary<String, PropertyValue> initialValuesBag;

protected void SetInitialPropertyValue<T>( Expression<Func<T>> property, T value )
{
    ...
}

protected virtual void SetInitialPropertyValue<T>( Expression<Func<T>> property, T value, PropertyMetadata metadata )
{
    ...
}

Che introduce un’altra caratteristica interessante, tipicamente una proprietà fa queste cose:

  • Memorizza un valore;
  • Notifica, se implementato o se necessario, il cambiamento del valore memorizzato;
  • Trigghera la notifica in cascata di altre proprietà, esempio tipico la variazione della proprietà BornDate : DateTime triggherà anche la notifica della variazione della proprietà Age : Int32 che probabilmente è in sola lettura;

Abbiamo quindi bisogno di poter definire per ogni singola proprietà un comportamento:

public class PropertyMetadata
{
    public static readonly PropertyMetadata Default = new PropertyMetadata();

    readonly HashSet<String> cascadeChangeNotifications = new HashSet<String>();

    public PropertyMetadata()
    {
        this.NotifyChanges = true;
    }

    public Boolean NotifyChanges { get; set; }

    public void AddCascadeChangeNotifications<T>( Expression<Func<T>> property )
    {
        this.cascadeChangeNotifications.Add( property.GetMemberName() );
    }

    public void RemoveCascadeChangeNotifications<T>( Expression<Func<T>> property )
    {
        var key = property.GetMemberName();
        if( this.cascadeChangeNotifications.Contains( key ) )
        {
            this.cascadeChangeNotifications.Remove( key );
        }
    }

    public IEnumerable<String> GetCascadeChangeNotifications()
    {
        return this.cascadeChangeNotifications.AsEnumerable();
    }
}

Permettendoci di scrivere questo:

class Person : Entity
{
    public Person( DateTime bornDate )
    {
        var metadata = new PropertyMetadata();
        metadata.AddCascadeChangeNotifications( () => this.Age );
        
this.SetInitialPropertyValue( () => this.BornDate, bornDate, metadata ); } public DateTime BornDate { get { return this.GetPropertyValue( () => this.BornDate ); } set { this.SetPropertyValue( () => this.BornDate, value ); } } public int Age { get{ /*Eval age base on BornDate*/ return 0; } } }

Che è decisamente interessante e soddisfa pienamente questo:

[TestMethod]
public void person_set_bornDate_should_raise_all_expected_notifications()
{
    var expected = 2;
    var actual = 0;

    var expectedNotifications = new[] { "BornDate", "Age" };
    var actualNotifications = new List<String>();

    var target = new Person( new DateTime( 1973, 1, 10 ) );
    target.PropertyChanged += ( s, e ) =>
    {
        actual++;
        actualNotifications.Add( e.PropertyName );
    };

    target.BornDate = new DateTime( 1978, 11, 5 );

    actual.ShouldBeEqualTo( expected );
    actualNotifications.ShouldBeSameAs( expectedNotifications );
}

Abbiamo anche la necessità, molto semplice, di voler impostare dei metadati senza impostare però un valore di default, possiamo soddisfare questo requisito così:

readonly IDictionary<String, PropertyMetadata> propertiesMetadata;

protected PropertyMetadata GetPropertyMetadata( String propertyName )
{
    PropertyMetadata md;
    if( !this.propertiesMetadata.TryGetValue( propertyName, out md ) )
    {
        md = PropertyMetadata.Default;
    }

    return md;
}

protected void SetPropertyMetadata<T>( Expression<Func<T>> property, PropertyMetadata metadata )
{
    Ensure.That( metadata ).Named( "metadata" ).IsNotNull();

    var key = property.GetMemberName();
    this.SetPropertyMetadata( key, metadata );
}

protected virtual void SetPropertyMetadata( String propertyName, PropertyMetadata metadata )
{
    Ensure.That( metadata ).Named( "metadata" ).IsNotNull();

    Ensure.That( propertiesMetadata )
        .WithMessage( "Metadata for the supplied property ({0}) has already been set.", propertyName )
        .IsFalse( d => d.ContainsKey( propertyName ) );

    propertiesMetadata.Add( propertyName, metadata );
}

Il focus a questo punto si sposta sulla gestione del valore della proprietà, quello che abbiamo bisogno di fare è:

  • get: vogliamo poter recuperare un valore, se questo valore non è mai stato impostato vogliamo l’eventuale valore iniziale e se questo non è mai stato impostato vogliamo il valore di default per il tipo (System.Type) della proprietà;
  • set: vogliamo poter fare il set di un valore, se il valore cambia e la proprietà è impostata per triggherare la notifica vogliamo la notifica, e se ci sono delle proprietà che devono essere “notificate” in cascata vogliamo la notifica in cascata;

get

Questo è abbastanza semplice:

protected T GetPropertyValue<T>( Expression<Func<T>> property )
{
    return this.GetPropertyValue<T>( property.GetMemberName() );
}

protected virtual T GetPropertyValue<T>( String propertyName )
{
    PropertyValue actual;
    if( this.valuesBag.TryGetValue( propertyName, out actual ) )
    {
        return ( ( PropertyValue<T> )actual ).Value;
    }

    return this.GetInitialPropertyValue<T>( propertyName );
}
protected T GetInitialPropertyValue<T>( Expression<Func<T>> property )
{
    return this.GetInitialPropertyValue<T>( property.GetMemberName() );
}

protected virtual T GetInitialPropertyValue<T>( String propertyName )
{
    PropertyValue value;
    if( this.initialValuesBag.TryGetValue( propertyName, out value ) )
    {
        return ( ( PropertyValue<T> )value ).Value;
    }

    return default( T );
}

set

Anche qui nulla di trascendentale a dire il vero:

protected void SetPropertyValueCore<T>( String propertyName, T data, PropertyValueChanged<T> pvc )
{
    var oldValue = this.GetPropertyValue<T>( propertyName );
if( !Object.Equals( oldValue, data ) ) { if( this.valuesBag.ContainsKey( propertyName ) ) { this.valuesBag[ propertyName ] = new PropertyValue<T>( data ); } else { this.valuesBag.Add( propertyName, new PropertyValue<T>( data ) ); } if( pvc != null ) { pvc( new PropertyValueChangedArgs<T>( data, oldValue ) ); } this.OnPropertyChanged( propertyName ); var metadata = this.GetPropertyMetadata( propertyName ); var cascadeChanges = metadata.GetCascadeChangeNotifications(); if( cascadeChanges.Any() ) { foreach( var cascade in cascadeChanges ) { this.OnPropertyChanged( cascade ); } } } } protected void SetPropertyValue<T>( Expression<Func<T>> property, T data, PropertyValueChanged<T> pvc ) { var propertyName = property.GetMemberName(); this.SetPropertyValue<T>( propertyName, data, pvc ); } protected virtual void SetPropertyValue<T>( String propertyName, T data, PropertyValueChanged<T> pvc ) { this.SetPropertyValueCore( propertyName, data, pvc ); }

Ci sono svariati overload di SetPropertyValue<T>, ho lasciato gli unici due degni di nota perchè “corposi”, tutti comunque alla fine arrivano a SetPropertyValueCore<T> che si limita a:

  • recuperare il valore attuale;
  • confrontarlo con quello in arrivo;
  • se c’è una differenza:
    • settare il nuovo;
    • chiamare l’eventuale callback che una classe derivata può iniettare per sapere quando una proprietà cambia;
    • notificare la variazione di stato della proprietà;
    • notificare le eventuali “cascade notifications”;

Tutto questo inoltre apre a scenari “collaterali” alquanto curiosi, osservate questi test:

[TestMethod]
public void entity_with_initial_value_rejectChanges_should_reset_property_values()
{
    var expected = "initial value";

    var target = new MockEntity( expected );
    target.FirstName = "Mauro";

    target.RejectChanges();

    target.FirstName.ShouldBeEqualTo( expected );
}
[TestMethod]
public void entity_set_property_should_set_entity_as_changed()
{
    var target = new MockEntity();
target.FirstName = "Mauro"; target.IsChanged.ShouldBeTrue(); } [TestMethod] public void entity_rejectChanges_should_reset_isChanged() { var target = new MockEntity(); target.FirstName = "Mauro";
target.RejectChanges(); target.IsChanged.ShouldBeFalse(); }

Tralascio l’implementazione perchè esula dallo scopo di questo post e per ora è ad un livello embrionale e probabilmente non vedrà mai la luce, ma credo sia facile immaginare come funzioni ;-)

Adesso il prossimo passaggio sarebbe far fare il tutto ad un bel proxy dinamico o a PostSharp in modo da tendere verso POCO… anche se non lo ritengo un must, piuttosto una seg* mentale.

Tendenzialmente sono orientato verso PostSharp anche se ha un complessità implementativa nettamente superiore, a dire il vero l’implementazione con il Dynamic Proxy di Castle c’è ed è pure perfettamente funzionante, circa un paio d’ore di lavoro, ma soffre di una serie di effetti collaterali poco piacevoli… ma è un’altra storia.

ndr1: gli effetti collaterali non sono derivanti dal Dynamic Proxy in se quanto piuttosto dal concetto di proxy in generale, ho provato anche altri runtime proxy generator e nessuno risolve i problemi che ho incontrato semplicemente perchè è l’infrastruttura del framework che non permette di risolverli a runtime.

ndr2: questo non è ancora codice di produzione e probabilmente non lo sarà mai, sto semplicemente sperimentando cosa che grazie all’estrema eleganza di C# è proprio piacevole, per ora sto usando questo esempio in una semplice applicazione per uso “familiare”, chi vivrà vedrà… quello che mi manca per ora, e non ho voglia di fare perchè decisamente noiso, è un serio confronto prestazionale per capire se i vantaggi, notevoli direi, in termini di scrittura non sono del tutto distrutti da una altrettanto notevole perdita di prestazioni… ci proverò?

Buona domenica tutti.

.m