Guest Post: MVVM e lo sviluppo cross-platform
Questo post è stato scritto da Matteo Pagani , Windows AppConsult Engineer in Microsoft
Nel corso dei post precedenti abbiamo parlato di come il pattern Model-View-ViewModel (da ora in poi MVVM) possa aiutare a rendere lo sviluppo di una Universal Windows app più agevole, rendendo il progetto più flessibile e facile da mantenere nel tempo. In realtà, il pattern MVVM può essere di grande aiuto anche nello sviluppo cross-platform, grazie a Xamarin, la tecnologia che consente di creare applicazioni per Android e iOS utilizzando C# e Visual Studio. Nel corso di questo post vedremo come, grazie all’utilizzo di questo pattern, saremo in grado di condividere buona parte del codice della nostra applicazione tra le versioni Windows, iOS e Android.
Prima di iniziare, assicuratevi di aver letto gli altri post dedicati all’implementazione del pattern MVVM e all’utilizzo della tecnica della dependency injection: sono concetti che ritorneranno spesso nel corso di questo post.
Servizi, servizi e servizi
Nel corso dei precedenti post abbiamo imparato come, in un progetto basato sul pattern MVVM, la logica e la gestione delle interazioni con l’utente venga spostata dal code-behind (che ha una stretta dipendenza con la View) al ViewModel (che, invece, è una semplice classe indipendente). Ipotizziamo, perciò, che sorga la necessità nella vostra applicazione di creare un comando, legato alla pressione di un pulsante, che mostri una finestra di dialogo all’utente. Basandoci su quanto abbiamo appreso negli altri post, il nostro ViewModel probabilmente avrebbe un comando definito in questo modo:
private RelayCommand _showDialogCommand;
public RelayCommand ShowDialogCommand
{
get
{
if (_showDialogCommand == null)
{
_showDialogCommand = new RelayCommand(async () =>
{
MessageDialog dialog = new MessageDialog("Hello world");
await dialog.ShowAsync();
});
}
return _showDialogCommand;
}
}
Ora ipotizziamo che il nostro committente ci chieda di portare l’applicazione che abbiamo sviluppato per Windows 10 anche sulle altre piattaforme mobile. Decidiamo di sfruttare Xamarin, così da massimizzare il riutilizzo di codice. In questo scenario, il nostro ViewModel ha un problema: dato che il comando fa uso di una classe specifica della Universal Windows Platform (la classe MessageDialog), non possiamo riutilizzarlo così com’è sulle altre piattaforme, in quanto utilizzano API differenti per gestire le finestre di dialogo.
La soluzione a questo problema è dato dai servizi: classi che si fanno carico di eseguire, in concreto, un’operazione, lasciando al ViewModel solamente la responsabilità di “descrivere” l’operazione da eseguire sfruttando un’interfaccia. Questo approccio ci porterà ad avere:
1. Un’interfaccia, che descrive solamente le operazioni che il servizio è in grado di offrire (nel nostro esempio, mostrare una finestra di dialogo). Tale interfaccia sarà usata dal ViewModel e ci permetterà di non avere alcun riferimento ad API specifiche della piattaforma. Questo file sarà memorizzato all’interno di un progetto condiviso tra tutte le piattaforme (ad esempio, tramite una Portable Class Library)
2. Un’implementazione concreta dell’interfaccia per ognuna delle piattaforme: sarà tale classe a sfruttare le API della piattaforma. Questo file, invece, sarà creato direttamente in ognuno dei progetti specifici per le varie versioni dell’applicazione.
Il primo passo, perciò, è quello di creare un’interfaccia comune, come nell’esempio seguente:
public interface IDialogService
{
Task ShowDialogAsync(string message);
}
Come potete vedere, l’interfaccia descrive solamente il servizio, il quale offrirà un metodo asincrono di nome ShowDialogAsync() che accetta, come parametro in ingresso, il messaggio da mostrare.
Il passaggio successivo è creare un’implementazione specifica di questa interfaccia per ognuna delle piattaforme che vogliamo supportare. Ad esempio, l’implementazione per la Universal Windows Platform sarà la seguente:
public class DialogService : IDialogService
{
public async Task ShowDialogAsync(string message)
{
MessageDialog dialog = new MessageDialog(message);
await dialog.ShowAsync();
}
}
L’implementazione, invece, per un progetto Xamarin Android sarà la seguente:
public class DialogService : IDialogService
{
public async Task ShowDialogAsync(string message)
{
AlertDialog.Builder alert = new AlertDialog.Builder(ActivityBase.CurrentActivity);
alert.SetTitle(message);
alert.SetPositiveButton("Ok", (senderAlert, args) =>
{
});
alert.SetNegativeButton("Cancel", (senderAlert, args) =>
{
});
alert.Show();
await Task.Yield();
}
}
Come vedete la firma del metodo è la stessa, ma viene implementato utilizzando le API specifiche di Android per mostrare finestre di dialogo, ovvero la classe AlertDialog.
A questo punto, possiamo riprendere i concetti appresi riguardo la dependency injection per gestire la dipendenza tra il servizio appena creato e il ViewModel. Il ViewModelLocator, nel costruttore, registrerà all’interno del container l’associazione tra l’interfaccia (presente nel progetto condiviso) e l’implementazione concreta (presente, invece, nel progetto specifico).
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IDialogService, DialogService>();
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
}
Il ViewModel, come conseguenza, includerà nel costruttore della classe un riferimento all’interfaccia che, a runtime, verrà automaticamente valorizzata dal container con la classe concreta che abbiamo registrato.
public class MainViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
public MainViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
}
A questo punto il nostro comando, anziché utilizzare direttamente la classe MessageDialog, sfrutterà il servizio che abbiamo appena creato.
private RelayCommand _showDialogCommand;
public RelayCommand ShowDialogCommand
{
get
{
if (_showDialogCommand == null)
{
_showDialogCommand = new RelayCommand(async () =>
{
await _dialogService.ShowDialogAsync("Hello world");
});
}
return _showDialogCommand;
}
}
Come potete notare, ora il ViewModel non ha più alcun riferimento ad API specifiche della piattaforma: di conseguenza, saremo in grado di spostarlo all’interno del progetto condiviso e riutilizzarlo anche con le versioni Android e iOS della nostra applicazione. Ovviamente, per continuare ad utilizzare le classi offerte da MVVM Light (come ViewModelBase o RelayCommand), dovrete installare la libreria tramite NuGet anche nel progetto condiviso.
Sfruttare il pattern MVVM nelle altre piattaforme
Nel momento in cui vogliate sfruttare il ViewModel appena creato in un’applicazione Xamarin Android o Xamarin iOS, vi scontrerete però con un problema: il pattern MVVM è fortemente basato sul binding, che è però una caratteristica esclusiva dello XAML e della Universal Windows Platform. Android e iOS non hanno questo concetto e, di conseguenza, non abbiamo modo di legare i controlli nella UI alle proprietà o ai command nel nostro ViewModel.
Per fortuna, ci viene in aiuto ancora una volta MVVM Light che, nelle ultime versioni, ha aggiunto la compatibilità anche con le altre piattaforme, integrando un meccanismo per supportare il binding anche sulle piattaforme che non lo supportano nativamente.
Vediamo come sfruttarlo, prendendo come esempio un progetto Xamarin Android, anche se lo stesso meccanismo può essere utilizzato con iOS. Per implementare gli esempi seguenti, è indispensabile:
1. Aggiungere al progetto Android un riferimento alla libreria condivisa che contiene le interfacce e i ViewModel creati in precedenza.
2. Installare tramite NuGet la libreria MVVM Light anche nel progetto Android.
Il ViewModelLocator
Anche nel progetto Android possiamo sfruttare il concetto di ViewModelLocator: la differenza è che, dato che in questo caso non abbiamo un corrispettivo della classe App, all’interno della quale registrarlo come risorsa globale, lo definiamo come classe statica. A parte questo, non c’è nessuna differenza implementativa: in fase di inizializzazione, si fa carico di registrare all’interno del container l’interfaccia IDialogService e di associarla all’implementazione concreta (in questo caso, quella specifica per Android).
public static class ViewModelLocator
{
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IDialogService, DialogService>();
SimpleIoc.Default.Register<MainViewModel>();
}
public static MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
}
Qualche concetto base di Xamarin Android
Non è questa la sede per approfondire lo sviluppo per Android con Xamarin: per questo, vi rimando al sito ufficiale per gli sviluppatori (http://developer.xamarin.com), che contiene molta documentazione e risorse. Per il nostro scenario, vi basti sapere che Android sfrutta il concetto di Activity per definire quelle che, in Windows, sono le pagine dell’applicazioni.
Ogni progetto Xamarin Android include, perciò, di default una classe chiamata MainActivity.cs, che rappresenta fondamentalmente il “code behind” della pagina principale dell’applicazione. Per la definizione dell’interfaccia grafica, Android sfrutta un approccio simile a quello di Windows, sfruttando un file XML (in questo caso, chiamato AXML) che contiene i vari controlli della pagina.
Se apriamo il file MainActivity.cs troveremo, ad un certo punto, il seguente blocco di codice:
[Activity(Label = "MVVMLight.Services.Android", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity: Activity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
}
}
Il metodo OnCreate() viene scatenato quando viene inizializzata la pagina. Al suo interno, viene utilizzato il metodo SetContentView() per specificare qual è il file di layout che sarà legato a questa classe. Nell’esempio, è definito dalla costante Resource.Layout.Main: ciò significa che, nel nostro progetto, troveremo un file di nome Main.axml all’interno del percorso Resource/Layout. Ecco la sua definizione:
<?xmlversion="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="@+id/MyButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/Hello" />
</LinearLayout>
Anche in questo caso non entreremo nel merito della definizione dell’AXML. Ci basti sapere che la pagina contiene un pulsante (identificato dal controllo Button) a cui è associato l’identificativo MyButton (definito tramite la proprietà android:id). È il pulsante che vogliamo associare al nostro comando del ViewModel e che dovrà mostrare a video la finestra di dialogo.
Binding e command
Dato che Android non supporta il concetto di binding, possiamo sfruttare l’activity e MVVM Light per implementarlo. Il primo passo è avere, all’interno dell’activity, un riferimento al ViewModel che vogliamo collegare. Nel nostro caso, si tratta della classe MainViewModel che si trova nel progetto condiviso, che possiamo recuperare tramite il ViewModelLocator definito in precedenza:
[Activity(Label = "MVVMLight.Services.Android", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : ActivityBase
{
public MainViewModel ViewModel => ViewModelLocator.Main;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
}
}
Prima di poter utilizzare i metodi offerti da MVVM Light, dobbiamo però effettuare un cambiamento nella definizione dell’activity: non deve più ereditare dalla classe base Activity, ma da ActivityBase, che ci mette a disposizione il toolkit di Laurent Bugnion.
Il secondo passo è quello di recuperare un riferimento al pulsante che abbiamo incluso nell’interfaccia grafica. L’activity di Android non si comporta come il code behind dello XAML e, di conseguenza, non ci espone direttamente i controlli che abbiamo aggiunto nel file AXML: dobbiamo prima sfruttare il metodo FindViewById<T>() per recuperarne un riferimento. Dopodiché, siamo finalmente in grado di sfruttare le funzionalità di MVVM Light per collegare il comando al controllo. Ecco l’esempio completo:
[Activity(Label = "MVVMLight.Services.Android", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : ActivityBase
{
public MainViewModel ViewModel => ViewModelLocator.Main;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
// Get our button from the layout resource,
// and attach an event to it
Button button = FindViewById<Button>(Resource.Id.MyButton);
button.SetCommand("Click", ViewModel.ShowDialogCommand);
}
}
Applichiamo il metodo SetCommand() al controllo, passando come parametri:
1. Una stringa con il nome dell’evento che vogliamo sottoscrivere. Nel nostro caso, è l’evento Click.
2. Il comando del ViewModel che vogliamo eseguire, nel nostro caso ShowDialogCommand.
Il gioco è fatto: se ora lanciassimo l’applicazione e premessimo il pulsante, vedremmo comparire la finestra di dialogo.
E se volessimo gestire il binding con le proprietà del ViewModel, anziché con i command? Nel nostro caso non c’è n’è stato bisogno, ma ipotizziamo che il nostro ViewModel abbia delle proprietà che vogliamo mostrare all’interno dell’interfaccia grafica (ad esempio, una proprietà di tipo string collegata ad un controllo TextView, che è il corrispettivo del controllo TextBlock in Android).
Possiamo sfruttare un altro metodo, di nome SetBinding() , che produce un oggetto di tipo Binding che dobbiamo definire come variabile globale dell’activity, affinchè il canale di binding venga sempre mantenuto attivo. Ipotizzando di aver aggiunto, all’interno del ViewModel, una proprietà di nome Message così definita:
private string _message;
public string Message
{
get { return _message; }
set { Set(ref _message, value); }
}
Ecco come possiamo collegarla ad un controllo di tipo TextView aggiunto nel file AXML:
[Activity(Label = "MVVMLight.Services.Android", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : ActivityBase
{
public MainViewModel ViewModel => ViewModelLocator.Main;
private TextView _messageView;
public TextView MessageView
=> _messageView ?? (_messageView = this.FindViewById<TextView>(Resource.Id.MessageView));
private Binding _messageBinding;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
_messageBinding = this.SetBinding(() => ViewModel.Message, () => MessageView.Text, BindingMode.OneWay);
}
}
Un primo passaggio importante da sottolineare è il modo in cui viene recuperato un riferimento al controllo TextView: tramite una proprietà definita a livello di Activity e non più chiamando il metodo FindViewById<T>() direttamente nel metodo OnCreate() . Senza questo accorgimento otterremo un errore in fase di esecuzione, in quanto il riferimento al controllo verrebbe distrutto al termine della creazione dell’activity.
Quello che facciamo, però, all’interno del metodo OnCreate() è definire il canale di binding, tramite il metodo SetBinding() che richiede, come parametri:
1. La proprietà del ViewModel che vogliamo collegare.
2. Il riferimento al controllo e alla proprietà che vogliamo gestire.
3. Opzionalmente, il tipo di binding. Esattamente come nello XAML, come impostazione predefinita il canale viene creato in modalità OneWay.
Il gioco è fatto. Se ora cambiassimo il nostro comando per valorizzare la proprietà Message anziché mostrare una finestra di dialogo, vedremmo comparire il messaggio direttamente all’interno della pagina.
private RelayCommand _showDialogCommand;
public RelayCommand ShowDialogCommand
{
get
{
if (_showDialogCommand == null)
{
_showDialogCommand = new RelayCommand(() =>
{
Message = "The button has been clicked";
});
}
return _showDialogCommand;
}
}
In conclusione
Nel corso di questo post abbiamo imparato come il pattern MVVM non sia soltanto un valido alleato nel progettare al meglio la nostra Universal Windows app, ma come ci consenta, con maggiore facilità, di riutilizzare il nostro codice anche su altre piattaforme. In questo caso abbiamo preso l’esempio di un progetto Android basato su Xamarin, ma poteva trattarsi, ad esempio, di un’applicazione WPF o Silverlight. Potete trovare il progetto di esempio utilizzato in questo post sul mio repository GitHub https://github.com/qmatteoq/UWP-MVVMSamples