Prima di passare all’argomento centrale di questa lunga trattazione dobbiamo fare un piccolo escursus sul sistema di comunicazione interno all’applicazione.
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;
Questo comporta però avere una reference a B da A, altrimenti, non c’è trippa per gatti :-D, gli eventi servono ad un gran poco. Facciamo quindi un esempio calato nel nostro scenario:
- 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;
Decisamente semplice ma non risolvibile con dei semplici eventi, quindi: Message Broker.
Siccome nel post linkato direi che la trattazione è esaustiva mi limito solo ad un paio di esempi, nel costruttore del ViewModel della Shell abbiamo:
var regionManager = regionService.GetRegionManager( this.View );
var message = new ViewModelLoading<IShellViewModel>( this, regionManager );
broker.Dispatch( message );
Mentre nel metodo Boot di un module potremmo avere qualcosa del tipo:
broker.Subscribe<ViewModelLoading<IShellViewModel>>( this, msg =>
{
var shellRegionManager = msg.RegionManager;
var region = shellRegionManager[ ShellKnownRegions.Toolbars ];
var viewModel = container.Resolve<IInjectableContentViewModel>();
region.Add( viewModel.View );
} );
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 Action<T>) al fine di essere notificato quando qualcuno posta quel tipo di messaggio. Il tutto può funzionare perchè il message broker è registrato nel service container come singleton, quindi chiunque chieda un’istanza del broker ottiene sempre la stessa reference.
Veniamo adesso alla parte interessante…quello che succede nel delegato che utilizziamo per fare la subscription al generico messagio ViewModelLoading<TViewModel>.
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:
Il problema dovrebbe essere evidentissimo, possiamo fare doppio click su più di un messaggio e quindi avere in contemporanea disponibili n MessageRegion…con l’inghippo che se l’unico modo per avere un riferimento ad una Region è il suo nome non avremmo nessun modo per scegliere in quale delle istanze esistenti di Region inserire i nostri contenuti.
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;
Questa accoppiata è sicuramente univoca, abbiamo quindi bisogno di supportare i seguenti scenari:
IView view = ...;
var regionService = container.Resolve<IRegionService>();
var manager = regionService.GetRegionManager( view );
manager[ "myRegionName" ].Add( ... );
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:
var shellRegionManager = regionService.GetKnownRegionManager<IShellView>();
shellRegionManager[ "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”.
Ok, ma la domanda è: come funziona tutto ciò?
UI Injection: RegionService
interface IRegionService
{
Boolean HoldsRegionManager( IView owner );
IRegionManager GetRegionManager( IView owner );
IRegionManager GetKnownRegionManager<TView>() where TView : IView;
IRegionManager RegisterRegionManager( IView owner );
void UnregisterRegionManager( IView owner );
}
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.
UI Injection: RegionManager
Un RegionManager gestisce le region definite nella view a cui appartiene:
interface IRegionManager
{
void RegisterRegion( IRegion region );
IRegion this[ String name ] { get; }
}
nulla di trascendentale, possiamo registrare una region e recuperare una reference attraverso il nome con cui la region è stata registrata.
UI Injection: Region
Una region infine è l’area in cui possiamo effettivamente iniettare contenuti:
interface IRegion
{
String Name { get; }
IView Owner { get; }
IView ActiveContent { get; }
void Add( IView content );
void Activate( IView content );
…
}
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.
Adesso che abbiamo definito al nostra infrastruttura come la usiamo? Facciamo il percorso inverso, e partiamo dalla Region questa volta, abbiamo qualcosa del tipo:
<sr:RibbonWindow.Ribbon>
<sr:Ribbon rg:RegionService.Region="{divex:RibbonRegion {x:Static my:ShellKnownRegions.Ribbon}}" />
</sr:RibbonWindow.Ribbon>
oppure, più semplicemente:
<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion {x:Static my:MyModuleKnownRegions.myRegionName}}" />
Che dire… Attached Properties Rulez!!!, cosa succede dietro le quinte:
- 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;
Come si vede dai due frammenti di xaml è possibile definire tutti i tipi di region che si desidera, nell’esempio vediamo una ContentPresenterRegion che è il tipo più semplice, può ospitare un contenuto alla volta e non ha altre funzionalità e vediamo anche una RibbonRegion che è facile intuire definisca un Ribbon in cui è possibile iniettare RibbonTab, quindi supporto per molti contenuti e funzionalità avanzate.
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<T> 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 ;-);
Le Region dinamiche introducono però alcuni problemi di gestione che quelle statiche non avrebbero, è possibile infatti registrare Region/RegionManager direttamente a runtime ma sarebbe anche molto importante deregistrare gli elementi creati al fine di tener sincronizzato il RegionService con lo stato attuale; non esistendo in WPF il concetto di Dispose non è possibile sfruttare quel momento per fare l’operazione di deregistrazione automatica, è quindi necessario fare una piccola acrobazia per capire, dal RegionService, quando un elemento, Window o Region Content che sia, viene chiuso e in quel momento scorrere il LogicalTree alla ricerca di elementi che siano delle view che abbiano registrato delle region al fine di deregistrarle. Vedremo approfonditamente nella prossima, ed ultima, puntata perchè questo è importante.
Allego il progetto allo stadio attuale: CompositeUI_v4.zip.
.m