Continuiamo
Siamo ancora ad un livello introduttivo, cerchiamo di capire quali sono le problematiche tecniche che dovremo affrontare e perchè.
Dogma: Diamoci delle regole e rispettiamole.
Nello sviluppo di applicazioni complesse, e comunque in generale nell’applicazione di un pattern, non abbiamo nessun supporto dall’ambiente di sviluppo, questo significa, ad esempio, che il compilatore (san csc.exe :-D) non ci aiuta in nessun modo segnalandoci che stiamo facendo una certa cosa nel modo sbagliato. Abbiamo quindi bisogno di capire quali sono i problemi, trovare una soluzione che ci permetta di rispettare i requisiti e poi non deviare dal seminato, perchè è facile farsi prendere dalla fretta e “appiccicare” una soluzione che poi inevitabilmente alla lunga ci si ritorcerà contro. Soprattuto ora in cui l’obiettivo è costruire un toolkit che sia riutilizzabile.
UI Design
La prima cosa che facciamo è disegnare un concept della UI che vogliamo realizzare, è irrilevante che funzioni o meno ci basta avere qualcosa da far vedere al cliente e qualcosa da analizzare per capire quali possono essere le problematiche. In questo senso wpf/xaml unito a Expression Blend sono una vera manna.
Il nostro obiettivo è arrivare qui:
image
Nell’immagine ho evidenziato:
  • in viola le aere (Region) che la Shell mette a disposizione per fare UI Injection;
  • in rosso la Region che il dettaglio dell’elemento selezionato mette a disposzione per la UI Injection;
Quello che vediamo nell’immagine è così strutturato:
  • Ogni Ribbon Tab viene iniettata, ma non è una regola, da un modulo in una RibbonRegion esposta dalla Shell. Ogni Ribbbon Tab, come del resto ogni elemento della UI è formato sempre da una coppia View/ViewModel. Nell’immagine vediamo:
    • SubjectsManagerView;
    • SubjectsManagerViewModel;
  • a seguito di una ricerca vengono visualizzati i risultati in una DocumentWindow iniettata in una DocumentsRegion offerta dalla Shell;
  • Se eseguite una seconda ricerca, senza selezionare la check-box, la DocumentWindow viene riutilizata per visualizzare i risultati;
Quello che vediamo graficamente può essere schematizzato così:
image
E’ evidente che la schematizzazione ci fa capire che le cose dietro le quinte sono un filino complesse, e M-V-VM non ci aiuta certo nella gestione, nonostante da un lato semplifichi notevolmente dall’altro introduce un ulteriore livello di complessità.
Inoltre è importante notare che l’esempio schematizzato è di quelli semplici perchè il nesting delle region, e della conseguente injection di elementi, potrebbe andare avanti all’infinito e nell’applicazione che stiamo realizzando è effettivamente un filino più complesso di questo concept.
Nello schema ho anche evidenziato quali sono le uniche zone di collegamento note tra i vari attori: solo ed esclusivamente il data binding tra View e ViewModel, nulla di più.
Per come funziona il modello è evidente che non è possibile avere una reference a SubjectsManagerViewModel ad esempio da SubjectsSearchResultsViewModel, da questo se ne deduce che non ha senso esporre un evento (eg. SearchRequest) da SubjectsManagerViewModel e agganciarvi un handler da SubjectsSearchResultsViewModel ad esempio per eseguire la ricerca.
Questo ci pone di fronte al primo problema.
La comunicazione
l'esempio che abbiamo appena sfiorato ci impone di trovare un sistema per far comunicare le varie parti del nostro ecosistema in maniera disaccoppiata, senza usare eventi.
La scelta che ho fatto è stata quella di optare per un sistema di messaggistica interno all’applicazione basato sul modello publisher/subscriber. Ne ho già parlato su questi schermi quindi direi che questo paragrafo lo possiamo dare per assodato.
Entrambi i 2 blasonati toolkit che ho preso in considerazione affrontano e risolvono il prolema in maniera molto simile.
Quello che succede quindi nella nostra UI è che alla pressione del pulsante cerca viene inviato un messaggio che contiene la richiesta di ricerca, questo messaggio viene “gestito da un handler”, ne parleremo, che altro non fa che recuperarare una reference al ViewModel che visualizza i risultati, passargli le informazioni sulla ricerca inserite dall’utente e invocare l’esecuzione della ricerca.
Per fare tutto ciò e non impazzire la soluzione ideale è utilizzare un framework di Inversion of Control…
Inversion of Control e Dependency Injection
Castle Windsor, ma il discorso vale anche per gli altri (Prism di default usa Unity ma è possibile rimpiazzarlo), ci permette di semplificare, centralizzare e configurare la gestione delle dipendenze tra i vari componenti all’interno di ogni modulo e tra i moduli stessi volendo.
L’introduzione di questo modello ci porta a poter realizzare qualcosa di decisamente interessante, questo snippet di codice ci pone uno scenario molto interessante:
namespace ModuleA
{
   class DoSomethingInterestingViewModel
   {
      void Method()
      {
         var env = this.GetService();
         if( env.IsModuleRunning<ModuleB.IModule>() )
         {
            var svc = this.GetService<ModuleB.IOtherServiceInOtherModule>();
            svc.BlaBla();
         }
      }
   }
}

