UI Composition :: IMessageBroker, Castle Windsor e le facility
- Non ho tempo, ogni tanto lavoro :-);
- La soluzione che per ora ho adottato per la gestione della navigazione, oggetto dell’ultima puntata, non mi piace, funziona, funziona bene, ma non mi piace… è rumorosa.
Una cosa che tipicamente si fa in un’applicazione composita è iniettare contenuti visuali, e lo si fa ad esempio rispondendo al messaggio che informa dello startup di una certa risorsa, nella fase di “Initialize” di un modulo:
Nulla di trascendentale è vero, ma decisamente noiosetto, soprattutto se l’applicazione è complessa e succedono tante cose, perchè quindi non introdurre un qualcosa che ci semplifichi la vita:this.Broker.Subscribe<ViewModelLoading<IShellViewModel>>( this, msg => { var shellRegionManager = msg.RegionManager; var viewModel = this.viewModelProvider.Resolve<IAccountingViewModel>(); shellRegionManager[ ShellKnownRegions.Ribbon ].Add( viewModel.View ); } );
Questo ci permette di fare una cosa del tipo:public interface IMessageHandler { void Handle( IMessage message ); } public interface IMessageHandler: IMessageHandler where T : IMessage { void Handle( T message ); }
Tralascio il codice dell’handler perchè è davvero triviale. Cosa abbiamo fatto:readonly IMessageHandler<ViewModelLoading<IShellViewModel>> shellViewModelLoadingHandler; public Module( IMessageBroker broker, IMessageHandler<ViewModelLoading<IShellViewModel>> shellViewModelLoadingHandler ) : base( broker ) { Ensure.That( shellViewModelLoadingHandler ).Named( "shellViewModelLoadingHandler" ).IsNotNull();
this.shellViewModelLoadingHandler = shellViewModelLoadingHandler; } protected override void OnInitialize() { this.Broker.Subscribe<ViewModelLoading<IShellViewModel>>( this, msg => { this.shellViewModelLoadingHandler.Handle( msg ); } ); }
- facciamo dipendere il nostro modulo dal message broker e dall’handler;
- durante la fase di “Initialize” del modulo altro non facciamo che “legare” i 2 signori: handler e borker;
Il dependency resolver di Castle Windsor in fase di resolve del componente si accorge che il modulo ha delle dipendenze, cerca quindi tra i componenti registrati qualcuno che sia in grado di soddisfare quelle dipendenze, se lo trova, lo istanzia e lo passa al costruttore…ma non tutto è perduto, altrimenti non sarei qui a tediarvi ;-), abbiamo sottolineato che il codice è decisamente plumbing code, quello che facciamo è semplicemente dire al message broker che se arriva un certo messaggio lo vogliamo far gestire ad uno specifico handler, quindi perchè non automatizzare la cosa?
nel nostro scenario però potete immaginare che ci siano n moduli e quindi ci siano n classi che implementano IMessageHandler> e questo è un problema perchè il dependency resolver non saprebbe quale darvi… ecco quindi che vi tocca cominciare a fare i salti mortali con la gestione della configurazione di Castle, che è tutto tranne che developer friendly (more to come…).
Castle Windsor: Facility
Innanzitutto un po’ di background: una facility è una classe che può essere registrata nel container al fine di interagire con il kernel di Castle e personalizzare il comportamento del container/kernel stesso.
Vediamo cosa ho fatto che è decisamente più semplice del concetto di facility: vogliamo registrare in maniera automatica un message handler in modo che venga implicitamente chiamato quando quel determinato tipo di messaggio viene “sparato”
Quale è il momento migliore per farlo?: durante la registrazione del componente, un message handler è un componente come gli altri quindi viene registrato nel container, quello che vogliamo fare è “infilarci” nel processo di registrazione e customizzare il comportamento. ma come facciamo a capire cosa fare, e soprattutto quando?
AOP is the word, cominciamo con il definire un attributo:
che possiamo usare così:[AttributeUsage( AttributeTargets.Class )] public sealed class SubscribeToMessageAttribute : Attribute { public SubscribeToMessageAttribute( Type messageType ) { if( !typeof( IMessage ).IsAssignableFrom( messageType ) ) { throw new ArgumentException(); } this.MessageType = messageType; } public Type MessageType { get; private set; } }
per decorare il nostro message handler, ok nulla di trascendentale, anzi triviale direi.[SubscribeToMessage( typeof( ViewModelLoading<IShellViewModel> ) )] class ShellViewModelLoadingHandler : MessageHandler<ViewModelLoading<IShellViewModel>> { readonly IViewModelProvider viewModelProvider; public ShellViewModelLoadingHandler( IViewModelProvider viewModelProvider ) { Ensure.That( viewModelProvider ).Named( "viewModelProvider" ).IsNotNull(); this.viewModelProvider = viewModelProvider; } public override void Handle( ViewModelLoading<IShellViewModel> message ) { var shellRegionManager = message.RegionManager; var ribbonRegion = shellRegionManager[ ShellKnownRegions.Ribbon ]; var viewModel = this.viewModelProvider.Resolve<IDeliveriesManagerViewModel>(); ribbonRegion.Add( viewModel.View ); } }
Ma come usiamo questa cosa? Realizziamo una facility per Castle Windsor:
Realizzare una facility è decisamente facile, se no non si chiamerebbe facility… battutaccia :-), creiamo una classe che deriva da AbstractFacility e implementiamo l’unico metodo abstract: Init().class SubscribeToMessageFacility : AbstractFacility { protected override void Init() { this.Kernel.ComponentRegistered += ( s, h ) => { SubscribeToMessageAttribute attribute; if( h.Service.Is<IMessageHandler>() && h.ComponentModel.Implementation.TryGetAttribute( out attribute ) ) { var messageType = attribute.MessageType; var broker = this.Kernel.GetService<IMessageBroker>(); broker.Subscribe( this, messageType, msg => { var handler = this.Kernel.Resolve( s, h.Service ) as IMessageHandler; handler.Handle( msg ); } ); } }; } }
Quello che facciamo è semplicemente chiedere al Kernel di Castle di notificarci quando viene registrato un nuovo componente:
- se il componente registrato implementa IMessageHandler, o viene registrato come un IMessageHandler;
- se il componente registrato è decorato con l’attributo SubscribeToMessageAttribute;
- altro non facciamo che:
- recuperare, sempre dal Kernel, un riferimento al message borker;
- effettuare una sottoscrizione per il tipo di messaggio che l’handler registrato dichiara;
- reagire all’arrivo del messaggio rigirando all’handler in questione l’onere di gestirlo;
container.AddFacility<SubscribeToMessageFacility>();
…e il gioco è fatto!Non solo abbiamo elegantemente risolto un problema cominciando finalmente a sfruttare alcuni degli internals di un container per IoC ma abbiamo anche ridotto a zero il rumore (aka smell) che il plumbing code generava nel codice del modulo.
.m