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( Expression<Func> 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( Expression<Func> property, T data );
protected T GetPropertyValue( Expression<Func> 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 : 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; di primo acchito la soluzione che ci inventiamo è probabilmente basata sull’ignobile tentativo di capire, all’interno di SetPpropertyValue(), 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, 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( Expression<Func> property, T value )
{
    ...
}

protected virtual void SetInitialPropertyValue( Expression<Func> 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( Expression<Func> property )
    {
        this.cascadeChangeNotifications.Add( property.GetMemberName() );
    }

    public void RemoveCascadeChangeNotifications( Expression<Func> 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( Expression<Func> 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( Expression<Func> property )
{
    return this.GetPropertyValue( property.GetMemberName() );
}

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

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

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

    return default( T );
}
set
Anche qui nulla di trascendentale a dire il vero:
protected void SetPropertyValueCore( String propertyName, T data, PropertyValueChanged pvc )
{
    var oldValue = this.GetPropertyValue( propertyName );
if( !Object.Equals( oldValue, data ) ) { if( this.valuesBag.ContainsKey( propertyName ) ) { this.valuesBag[ propertyName ] = new PropertyValue( data ); } else { this.valuesBag.Add( propertyName, new PropertyValue( data ) ); } if( pvc != null ) { pvc( new PropertyValueChangedArgs( 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( Expression<Func> property, T data, PropertyValueChanged pvc ) { var propertyName = property.GetMemberName(); this.SetPropertyValue( propertyName, data, pvc ); } protected virtual void SetPropertyValue( String propertyName, T data, PropertyValueChanged pvc ) { this.SetPropertyValueCore( propertyName, data, pvc ); }
Ci sono svariati overload di SetPropertyValue, ho lasciato gli unici due degni di nota perchè “corposi”, tutti comunque alla fine arrivano a SetPropertyValueCore 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