da un ipotetico modulo A possiamo accedere ad informazioni gestite da un ipotetico modulo B, la cosa è decisamente fattibile, funziona e vi permette scenari molto ma molto interessanti… ma…
Il processo di Boot
Se abbiamo bisogno di soddisfare lo scenario appena visto ci troviamo di fronte alla magagna che il container di IoC deve essere uno ed uno solo e in fase di boot dell’applicazione ogni modulo deve avere la possibilità di configurare il sistema per far si che possa rispondere alle proprie esigenze e eventualmente a quelli di altri.
Questo ci porta ad un processo di boot fatto da una serie di stage:
  • Discovery: durante la fase di discovery vengono cercati tutti i moduli installati e vengono caricati dei “proxy” (ModuleDescriptor) che descirvono i moduli;
  • Initialize: tutti i moduli che dovranno effettivamente essere caricati/utilizzati passano da una fase di intialize in cui hanno la possibilità di accedere alla configurazione del container e iniettare la propria parte di configurazione;
  • Startup: i moduli inizializzati vengono a tutti gli effetti avviati dando inizio alle danze;
I Moduli
Un modulo è fatto come minimo da 3 parti:
  1. ModuleDescriptor: rappresenta un modulo, fornisce informazioni descrittive ed è in grado di “trasformarsi” in un ModuleBootstrapper; Lo scopo di un ModuleDescriptor è quello di avere informazioni riguardo ad un modulo senza necessariamente avere un riferimento a quel modulo e quindi senza dover caricare in memoria nulla che riguardi quel modulo;
  2. Un ModuleDescriptor all’occorrenza può diventare un ModuleBootstrapper che è quel componente, a questo punto specializzato per il modulo che stiamo caricando, in grado di inizializzare la configurazione del modulo stesso.
  3. Il processo di bootstrap porta alla creazione di un’istanza di un IModule che è a tutti gli effetti il nostro modulo, un IModule ha un solo metodo Run() che verrà invocato al momento giusto dall’engine di boot.
Quello che succede durante il processo di avvio ad esempio è una cosa del tipo:
  1. Il processo di boot viene avviato;
  2. viene eseguita la configurazione del container da parte della Shell;
  3. viene fatto il discovery dei moduli:
    1. vengono caricati i bootstrapper;
    2. viene data la possibilità ai bootstrapper di partecipare al processo di configurazione;
    3. vengono avviati i moduli;
  4. la Shell si avvia definitivamente e:
    1. registra, vedremo come, le region che saranno disponibili ai moduli;
    2. notifica, via message broker, al mondo che si sta avviando;
  5. Ogni modulo ha l’opportunità di reagire (message Subscription) all’avvio della Shell:
    1. può recuperare un riferimento alle region “pubblicate” dalla Shell;
    2. può iniettare contenuti, ad esempio nel RibbonTab, nella Shell;
Alla pressione del pulsante “Cerca” succede una cosa molto simile, quindi per ora passo.
Dynamic Region(s)
Ma… maledetti ma ;-)… una cosa che deve essere possibile fare è questa:
image
eseguire cioè 2 ricerche in 2 finestre distinte (qui le ho affiancate sfruttando una fantastica suite di controlli di un collega MVP: Tim Dawson). Questo porta ad un altro piccolo problema: le Region non possono essere definite a priori (come ad esempio fa PRISM v1) in maniera statica perchè se definissi una generica Region “SelectedSubjectDetails” non saprei come fare a gestire le 2 istanze di cui sopra.
Inizialmente ero quindi partito definendo il concetto di region Statiche, che esistono (punto) e possono essere recuperate per nome, e di region dinamiche di cui poteva essere fatto il discovery a runtime. Alla fine era troppo complesso da gestire e anche da usare in realtà se ci pensiamo possiamo asserire che tutte le region sono dinamiche solo che alcune hanno la caratteristicha di esistere per tutto il ciclo di vita dell’applicazione proprio come se fossero dei singleton, non lo sono ma il concetto calza a pennello.
Il tutto adesso è gestito da una triade:
  • RegionService: è il motore che consente la registrazione delle Region e la gestione dei RegionManager, c’è un solo RegionService per tutti;
  • RegionManager: è il componente che si occupa di gesire un insieme di region, i RegionManager possono essere creati, distrutti e manipolati, sono l’entry point per avere accesso alle region. Nel mio modello se una View definisce delle region esisterà un RegionManager per quella View; non è possibile, e non ne vedo la necessità, avere più RegionManager collegati ad una singola View;
  • Region: è la definizione astratta di un’area in cui è possibile iniettare contenuti, ogni Region ha le sue regole ed è la Region stessa che decide come visualizzare e gestire i contenuti che vengono iniettati; alcuni esempi:
    • Region: è un tipo di region pensato per ospitare RibbonTab(s);
    • Region: un tipo di Region pensato per ospitare DocumentWindow(s);
    • Region: un tipo di region pensato per ospitare un singolo contentuo figlio;
    • Region…;
Un RegionManager è naturalmente in grado di gestire Region tra loro eterogenee.
Navigation System
Ci troviamo infine nella terra di nessuno, sia Caliburn che Prism fanno acqua in questo senso, ci rendiamo cioè conto che la nostra applicazione non è fatta da una singola finestra… sarebbe bello, ma su quale pianeta ;-)
Quello che vogliamo poter fare è, ad esempio:
  • fare doppio click su un risultato di una ricerca e aprire una Window che ci visualizzi l’elemento nella sua interezza e, ad esempio, in lettura/scrittura;
  • nel momento in cui facciamo doppio click su un elemento che stiamo già visualizzando in un’altra Window semplicemente portare quella Window in primo piano;
Tutto questo viene delegato ad un NavigationService che è quel componente che è in grado di orchestrare i rapporti tra le varie Window, non sa nulla delle Window stesse ma le conosce quel tanto che basta per poterci dialogare.
And now…
Anche stavolta di carne al fuoco ne abbiamo messa parecchia, nelle prossime puntate cercheremo di affrontare ogni singolo punto nel dettaglio che più dettaglio non si può :-). Ho anche creato una categoria ad hoc.
Spero che il tempo tiranno mi permetta di continuare questa serie senza troppe pause di riflessione ;-)
.m