UI Composition :: RegionService, RegionManager(s) & Region(s)
Messaging
Il mondo .net ci ha abituato molto bene, gli eventi sono una vera manna dal cielo, ma purtroppo nel nostro caso servono veramente a poco. Facciamo un esempio chiarificatore:
Avete 2 oggetti che devono comunicare tra loro, e per l’esattezza, l’oggetto A deve sapere quando succede qualcosa all’oggetto B.Tradizionalmente fareste:
- Esporre a B un evento;
- Aggiungere un handler a B.Event da A;
- La Shell si avvia;
- I moduli in esecuzione, che sono stati avviati prima dell’avvio della Shell, hanno bisogno di sapere quando la Shell si sta avviando per iniettare contenuti;
Siccome nel post linkato direi che la trattazione è esaustiva mi limito solo ad un paio di esempi, nel costruttore del ViewModel della Shell abbiamo:
Mentre nel metodo Boot di un module potremmo avere qualcosa del tipo:var regionManager = regionService.GetRegionManager( this.View ); var message = new ViewModelLoading<IShellViewModel>( this, regionManager ); broker.Dispatch( message );
Per risolvere il problema abbiamo introdotto un nuovo attore, il message broker, che è noto ad entrambi i “giocatori”. La Shell adesso può andare dal borker e chiedergli di “postare” un messaggio, allo stesso modo chiunque (che conosca quel tipo di messaggio) può andare dal broker e sottoscrivere un handler (un normalissimo delegate Actionbroker.Subscribe<ViewModelLoading<IShellViewModel>>( this, msg => { var shellRegionManager = msg.RegionManager; var region = shellRegionManager[ ShellKnownRegions.Toolbars ]; var viewModel = container.Resolve<IInjectableContentViewModel>(); region.Add( viewModel.View ); } );
Veniamo adesso alla parte interessante…quello che succede nel delegato che utilizziamo per fare la subscription al generico messagio ViewModelLoading
Region Statiche e Region Dinamiche
Cominciamo con il dirimere questa diatriba. La problematica è di questo genere: avete la necessità di definire delle aree in cui poter iniettare contenuti, naturalmente queste aree devono essere note a priori affinchè un terzo attore possa iniettare contenuti, la naturale conseguenza di questa affermazione è che ogni area (Region) sia identificata con un nome univoco (nell’esempio di poco fa ShellKnownRegions.Toolbars è una “const string”, nulla di più). Seguendo questa strada diventa quindi molto facile recuperare una reference ad una Region e fare quello che vogliamo, ma il mondo non è così semplice…purtroppo ;-)
Lo scenario reale è un po’ più copmplesso, per esemplificarlo viene molto facile fare un parallelo con la UI di Microsoft Outlook:
- Outlook Main Form (la nostra Shell), ad esempio, definisce:
- ToolbarRegion;
- MenuRegion;
- WunderbarRegion;
- ToDoBarRegion;
- …blaBlaRegion…;
- Quando fate doppio click su un messaggio viene aperta una nuova finestra che potrebbe definire:
- MessageRegion;
Potremmo quindi dire che il primo tipo di Region (ad esempio ToolbarRegion) è statica perchè per tutto il ciclo di vita dell’applicazione ne esiste una sola istanza, mentre il secondo tipo, quella definita nel messaggio, è dinamica perchè possiamo avere più istanze; questo secondo scenario ci obbliga a “cercare” una Region sulla base di due chiavi e non più una:
- Il nome della Region;
- La reference alla View che la ospita;
In questo caso abbiamo una reference ad una View, recuperiamo il region service, accediamo al region manager che gestisce le region contenute in quella specifica view e iniettiamo contenuti, ma possiamo anche fare:IView view = ...; var regionService = container.Resolve<IRegionService>(); var manager = regionService.GetRegionManager( view ); manager[ "myRegionName" ].Add( ... );
Che è decisamente più semplice, stiamo però assumento che esista una sola istanza della view in questione, internamente succede quasi la stessa cosa dello scenario precedente ma se la nostra architettura prevede che non possano esistere più istanze della Shell questa seconda possibilità è una scorciatoia molto comoda. Possiamo quindi dire che le Region statiche sono inutili, in realtà tutte le region appartengono ad un’istanza di una view sono qundi tutte region dinamiche, solo che alcune sono “singleton”.var shellRegionManager = regionService.GetKnownRegionManager<IShellView>(); shellRegionManager[ "myRegionName" ].Add( ... );
Ok, ma la domanda è: come funziona tutto ciò?
UI Injection: RegionService
Esiste un solo RegionService (Singleton) per ogni istanza dell’applicazione in esecuzione, quello che ci permette di fare è recuperare una reference ad un RegionManager, registrarne uno nuovo o deregistrarlo.interface IRegionService { Boolean HoldsRegionManager( IView owner ); IRegionManager GetRegionManager( IView owner ); IRegionManager GetKnownRegionManager() where TView : IView; IRegionManager RegisterRegionManager( IView owner ); void UnregisterRegionManager( IView owner ); }
UI Injection: RegionManager
Un RegionManager gestisce le region definite nella view a cui appartiene:
nulla di trascendentale, possiamo registrare una region e recuperare una reference attraverso il nome con cui la region è stata registrata.interface IRegionManager { void RegisterRegion( IRegion region ); IRegion this[ String name ] { get; } }
UI Injection: Region
Una region infine è l’area in cui possiamo effettivamente iniettare contenuti:
Perchè esiste il concetto di ActiveContent, Add o Activate? perchè una Region potrebbe essere qualsiasi cosa e quindi non ospitare solo ed esclusivamente un singolo contenuto, un esempio: TabPagesRegion.interface IRegion { String Name { get; } IView Owner { get; } IView ActiveContent { get; } void Add( IView content ); void Activate( IView content );
… }
Adesso che abbiamo definito al nostra infrastruttura come la usiamo? Facciamo il percorso inverso, e partiamo dalla Region questa volta, abbiamo qualcosa del tipo:
oppure, più semplicemente:<sr:RibbonWindow.Ribbon> <sr:Ribbon rg:RegionService.Region="{divex:RibbonRegion {x:Static my:ShellKnownRegions.Ribbon}}" /> sr:RibbonWindow.Ribbon>
Che dire… Attached Properties Rulez!!!, cosa succede dietro le quinte:<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion {x:Static my:MyModuleKnownRegions.myRegionName}}" />
- Un FrameworkElement, che contiene quel tipo di markup, viene “avviato”;
- Viene invocata la attached property, partendo dal FrameworkElement in cui è definita la property:
- Viene fatto il walking a “marcia indietro” del VisualTree;
- Il primo elemento che implementa l’interfaccia IView viene considerato come l’owner della region;
- si va dal region service e si recupera (o si crea se non esiste) un region manager per quella view;
- si registra la region con il region manager;
Lo sviluppatore che approccia lo sviluppo di un modulo quindi non deve preoccuparsi di nulla, ma semplicemente dichiarare, via xaml, cosa esporre e dove. Se proprio vuole può definire nuove tipologie di region creando una classe che implementa l’interfaccia IRegion e utilizzarla via xaml, l’infrastruttura digerirà il nuovo arrivato senza battere ciglio, che stomaco… ;-)
Dal punto di vista WPF invece c’è da notare che:
- Esiste una classe base Region
che implementa IRegion e che si “smazza” l’implementazione di MarkupExtension al fine di permettere la sintassi esposta: Region="{rg:ContentPresenterRegion ’myRegionName’}"; - La classe RegionService, che implementa IRegionService, espone anche la logica statica per far funzionare l’Attached Property. Qui purtroppo c’è una magagna che non ho ancora risolto, o meglio ci ho messo una pezza, ho in mente un paio di altre possibilità, ma in generale è comunque una bruttura… L’inghippo è che una Attached property altro non è che una manciata di metodi statici e una dependency property registrata come attached, essendo tutto statico è impossibile iniettare la dipendenza dal region service, la soluzione (brutta, ma efficace) è stata esporre dalla attached property anche un evento statico che viene invocato la prima volta che la attached property ha bisogno del region service; in questa fase è il bootstrapper dell’applicazione che si fa carico di registrare un handler per l’evento e risolvere il region service, si potrebbe pensare di scrivere una facility per Castle, sempre bruttino ma almeno nascosto ;-);
Allego il progetto allo stadio attuale: CompositeUI_v4.zip.
.m