Un altro intermezzo, si lo so che ho promesso un’ultima puntata ma:
- 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.
Facciamo invece un passo indierto: tempo fa abbiamo parlato di “message broker” per sopperire all’impossibilità di usare, in un’applicazione composita, i tradizionali eventi del mondo .net.
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:
this.Broker.Subscribe<ViewModelLoading<IShellViewModel>>( this, msg =>
{
var shellRegionManager = msg.RegionManager;
var viewModel = this.viewModelProvider.Resolve<IAccountingViewModel>();
shellRegionManager[ ShellKnownRegions.Ribbon ].Add( viewModel.View );
} );
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:
public interface IMessageHandler
{
void Handle( IMessage message );
}
public interface IMessageHandler<T> : IMessageHandler
where T : IMessage
{
void Handle( T message );
}
Questo ci permette di fare una cosa del tipo:
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 );
} );
}
Tralascio il codice dell’handler perchè è davvero triviale. Cosa abbiamo fatto:
- 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;
ma… il codice diventa sempre di più plumbing code noiosissimo e introduce un altro problema da non sottovalutare:
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…
nel nostro scenario però potete immaginare che ci siano n moduli e quindi ci siano n classi che implementano IMessageHandler<ViewModelLoading<IShellViewModel>> 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…).
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?
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” <cit.>.
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:
[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;
}
}
che possiamo usare così:
[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 );
}
}
per decorare il nostro message handler, ok nulla di trascendentale, anzi triviale direi.
Ma come usiamo questa cosa? Realizziamo una facility per Castle Windsor:
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 );
} );
}
};
}
}
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().
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;
L’ultima cosa che ci resta da fare è registrare in fase di startup la nostra facility:
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