Wpf: WeakEventManager
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:
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;
Grazie al supporto dei command di Wpf diventa comunque abbastanza facile far funzionare il tutto:
Ho già parlato sia di DelegateCommand che di trigger quindi vado al sodo… TriggerUsing() faceva una cosa del genere: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 ); } );
adesso si è posto il problema che in quello scenario complesso questa dipendenza diretta porta, in alcuni casi, il GC a mordersi la coda.public IBindableCommand TriggerUsing( IMonitor source ) { source.Changed += onSourceChanged; return this; }
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;
Non agganciamo più direttamente l’evento ma deleghiamo; per far si che comunque la notifica dell’evento arrivi da noi implementiamo l’interfaccia:public IBindableCommand TriggerUsing( IMonitor source ) { MonitorChangedWeakEventManager.AddListener( source, this ); return this; }
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… :-)Boolean IWeakEventListener.ReceiveWeakEvent( Type managerType, object sender, EventArgs e ) { if( managerType == typeof( MonitorChangedWeakEventManager ) ) { this.OnTriggerChanged( ( IMonitor )sender ); } else { return false; } return true; }
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;
.m