Ci siamo da poco scontrati con il concetto di Logical e Visual Tree e con l’evidente necessità di tenere in ordine la memoria per consentire al GC di fare il suo lavoro quando serve, risulta però evidente, sempre più al crescere della soluzione, che lasciare per strada qualcosa potrebbe essere molto facile.
Weak*
Abbiamo già visto come una WeakReference possa essere un’ottima soluzione ad alcune problematiche, ribaltiamo adesso lo scenario e mettiamoci nel panni di una classe che aggancia un handler ad un evento: se la classe (target) che ha agganciato l’evento non ha modo di sapere quando e se il sender va out-of-scope è evidente che questo può portare ad un .net-memory-leak perchè il nostro target sta impedendo che il GC possa liberare la memoria occupata dal sender.
Wpf introduce il supporto a questo scenario estendendo il concetto di WeakReference e introducendo quello di WeakEventManager: un weak event manager è qualcuno che si mette in mezzo tra il sender e il target facendo da ponte tra i due e gestendo internamente una sorta di weak reference in modo che se il sender esce dallo scope possa venir correttamente gestito dal GC.
Si, ok… ma come si fa?
Inquadriamo prima il potenziale problema con un esempio:
image
In una UI come quella qui sopra succedono generalmente un po’ di cose, tra cui:
  • I bottoncini “impegnato” e “stampa” si attivano disattivano in base alla selezione nel documento sottostante… a patto che:
    • Un documento sottostamte esista…;
    • Un documento sottostante sia il documento attivo;
    • etc…
  • Il ribbon reagisce al cambio di documento corrente “seguendo” come un segugio, sulla base di un set di regole il documento attivo: ad esempio se selezionate il documento “Home” automaticamente la tab “Home” del ribbon diventa quella attiva e se tornate ai risultati della ricerca la tab “Pubblicazioni” diventa quella attiva; ma se vi spostate su Home e poi manualmente riportate il ribbon su “Pubblicazioni” allora i pulsanti (di cui sopra) reagiscono di conseguenza disattivandosi perchè il documento attivo non è quello giusto;
Come potete immaginare in tutto questo giro di handling di eventi ce ne sono una vera montagna, non solo…, la cosa è complicata dal fatto che alcune parti di quella UI sono iniettate a runtime e sanno poco o nulla di chi le sta ospitando e chi le ospita proprio non sa nulla di loro.
Grazie al supporto dei command di Wpf diventa comunque abbastanza facile far funzionare il tutto:
this.PrintCommand = DelegateCommand.Create()
    .TriggerUsing( this.regionMonitor )
    .TriggerUsing( this.listChangedMonitor )
    .OnCanExecute( o =>
    {
        var canExecute = false;
        this.regionMonitor.TryGetViewModel<IPublicationListViewModel>( this.regionMonitor.ActiveContent, vm =>
        {
            canExecute = vm.SelectedItems.Count() == 1;
        } );

        return canExecute;
    } )
    .OnExecute( o =>
    {
        this.PrintSelection( this.actualPrinterSettings );
    } );
Ho già parlato sia di DelegateCommand che di trigger quindi vado al sodo… TriggerUsing() faceva una cosa del genere:
public IBindableCommand TriggerUsing( IMonitor source )
{
    source.Changed += onSourceChanged;
    return this;
}
adesso si è posto il problema che in quello scenario complesso questa dipendenza diretta porta, in alcuni casi, il GC a mordersi la coda.
Please welcome the “WeakEventManager”
La linee guida dicono di:
  • Creare una classe ad hoc, che eredita da WeakEventManager, ed è specilizzata per la gestione di un particolare evento: MonitorChangedWeakEventManager;
  • Implementare sul “gestore” dell’evento, in questo caso quindi il DelegateCommand, l’interfaccia IWeakEventListener;
  • Modificare il codice di “aggancio” dell’evento e passare la responsabilità al WeakEventManager di turno;
Partiamo quindi dal fondo, TriggerUsing() diventa:
public IBindableCommand TriggerUsing( IMonitor source )
{
    MonitorChangedWeakEventManager.AddListener( source, this );

    return this;
}
Non agganciamo più direttamente l’evento ma deleghiamo; per far si che comunque la notifica dell’evento arrivi da noi implementiamo l’interfaccia:
Boolean IWeakEventListener.ReceiveWeakEvent( Type managerType, object sender, EventArgs e )
{
    if( managerType == typeof( MonitorChangedWeakEventManager ) )
    {
        this.OnTriggerChanged( ( IMonitor )sender );
    }
    else
    {
        return false;
    }

    return true;
}
Questo è quanto: quando il WeakEventManager deve notificarci chiama ReceiveWeakEvent passandoci il tipo di manager, in modo da consentirci di prendere decisioni sulla base di chi sia il gestore, e passandoci i classici “sender” e “args”. Nostro compito è ritonare un Boolean che confermi o meno se l’evento ci è piaciuto… :-)
Il cuore del giochetto
Il tutto è gestito in maniera abbastanza semplice dal nostro manager specializzato:
public sealed class MonitorChangedWeakEventManager : WeakEventManager
{
    static MonitorChangedWeakEventManager GetCurrentManager()
    {
        var mt = typeof( MonitorChangedWeakEventManager );

        var manager = ( MonitorChangedWeakEventManager )WeakEventManager.GetCurrentManager( mt );
        if( manager == null )
        {
            manager = new MonitorChangedWeakEventManager();
            WeakEventManager.SetCurrentManager( mt, manager );
        }

        return manager;
    }

    public static void AddListener( IMonitor source, IWeakEventListener listener )
    {
        MonitorChangedWeakEventManager
            .GetCurrentManager()
            .ProtectedAddListener( source, listener );
    }

    public static void RemoveListener( IMonitor source, IWeakEventListener listener )
    {
        MonitorChangedWeakEventManager
            .GetCurrentManager()
            .ProtectedRemoveListener( source, listener );
    }

    private MonitorChangedWeakEventManager()
    {

    }

    void OnChanged( object sender, EventArgs args )
    {
        base.DeliverEvent( sender, args );
    }

    protected override void StartListening( object source )
    {
        var trigger = source as IMonitor;
        if( trigger != null )
        {
            trigger.Changed += OnChanged;
        }
    }

    protected override void StopListening( object source )
    {
        var trigger = source as IMonitor;
        if( trigger != null )
        {
            trigger.Changed -= OnChanged;
        }
    }
}
  • GetCurrentManager() sostanzialmente è un singleton e si appoggia a WeakEventManager per persistere la reference;
  • AddListener e RemoveListener sono i nostri entry point che delegano alla classe base l’operatività;
  • StartListening e StopListening vengono invocati dalla classe base per gestire il vero e proprio aggiancio/sgancio dell’handler;
  • Infine OnChanged, il nostro handler, utilizza il metodo DeliverEvent della classe base per dispatchare in maniera sicura l’evento;
Nulla di trascendentale, anzi alla lunga un po’ noiosetto da scrivere, ma per molte situazioni ottimo e risolutivo.
.m