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