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