Trovo che la sintassi di C# accoppiata ad un po’ di sane Fluent Interfaces porti a qualcosa di semplicemente bello, l’eleganza del risultato è una delle cose che mi piacciono sempre molto.
var command = DelegateCommand.Create()
.OnCanExecute( o => true )
.OnExecute( o => { } )
.TriggerUsing( PropertyChangedObserver
.Monitor( this )
.HandleChangesOf( vm => vm.IsValid ) );
Cosa è? la prima parta direi che non necessità di spiegazioni, se usate Wpf e avete mai sentito parlare di M-V-VM siete sicuramente incappati in una della migliaia di implementazioni del concetto di DelegateCommand / RelayCommand / OgnunoLoChiamaComeVuoleMaSempreQuelloè… cioè un meccanismo per costruire un ICommand, componente vitale e fantastico nel mondo Wpf, utilizzando i delegati.
La necessità però in M-V-VM è spesso quella di poter rivalutare lo stato di CanExecute() di un command a “richiesta” e spesso si arriva a soluzioni di questo genere, perlomeno io ho sempre fatto così:
protected override void OnPropertyChanged( PropertyChangedEventArgs e )
{
base.OnPropertyChanged( e );
switch( e.PropertyName )
{
case "IsValid":
this.ReevaluateCommands();
break;
}
}
che sinceramente sono di una bruttura mai vista :-) oltre al fatto che se volete triggherare per motivi diversi dal “Property Changed” dovete spargere altre brutture.
Guarda giù tu…
Questo è il classico esempio in cui un piccolo refactoring introducendo un piccolo componente fa la differenza:
public interface IMonitor
{
event EventHandler Changed;
void StopMonitoring();
}
public interface IMonitor<T> : IMonitor
{
T Source { get; }
}
Definiamo che cosa ci serve e come lo vogliamo usare:
public class DelegateCommand : ICommand
{
… omissis …
private EventHandler triggerHanlder = null;
public DelegateCommand TriggerUsing( IMonitor source )
{
source.Changed += this.triggerHanlder;
return this;
}
public DelegateCommand RemoveTrigger( IMonitor source )
{
source.Changed -= this.triggerHanlder;
return this;
}
}
Ho rimosso di tutto e di più, come la RAI :-), e a questo punto proviamo a realizzarne uno di Observer/Monitor.
PropertyChangedObserver
public static class PropertyChangedObserver
{
public static PropertyChangedMonitor<T> Monitor<T>( T source )
where T : class, INotifyPropertyChanged
{
return new PropertyChangedMonitor<T>( source );
}
}
public class PropertyChangedMonitor<T> : INotifyPropertyChanged, IMonitor<T>
where T : class, INotifyPropertyChanged
{
public event EventHandler Changed;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged( PropertyChangedEventArgs args )
{
if( this.PropertyChanged != null )
{
this.PropertyChanged( this, args );
}
if( this.Changed != null )
{
this.Changed( this, EventArgs.Empty );
}
}
IDictionary<String, Action<T, String>> propertiesToWatch = new Dictionary<String, Action<T, String>>();
PropertyChangedEventHandler handler = null;
public PropertyChangedMonitor( T source )
{
Ensure.That( source ).Named( "source" ).IsNotNull();
handler = ( s, e ) =>
{
Action<T, String> callback;
if( propertiesToWatch.TryGetValue( e.PropertyName, out callback ) )
{
if( callback != null )
{
callback( source, e.PropertyName );
}
}
if( propertiesToWatch.ContainsKey( e.PropertyName ) )
{
this.OnPropertyChanged( e );
}
};
this.Source = source;
this.Source.PropertyChanged += handler;
}
public T Source
{
get;
private set;
}
public PropertyChangedMonitor<T> HandleChangesOf<TProperty>( Expression<Func<T, TProperty>> property )
{
Ensure.That( property ).Named( "property" ).IsNotNull();
this.HandleChangesOf( property, null );
return this;
}
public PropertyChangedMonitor<T> HandleChangesOf<TProperty>( Expression<Func<T, TProperty>> property, Action<T, String> callback )
{
Ensure.That( property ).Named( "property" ).IsNotNull();
var propertyName = property.GetMemberName();
if( propertiesToWatch.ContainsKey( propertyName ) )
{
propertiesToWatch[ propertyName ] = callback;
}
else
{
propertiesToWatch.Add( propertyName, callback );
}
return this;
}
public PropertyChangedMonitor<T> DismissHandlingOf<TProperty>( Expression<Func<T, TProperty>> property )
{
var propertyName = property.GetMemberName();
if( propertiesToWatch.ContainsKey( propertyName ) )
{
propertiesToWatch.Remove( propertyName );
}
return this;
}
public void StopMonitoring()
{
this.Source.PropertyChanged -= handler;
this.Source = null;
}
}
Che altro non fa che tenere sotto controllo una istanza di INotifyPropertyChanged e osservare se viene scatenato l’evento PropertyChanged per una delle proprietà che ci interessano.
Con poco codice e decisamente pochi minuti (direi non più di mezz’ora tra pensare la cosa, realizzarla e affinarla) abbiamo incapsulato della logica in un componente riutilizzabile ma soprattutto abbiamo delegato al “coso” giusto il suo compito.
Naturalmente i “trigger” sono impilabili e quindi questo funziona come ci si aspetta:
var command = DelegateCommand.Create()
.OnCanExecute( o => true )
.OnExecute( o => { } )
.TriggerUsing( PropertyChangedObserver
.Monitor( this )
.HandleChangesOf( vm => vm.IsValid )
.HandleChangesOf( vm => vm.DataSource ) )
.TriggerUsing( MementoObserver.Monitor( this.Memento ) );
Dove il MementoObserver è un IMonitor per un “memento service” che è un’altra storia, stay tuned :-)
Cosa manca?
probabilmente la gestione degli eventi tramite “weak reference” ma è un’ottimizazzione prematura…
.m