Ancora su Silverlight 4 ed il caricamento del ViewModel dinamico con MEF
Rispetto al mio post precedente, le cose si sono fatte più serie. Nel tentativo di tranquillizzare Roberto :-), e nel tentativo di dare una risposta a Corrado, mi sono immaginato uno scenario più “complessoâ€, che riassumo così:
- L’applicazione Silverlight parte: la UI è minimale, ed il corrispondente ViewModel sottostante espone un sottoinsieme limitato di funzionalitÃ
- Appena dopo l’avvio, MEF comincia a fare il suo sporco lavoro, risolve le dipendenze ed inietta nell’applicazione il ViewModel reale
Adesso vediamo di rendere più chiare le cose. Supponiamo di avere un’app Silverlight che nel browser si presenta in questo modo:
Nulla di particolarmente evoluto, no? Quella che vedete qui sopra è contenuto in MainPage.xaml. Ho aggiunto nelle Resources di questo UserControl un’istanza di InitialViewModel:
<UserControl.Resources>
<ivm:InitialMainViewModel x:Key="InitialViewModel" />
</UserControl.Resources>
La classe InitialViewModel ha le seguenti caratteristiche:
- Eredita da Galasoft.MvvmLight.ViewModelBase
- Implementa l’interfaccia IMainViewModel, che è definita così:
public interface IMainViewModel
{
string Message { get; set; }
RelayCommand NewCommand { get; set; }
RelayCommand ListCommand { get; set; }
RelayCommand MailCommand { get; set; }
RelayCommand LoginCommand { get; set; }
}
La stringa Message è quella che vedete sulla UI – “Attendere prego…inizializzazione in corso!â€. I quattro command sono bindati ai quattro pulsanti che vedete sulla UI.
Piccola precisazione: l’idea è quella secondo la quale l’applicazione parte con un ViewModel minimale, così l’utente non vede una pagina bianca, ma può comunque interagire in qualche modo. Per questo motivo, all’inizio se l’utente clicca su uno qualsiasi dei pulsanti, appare il seguente messaggio:
L’unica differenza è la pressione del pulsante Login. Questo pulsante comincia a far lavorare MEF. L’execute del LoginCommand è definito come segue:
void loginCommandExecute()
{
ViewModelLocator.Current.ResolveDependencies();
}
In pratica, quindi, la pressione di New/List/Mail dice all’utente di attendere, mentre la pressione di Login fa qualcosa di diverso. E qui entra in ballo ancora il ViewModelLocator, che oggi ho reso singleton, e ne invoco il metodo ResolveDependencies, che fa quanto segue:
public void ResolveDependencies()
{
Uri uri = new Uri(this.Parameters["ViewModelAssembly"]);
WebClient client = new WebClient();
client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
client.OpenReadAsync(uri);
}
Leggo un Uri dal web.config (secondo la tecnica descritta qui), e comincio il download asincrono. Quando il download è completato…
void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null) return;
AssemblyPart assemblyPart = new AssemblyPart();
Assembly pluginAssembly = assemblyPart.Load(e.Result);
AssemblyCatalog ac = new AssemblyCatalog(pluginAssembly);
CompositionContainer container = new CompositionContainer(ac);
container.SatisfyImportsOnce(this);
}
L’Uri deve puntare ad un assembly .NET, che deve contenere una classe che implementa IMainViewModel. Quello che accade qui è che l’assembly scaricato viene caricato in un’istanza di Assembly, poi viene creato un AssemblyCatalog. Alla fine viene invocato il metodo SatisfyImportsOnce del CompositionContainer, che non fa nient’altro che fare il match tra [Export] ed [Import] di MEF. Viene quindi risolta la dipendenza della proprietà Main del ViewModelLocator:
[Import]
public IMainViewModel Main
{
get { return _mainViewModel; }
set
{
_mainViewModel = value;
UserControl control = Application.Current.RootVisual as UserControl;
injectViewModel(control, _mainViewModel);
}
}
Quindi, una volta che il download dell’assembly è terminato, non faccio altro che prende lo UserControl principale dell’applicazione, e vado a reimpostarne il DataContext, attraverso la chiamata a injectViewModel(UserControl, object).
Morale: il messaggio ed i pulsanti sulla UI non dicono più di attendere, ma grazie al nuovo ViewModel iniettato da MEF dinamicamente, cominciano a “funzionare†veramente:
Punti di forza di questo approccio:
- Supponiamo che l’assembly scaricato sia http://www.mywebsite/viewmodel/viewmodel_10.dll – ebbene, nessuno degli assembly che compone l’applicazione principale ha un riferimento a questa dll – e ci mancherebbe!
- Modificando il web.config posso determinare quale assembly viene scaricato, e quindi di fatto assegnare diversi ViewModel , e quindi comportamenti diversi. Mi spiego meglio: nell’esempio qui sopra, il NewCommand del nuovo ViewModel non fa altro che mostrare una MessageBox con “New!â€. Poco utile! 🙂 Ma basta scrivere una nuova classe che implementi IMainViewModel, deployarla da qualche parte, modificare il web.config e di fatto si deploya una nuova applicazione – UI sempre uguale, ma comportamento differente.
- L’idea finale non è tanto che l’utente debba premere Login per risolvere le dipendenze – in realtà , tutto questo meccanismo potrebbe partire anche all’avvio. Quindi di fatto l’utente raggiungere l’url, aspetta qualche secondo per il download della dll e vede la UI refresharsi adeguandosi al nuovo ViewModel.
- Per rispondere esplicitamente a Corrado, cosa mi impedisce di mettere nel ViewModelLocator una bella new() che ritorna l’istanza di IMainViewModel? Diverse cose: una new() richiede una reference esplicita all’assembly, annullando di fatto tutti i vantaggi descritti prima.
- Supporto a design-time? Il ViewModelBase di Galasoft.MvvmLight espone una proprietà IsInDesignTime, che posso utilizzare per capire se sono in IDE oppure no. E poi c’è un altro vantaggio: l’applicazione a design-time vede InitialViewModel, e quindi di fatto a design-time gira questo viewmodel. Tant’è che se apro la MainPage.xaml in Visual Studio 2010, vedo quanto segue:
Quindi anche a design-time ho un ViewModel attivo a tutti gli effetti (InitialViewModel), che è quello che verrà sostituito a run-time da MEF.
Che ne dite?
Il codice è liberamente scaricabile da qui (406Kb).