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

Technorati Tags: ,