Nel seguito fornirò alcuni dettagli per implementare in modo corretto il navigator usando il framework FreshMVVM.
La "spieghescion" non sarà assolutamente esaustiva, e tantomeno il mio intento è quello di esporre in modo completo tutte le funzionalità offerte dal framework FreshMVVM.
Lo scopo è semplicemente quello di fare un "crash-course" che in pochi passaggi permettano di appropriarsi delle tecniche necessarie per implementare compiutamente un navigator usando FreshMVVM: daltronde la documentazione ufficiale è molto dettagliata, e per saperne di più Vi rimando ai link segnati in linkografia.
Il primo punto da sapere è che il framework si basa su una navigazione ViewModel2ViewModel, nel senso che quando si intende aprire una View (=pagina) in realtà si richiamerà la ViewModel ad esso associata: mai a poi mai si farà cenno direttamente alla pagina.
Oss.: L'attività di apertura di un nuova View è chiamata anche push, e con questo termine sarà identificata nel seguito.
Questo tipo di logica i funzionamento è assolutamente compatibile con il pattern MVVM: infatti a livello di ViewModel è assolutamente e solo permesso interagire con altre classi di ViewModel.
Ma come fa il framework ad aprire la pagina corretta se si specifica solo il relativo ViewModel ?
Esiste una naming-convention che occorre seguire religiosamente e che permette al framework di risalire alla View data la ViewModel, e viceversa.
Tali convenzione si basa sul fatto che ogni classe ViewModel dovrà avere il proprio nome che termina con il suffisso ViewModel, mentre la relativa pagina View avrà analogo nome però questa volta con il suffisso Page.
AnagraficaClientiViewModel.cs → AnagraficaClientiPage.xaml
Al posto di ViewModel è anche possibile usare PageModel
AnagraficaClientiPageModel.cs → AnagraficaClientiPage.xaml
Ogni ViewModel dovrà necessariamente derivare dalla classe FreshBasePageModel.
Questa classe è il cuore del framework, e dispone di una proprietà CoreMethods, che è il motore di tutto.
Questa proprietà viene automaticamente inizializzata dal framework quando il ViewModel viene instanziato dal framework, e permette di accedere a tutte le funzioni come push di nuove pagine, pop, lancio di maschere modali, etc etc
Dunque ecco un primo esempio: per aprire una nuova pagina agendo da dentro un ViewModel occorrerà usare il seguente comando.
await this.CoreMethods.PushPageModel<FocacciaPostEditViewModel>();
Il comando instanzierà la classe FocacciaPostEditViewModel nonché la pagina FocacciaPostEditPage: quindi provvederà anche a visualizzare quest’ultima e automagicamente associerà anche il suo BindingContext all’istanza del ViewModel.
Oss.: per scongiurare errori strani e incomprensibili occorre che il ViewModel e la pagina coesistano nello stesso Namespace.
Quando si inzializza unclasse ViewModel occorre essere in grado di inizializzarne correttamente lo stato interno (preparare le variabili da esporre nelle proprietà per essere visualizzate dalla pagina, inizializzare variabili interne, etc etc).
Sempre la stessa classe FreshBasePageModel propone un metodo che è stato approntato allo scopo: virtual Init.
public virtual void Init(object initData);
E’ tramite questo che è possibile instanziare nel modo corretto la ViewModel. E' possibile osservare che il metodo propone anche un parametro initData.
Per sfruttare l'utilizzo di tale parametro, quindi per inviare dall'esterno dei valori che servano alla ViewModel di inizializzare correttamente il suo stato interno, la chiamata PushPageModel ha due possibili overloading.
Task PushPageModel() where T : FreshBasePageModel;
Task PushPageModel(object data, bool modal = false) where T : FreshBasePageModel;
Usando il secondo overloading è possibile specificare un parametro, che si ritroverà come argomento del citato metodo Init della ViewModel richiamata.
L'altro parametro permetterà di specificare se si vuole richiamare la oagina come modale o meno.
E per passare degli argomenti al costruttore del ViewModel ? Cioè in tutti i casi pratici ci sarà sempre da passare nel costrutture un’istanza di una qualche classe di servizi, per esempio il repository per l’accesso al database. Ovviamente la cosa può essere fatta come sopra, con PushPageModel e Init, ma il framework permette un approccio più efficace.
E' possibile definire nello startup dell'applicazione (app.cs) un'associazione interfaccia/classe, con l'ausilio di Container.Register.
FreshIOC.Container.Register<IFocacBookRepository, FocacBookRepository>();
Così facendo il framework tutte le volte che instanzierà un ViewModel verificherà la presenza dei parametri da fornire al costruttore, e se troverà che questo necessita di un'interfaccia tra quelle dichiarate in modo automatico passerà un'istanza del "concrete object" specificato. Penso che il codice a corredo sia abbastanza esplicativo.
In termini più precisi il framework permette di implementare il pattern IoC Inversion of Control, nella variante Constructor injection.
Prima di scrivere il primo unit-test…..
In Xamarin c’è la grossa rogna che se si esegue qualche chiamata a qualche funzione fornita da Xamarin, si ottiene un errore come nel seguito.
System.InvalidOperationException : You MUST call Xamarin.Forms.Init(); prior to using it.
L’errore è bloccante per cui non permette l’implementazione di alcun unit-test. Questo accede perché alcune parti del framework non sono correttamente inizializzate. Anche se ci se ne dimentica facilmente Xamarin, per poter funzionare correttamente, necessita che venga inizializzato tramite una chiamata apposita.
Xamarin.Forms.Forms.Init();
Questa chiamata deve essere direttamente fatta dalla piattaforma di utilizzo del framework (piattaforma supportata da Xamarin)
- Android: è in in MainActivity.cs - Xamarin.Forms.Forms.Init(this, bundle)
- iOS: è in AppDelegate.cs - Xamarin.Forms.Forms.Init();
Ora non è possibile in alcun modo chiamare questo Init anche dal progetto di unit test (Windows non è una piattaforma supportata): per risolvere il problema esistono diverse soluzioni, ma la più semplice è basarsi sul pacchetto NuGet Xamarin.Forms.Mocks.
Oss.: Affinchè tutto funzioni occorre installare la versione di Xamarin.Forms.Mocks pari o inferiore a quella di Xamarin.Forms. Per esempio se si sta usando Xamarin.Forms 3.5.0.XXX allora occorrerà utilizzare Xamarin.Forms.Mocks versione 3.5.0
Una bella chiamata nel costruttore di ogni classe che contiene gli unit test risolve brillantemente il problema dell’errore improvvido esposto sopra.
Xamarin.Forms.Mocks.MockForms.Init();
Finalmente il primo unit-test al gusto Xamarin !
In questo codice sono presenti due test: uno atto a simulare l’apertura della nuova pagina una volta azionato il bottone nuovo, e l'altro per simulare la selezione di un elemento della lista, con apertura del relativo dettaglio.
La prima parte riguarda il setup del mock del repository: l’oggetto NON restituisce alcun item (la lista è vuota).
Successivamente si simula l’azionamento del bottone nuovo, eseguendo il command associato allo stesso stesso (AggiungiPostFocacciaPostCommand).
Questa azione deve aprire una nuova maschera: per stabilire correttamente i metodi da passare si utilizza un oggetto mock.
Il secondo test simula la pressione del primo elemento di una lista, che apre il relativo dettaglio.
Linkografia
Informatica pressapochista - Un viaggio negli unit-test: Introduzione - Prima parte
Informatica pressapochista - Un viaggio negli unit-test: xUnit e dintorni – Seconda parte
Informatica pressapochista - Un viaggio negli unit-test: Fake class e dintorni - Terza parte
Informatica pressapochista - Un viaggio negli unit-test: Repository pattern - Quarta parte
Informatica pressapochista - Un viaggio negli unit-test: Non male le fake class.... – Quinta parte
Informatica pressapochista - Un viaggio negli unit-test: Un viaggio negli unit-test: Ci serve Moq ? – Sesta parte
Informatica pressapochista - Un viaggio negli unit-test: E in Xamarin ? Con FreshMVVM ! – Settima parte
Git Hub Page InformaticaPressapochista
Brian Lagunas - Mocking and Unit Testing the Xamarin.Forms Application class
Jonathan Peppers - Mocking Xamarin.Forms
GitHub - FreshMvvm