Detto fatto :-D
Pochissimo tempo fa ho fatto una lunga digressione su Model-View-ViewModel calato in applicazione complessa dove la complessità risiede principalmente lato presentation.
Una delle necessità è quella di mantenere la separazione tra i vari ViewModel al fine di consentire agili refactoring dell’interfaccia utente e volendo anche molto altro. L’inghippo purtroppo è che i ViewModel in qualche modo devono comunicare tra di loro, l’esempio potrebbe essere:
- finestra principale, la Shell (terminologia presa in prestito da PRISM), che ospita 2 “region”:
- Region 1: user control per la ricerca/visualizzazione dei risultati;
- Region 2: user control, “nascosto” dentro un AdornerLayer, che funge da “BusyBox”;
- durante un’operazione asincrona vogliamo visualizzare una sorta di dialo di attesa;
Per far si che i due ViewModel possano cominicare tra loro senza accoppiarli, ad esempio con un evento, abbiamo bisogno di mettere in mezzo un terzo attore, noto ad entrambi che faccia il dispatch dei messaggi:
public interface IMessageBroker
{
void Subscribe<T>( Object subscriber, System.Action<T> callback ) where T : IMessage;
void Unsubscribe( Object subscriber );
void Unsubscribe<T>( Object subscriber ) where T : IMessage;
void Unsubscribe<T>( Object subscriber, System.Action<T> callback ) where T : IMessage;
void Post<T>( T message ) where T : IMessage;
}
Una rapida carrellata sull’interfaccia:
- Subscribe: il subscriber chiede al message borker di essere notificato, invocando il delegato passsato come secondo parametro, quando un messaggio di tipo T viene postato;
- i 3 overload di Unsubscribe permettono rispettivamente di:
- deregistrare la richiesta di notifica, da parte di un subscriber, per un determinato messaggio registrato con una specifica callback;
- deregistrare tutte le richieste di notifica per un determinato messaggio T da parte di uno specifico subscriber;
- deregistrare tutte le richieste di notifica di uno specifico subscriber per tutti i messaggi sottoscritti a prescindere dal tipo di messaggio;
- Post: naturalmente ha o scopo di “postare” un messaggio di tipo T;
IMessage per ora è semplicemente un’interfaccia vuota, li per possibili implementazioni future.
Prima dell’implementazione una semplice nota: ci sono ampi margini di miglioramento e ci possiamo inventare molte altre “funzionalità” ma non mi servivano mi serviva esattamente quello che ho e nulla di più, quindi probabilmente per molti è abbastanza ma per molti altri è decisamente poco… fermatevi un secondo e chiedetevi se è veramente troppo poco tenendo sempre a mente un sanissimo principio: “keep it simple”.
L’implementazione condita da qualche commento:
public class MessageBroker : IMessageBroker
{
private class Subscription
{
public Subscription( Object subscriber, Delegate action )
{
this.Subscriber = subscriber;
this.Action = action;
}
public Object Subscriber { get; private set; }
public Delegate Action { get; private set; }
}
La classe private “Subscription” è un banale tupla che serve per tenere traccia delle callback di ogni subscriber.
readonly Dispatcher dispatcher;
readonly IDictionary<Type, IList<Subscription>> subscriptions = null;
public MessageBroker( Dispatcher dispatcher )
{
Ensure.That( dispatcher ).Named( "dispatcher" ).IsNotNull();
this.dispatcher = dispatcher;
this.subscriptions = new Dictionary<Type, IList<Subscription>>();
}
Il costruttore si assicura di avere un Dispatcher, essendo per un’applicazione WPF ed essendo targhettizzato per il dialogo con la UI vogliamo evitare le insidiose CrossThreadException e garantirci che le invocazioni delle callback avvengano sempre nel thread giusto.
Costruiamo inoltre un dictionary che avrà lo scopo di tener traccia della lista di subscription per un determinato tipo di messaggio.
public void Subscribe<T>( object subscriber, System.Action<T> callback ) where T : IMessage
{
Ensure.That( subscriber ).Named( "subscriber" ).IsNotNull();
Ensure.That( callback ).Named( "callback" ).IsNotNull();
var subscription = new Subscription( subscriber, callback );
if( this.subscriptions.ContainsKey( typeof( T ) ) )
{
var subscribers = this.subscriptions[ typeof( T ) ];
subscribers.Add( subscription );
}
else
{
this.subscriptions.Add( typeof( T ), new List<Subscription>() { subscription } );
}
}
Subscribe è il metodo che consente a qualcuno, è di tipo Object quindi chiunque, di chiedere di essere notificato quando un determinato tipo di messaggio viene postato, subscribe richiede una reference al subscriber e una reference ad un delegato da invocare quando il messaggio richiesto viene postato.
public void Unsubscribe( object subscriber )
{
Ensure.That( subscriber ).Named( "subscriber" ).IsNotNull();
this.subscriptions.Where( msgSubscriptions => msgSubscriptions.Value.Where( subscription => Object.Equals( subscription.Subscriber, subscriber ) ).Any() )
.AsReadOnlyCollection()
.ForEach( kvp => this.subscriptions.Remove( kvp ) );
}
public void Unsubscribe<T>( object subscriber ) where T : IMessage
{
Ensure.That( subscriber ).Named( "subscriber" ).IsNotNull();
if( this.subscriptions.ContainsKey( typeof( T ) ) )
{
var allMessageSubscriptions = this.subscriptions[ typeof( T ) ];
allMessageSubscriptions.Where( subscription => Object.Equals( subscriber, subscription.Subscriber ) )
.AsReadOnlyCollection()
.ForEach( subscription => allMessageSubscriptions.Remove( subscription ) );
}
}
public void Unsubscribe<T>( object subscriber, System.Action<T> callback ) where T : IMessage
{
Ensure.That( subscriber ).Named( "subscriber" ).IsNotNull();
Ensure.That( callback ).Named( "callback" ).IsNotNull();
if( this.subscriptions.ContainsKey( typeof( T ) ) )
{
var allMessageSubscriptions = this.subscriptions[ typeof( T ) ];
allMessageSubscriptions.Where( subscription => Object.Equals( subscriber, subscription.Subscriber ) && Object.Equals( callback, subscription.Action ) )
.AsReadOnlyCollection()
.ForEach( subscription => allMessageSubscriptions.Remove( subscription ) );
}
}
I vari overload di Unsubscribe direi che non necessitano commenti, altro non fanno che rimuovere una subscription.
public void Post<T>( T message ) where T : IMessage
{
Ensure.That( message ).Named( "message" ).IsNotNull();
if( this.subscriptions.ContainsKey( typeof( T ) ) )
{
var subscribers = this.subscriptions[ typeof( T ) ];
subscribers.ForEach( subscription => this.dispatcher.Invoke( subscription.Action, message ) );
}
}
#endregion
}
Infine Post che itera sulle subscription di un determinato messaggio e invoca il corrispondente delegato.
Come si usa? Nulla di più semplice:
public ShellViewModel( Dispatcher dispatcher, IMessageBroker broker )
: base( dispatcher )
{
Ensure.That( broker ).Named( "broker" ).IsNotNull();
this.broker = broker;
this.broker.Subscribe<Messaging.AsyncOperationStartedMessage>( this, msg =>
{
this.IsBusy = true;
} );
this.broker.Subscribe<Messaging.AsyncOperationEndedMessage>( this, msg =>
{
this.IsBusy = false;
} );
}
ShellViewModel è il view model che controlla la shell che altro non è che il macro contenitore che però non sa nulla di quello che ospita, una delle “facilty” erogate dalla shell è la possibilità di visualizzare un pannello in overlay (il classico “Please Wait…”) durante le operazioni asincrone, il pannello è uno UserControl ospitato in un AdornerLayer e la Visibility è in binding che la proprietà IsBusy del view model.
la shell quindi chiede al broker di essere notificata quando vengono postati i 2 messaggi che indicano, rispettivamente, l’inizio e la fine di una operazione asincrona.
Qualcuno che vuole notificare semplicemente si limita a fare una cosa del tipo:
this.broker.Post( new Messaging.AsyncOperationStartedMessage() );
Evoluzioni possibili? una marea:
- Post: aggiungere il “poster” per ad esempio prendere decisioni in base a chi sia il “mandante”…
- Broadcast: una sorta di Post che però non sia sincrono/seriale e che quindi non attenda che tutti i subscriber abbiano processato il messaggio per rilasciare il “poster”;
- Safety/Unsafety:
- Questa implementazione usa necessariamente un dispatcher ma non è detto che serva…;
- Se uno dei subscriber “fallisce” il processo di posting si interrompe potrebbe non essere desiderabile;
- Rivedere la logica di Unsubscribe() per farla funzionare come la registrazione/deregistrazione di un handler ad un evento, in questo momento infatti la logica esegue il confronto per reference con le ovvie conseguenze…;
- Varie ed eventuali.. :-D;
Ogni commento è come al solito ben accetto.
.m