Guest post: Il pattern MVVM nelle Universal Windows app - Concetti avanzati

Questo post è stato scritto da Matteo Pagani , Support Engineer in Microsoft per il programma AppConsult

Continuiamo il nostro viaggio alla scoperta del pattern Model-View-ViewModel. In questo post vedremo alcuni concetti avanzati ma comunque di uso frequente: la gestione degli eventi secondari, lo scambio di messaggi e l’uso del dispatcher.

 

La gestione degli eventi

In uno dei post precedenti abbiamo visto come il framework ci metta a disposizione l’interfaccia ICommand e la proprietà Command, esposta da molti controlli, per poter collegare un’azione ad un evento senza passare da un event handler, che ha il difetto di avere una stretta dipendenza con la View, in quanto può essere definito solo nella classe di code-behind.

La proprietà Command, però, permette di gestire solamente l’evento principale esposto dal controllo. Ad esempio, nel caso del controllo Button, ci permette di gestire il tap. Esistono però tanti scenari in cui abbiamo la necessità di gestire anche eventi secondari. Ad esempio, il controllo ListView espone un evento chiamato SelectionChanged che viene sollevato quando l’utente seleziona un elemento dalla lista. Oppure la classe Page espone un evento chiamato Loaded che viene sollevato quando la pagina è stata caricata.

Per gestire queste situazioni ci viene in aiuto la Behaviors SDK, ovvero una libreria di Microsoft (che viene installata insieme a Visual Studio) che contiene una collezione di behavior già pronti per essere utilizzati. I behavior sono una funzionalità dello XAML che permette di incapsulare della logica, che tipicamente sarebbe espressa nel code-behind, all’interno di componenti riusabili nello XAML. L’uso di behavior è molto diffuso in ottica MVVM, dato che riducono la necessità di scrivere codice all’interno del code-behind.

I behavior sono basati su due concetti:

1. Trigger, ovvero l’azione che fa scatenare l’esecuzione del behavior.

2. Action, ovvero l’azione da eseguire quando viene applicato il behavior.

La Behaviors SDK include una coppia di trigger / action che nasce proprio per gestire il nostro scenario, ovvero poter collegare dei comandi nel ViewModel ad eventi secondari.

Vediamo un esempio reale: il primo passo è quello di aggiungere, all’interno del vostro progetto Visual Studio, un riferimento all’SDK. Fate clic con il tasto destro sul progetto e scegliete Add reference: troverete la voce Behaviors SDK nella sezione Universal Windows -> Extensions.

Dopo averla aggiunta, dovrete dichiarare i namespace dell’SDK indispensabili per utilizzare i behavior, ovvero Microsoft.Xaml.Interactivity e Microsoft.Xaml.Interactions.Core, come nell’esempio seguente:

 

<Page

   x:Class="MVVMLight.Advanced.Views.MainView"

   xmlns:interactivity="using:Microsoft.Xaml.Interactivity"

   xmlns:core="using:Microsoft.Xaml.Interactions.Core"

   mc:Ignorable="d">

</Page>

 

A questo punto avrete la possibilità di usare la classe EventTriggerBehavior: si tratta di un behavior che permette di collegare un trigger ad un evento esposto da un controllo. In accoppiata, andremo ad utilizzare un’altra classe di nome InvokeCommandAction, che permette invece di collegare un command dichiarato nel ViewModel all’evento gestito dal trigger.

Ecco come appare la dichiarazione completa di questo behavior nello XAML:

 

<ListView ItemsSource="{Binding Path=News}" SelectedItem="{Binding Path=SelectedFeedItem, Mode=TwoWay}">

    <interactivity:Interaction.Behaviors>

        <core:EventTriggerBehavior EventName="SelectionChanged">

            <core:InvokeCommandAction Command="{Binding Path=ItemSelectedCommand}" />

        </core:EventTriggerBehavior>

    </interactivity:Interaction.Behaviors>

</ListView>

 

 

Innanzitutto potete notare come il behavior venga dichiarato come se fosse una proprietà complessa del controllo: viene, perciò, incluso tra il tag di inizio e fine del controllo stesso (in questo caso, tra <ListView> e </ListView> ).

I behavior (perché se ne possono specificare più di uno) vengono inclusi all’interno della collezione Interaction.Behaviors, che è inclusa nel namespace Microsoft.Xaml.Interactivity. Sfruttando invece l’altro namespace che abbiamo precedentemente aggiunto (Microsoft.Xaml.Interactions.Core) andiamo ad aggiungere il behavior che ci interessa, ovvero EventTriggerBehavior. L’unica azione da compiere con questa classe è impostare la proprietà EventName, che rappresenta il nome dell’evento del controllo che vogliamo andare a gestire: nell’esempio, si tratta dell’evento SelectionChanged, che viene sollevato quando l’utente seleziona un elemento dalla lista.

Ora che con il behavior abbiamo registrato l’evento, dobbiamo però specificare che azione vogliamo eseguire: lo facciamo tramite la classe InvokeCommandAction, che ci permette di sfruttare la proprietà Command per specificare uno dei comandi dichiarati nel ViewModel.

Il gioco è fatto: ora quando l’utente selezionerà un elemento dalla lista, verrà eseguito il comando di nome ItemSelectedCommand. Dal punto di vista della sintassi, il comando non presenta alcuna differenza rispetto ad un altro che, invece, è associato direttamente alla proprietà Command del controllo. Ecco come potrebbe apparire, ad esempio, la definizione di ItemSelectedCommand:

 

private RelayCommand _itemSelectedCommand;

 

public RelayCommand ItemSelectedCommand

{

    get

    {

        if (_itemSelectedCommand == null)

        {

            _itemSelectedCommand = new RelayCommand(() =>

            {

                Debug.WriteLine(SelectedFeedItem.Title);

            });

        }

 

        return _itemSelectedCommand;

    }

}

 

SelectedFeedItem è una proprietà di tipo FeedItem dichiarata nel ViewModel che è stata messa in binding con la proprietà SelectedItem del controllo ListView: in questo modo, quando viene selezionato un elemento della lista, verrà stampata nella Output Window di Visual Studio il titolo della notizia selezionata.

I messaggi

A volte, durante il ciclo di sviluppo, sorge la necessità di far comunicare tra di loro due ViewModel o un ViewModel con la classe di code-behind legata alla View. Pensiamo, ad esempio, alla necessità di eseguire un’animazione nella View in seguito al verificarsi di una determinata condizione: quasi sicuramente, il momento in cui avviare l’animazione viene determinato dal ViewModel, perché è lì che è contenuta tutta la logica applicativa e di interazione con l’utente. L’animazione è però controllata dalla View: anche se è necessario scrivere del codice per avviarla, stiamo comunque parlando di una porzione dell’applicazione legata all’interfaccia grafica.

In queste situazioni il punto di forza di MVVM (ovvero la separazione dei ruoli) può diventarne anche la sua debolezza: come possiamo far parlare la View e il ViewModel se questi non hanno punti di contatto, se non tramite il DataContext?

Questi scenari vengono solitamente risolti con un’architettura a messaggi, ovvero dei semplici pacchetti che una classe centralizzata (che possiamo immaginare come un postino) si fa carico di distribuire alle varie componenti dell’applicazione. Il punto di forza dei messaggi è che sono disaccoppiati: non c’è alcun legame tra il mittente ed il destinatario. Tramite questo approccio:

1. Il mittente (un ViewModel o una View, ad esempio) invia un messaggio, specificandone il tipo.

2. Il destinatario (un altro ViewModel o un’altra View) si sottoscrive per ricevere i messaggi di un certo tipo.

Come vedete, il mittente è in grado di inviare un messaggio senza sapere chi lo riceverà; viceversa, il destinatario è in grado di ricevere messaggi, indipendentemente da chi li abbia inviati. Tutti i principali toolkit e framework per l’implementazione del pattern MVVM offrono delle classi che permettono di implementare questa architettura. MVVM Light non fa eccezione: useremo la classe Messenger che ci mette a disposizione per implementare l’esempio citato poco fa, ovvero la necessità di avviare un’animazione definita nella View da un ViewModel.

 

Il messaggio

Il primo passo è quello di creare il vero e proprio messaggio, che è rappresentato da una semplicissima classe. Per mantenere il progetto meglio organizzato, creiamo una cartella ad hoc nella quale includeremo i nostri messaggi, dopodiché facciamoci clic sopra con il tasto destro, scegliamo Add -> New item -> Class e diamogli un nome a piacimento. Ecco come appare lo scheletro di un messaggio:

 

public class StartAnimationMessage

{

}

 

 

Come potete notare, si tratta semplicemente di una classe, che può essere:

  • Vuota, come in questo caso, nel caso in cui il messaggio serva per segnalare un evento od un’attività.
  • Contenere una o più proprietà, nel caso in cui il messaggio debba anche includere delle informazioni da trasportare da una classe all’altra.

Nel nostro esempio, la definizione della classe è vuota perché non dobbiamo portare alcuna informazione dal ViewModel alla View: dobbiamo semplicemente segnalare alla View quando l’animazione debba essere avviata.

 

Il mittente

Analizziamo ora come inviare un messaggio, la cui tipologia è rappresentata dalla classe che abbiamo appena creato. Nel nostro scenario, il ViewModel deve segnalare alla View quando avviare l’animazione: sarà il ViewModel, perciò, ad essere il mittente, ovvero colui che invierà il messaggio.

Nella nostra applicazione di esempio ipotizziamo di inserire un pulsante nell’interfaccia grafica per avviare l’animazione: definiamo, perciò, un command che invii un messaggio di tipo StartAnimationMessage.

 

private RelayCommand _startAnimationCommand;

 

public RelayCommand StartAnimationCommand

{

    get

    {

        if (_startAnimationCommand == null)

        {

            _startAnimationCommand = new RelayCommand(() =>

            {

                Messenger.Default.Send<StartAnimationMessage>(new StartAnimationMessage());

            });

      }

 

        return _startAnimationCommand;

    }

}

 

 

Inviare un messaggio con MVVM Light è un’operazione semplice: si utilizza la classe Messenger (inclusa nel namespace GalaSoft.MvvmLight.Messaging), che espone un’istanza statica all’interno della proprietà Default. Perché si utilizza un’istanza statica? Perché il “postino” deve essere unico per tutta l’applicazione, altrimenti non sarebbe in grado di gestire l’invio e la ricezione di messaggi da parte di qualsiasi classe inclusa nel nostro progetto. Tale istanza espone un metodo Send<T>() , dove T è il tipo di messaggio che vogliamo inviare. Come parametro, dobbiamo creare una nuova istanza del tipo di messaggio che abbiamo appena specificato, nel nostro caso StartAnimationMessage.

Il gioco è fatto: a questo punto il messaggio è stato inviato, pronto per essere ricevuto da qualche altra classe.

 

Il destinatario

Nella nostra applicazione di esempio, il destinatario sarà la classe di code-behind legata alla View che contiene l’animazione: è da lì, infatti, che siamo in grado di avviarla. Ecco come appare una semplice animazione, definita nello XAML mediante la classe Storyboard:

<Page

   x:Class="MVVMLight.Messages.Views.MainView"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   xmlns:local="using:MVVMLight.Messages.Views"

   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

   DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=Main}"

   mc:Ignorable="d">

 

    <Page.Resources>

        <Storyboard x:Name="RectangleAnimation">

            <DoubleAnimation Storyboard.TargetName="RectangleTranslate"

                            Storyboard.TargetProperty="X"

                            From="0"

                            To="200"

                            Duration="00:00:05" />

        </Storyboard>

  </Page.Resources>

 

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Rectangle Width="100" Height="100" Fill="Blue">

            <Rectangle.RenderTransform>

                <TranslateTransform x:Name="RectangleTranslate" />

            </Rectangle.RenderTransform>

        </Rectangle>

    </Grid>

   

    <Page.BottomAppBar>

        <CommandBar>

            <CommandBar.PrimaryCommands>

                <AppBarButton Label="Play" Icon="Play" Command="{Binding Path=StartAnimationCommand}" />

            </CommandBar.PrimaryCommands>

        </CommandBar>

    </Page.BottomAppBar>

</Page>

 

 

 

Abbiamo incluso un rettangolo di colore blu e abbiamo definito, al suo interno, una TranslateTransform: si tratta di una delle trasformazioni messe a disposizione dello XAML, che ci permette semplicemente di variare le coordinate X e Y del controllo. Grazie a tale trasformazione, sarà semplice definire un’animazione che ci permetterà di spostare il rettangolo dall’interno della pagina.

La definizione dell’animazione la troviamo poco sopra, all’interno delle risorse della pagina: si tratta di una DoubleAnimation, che ci permetterà di variare la coordinata X del rettangolo da 0 (la proprietà From) a 200 (la proprietà To) in 5 secondi (la proprietà Duration). Avviare l’animazione richiede, per forza di cose, la scrittura di codice in code-behind: dobbiamo infatti chiamare il metodo Begin() del controllo Storyboard, il quale, però, essendo dichiarato a livello di risorsa della pagina, non sarebbe accessibile dal nostro ViewModel.

Quello che facciamo è perciò, all’interno del code-behind, spiegare al nostro “postino” che la classe è in grado di ricevere e manipolare i messaggi di tipo StartAnimationMessage: quando ne riceveremo uno, avvieremo l’animazione. Ecco un esempio di registrazione:

 

public sealed partial class MainView : Page

{

    public MainView()

    {

        this.InitializeComponent();

        Messenger.Default.Register<StartAnimationMessage>(this, message =>

        {

            RectangleAnimation.Begin();

        });

    }

}

 

 

Nel costruttore della classe di code-behind della pagina che contiene l’animazione andiamo nuovamente a sfruttare l’istanza statica della classe Messenger e, nello specifico, il metodo Register<T>() , dove T è la tipologia di messaggio che vogliamo ricevere. Sono richiesti due parametri:

1. La classe che si farà carico di gestire il messaggio. Tipicamente, coincide con la classe stessa dove si sta chiamando il metodo Register<T>() , quindi si utilizza semplicemente la parola chiave this.

2. Una Action che rappresenta il codice da eseguire quando viene ricevuto il messaggio. Nell’esempio, la esprimiamo con un anonymous method e andiamo a chiamare il metodo Begin() dell’oggetto di tipo Storyboard (chiamato RectangleAnimation) che abbiamo definito nello XAML.

Da questo momento in poi, ogni volta qualunque altra classe dell’applicazione invierà un messaggio di tipo StartAnimationMessage, la View sarà in grado di riceverlo e avviare l’animazione.

 

Attenzione!

Quando si lavora con i messaggi, è bene fare attenzione alla gestione delle pagine nella cache. Potremmo trovarci, infatti, nella situazione in cui un ViewModel o una View siano ancora in memoria anche se non visibili in quel momento; di conseguenza, anche la classe Messenger sarebbe ancora attiva e pronta a ricevere messaggi. Ciò potrebbe portare ad una serie di problemi nel momento in cui lo stesso tipo di messaggio sia stato sottoscritto da più View diverse: il messaggio potrebbe essere ricevuto da una View diversa da quella che ci aspettiamo.

Per questo motivo, è importante ricordarsi di deregistrare la ricezione di messaggi nel momento in cui l’utente si sposta dalla pagina corrente (a meno che non ci sia la necessità esplicita di mantenere attiva la ricezione di messaggi anche quando la pagina non è visualizzata). Tale risultato si raggiunge chiamando il metodo Unsubscribe<T>() , dove T è il tipo di messaggio:

 

protected override void OnNavigatedFrom(NavigationEventArgs e)

{

    Messenger.Default.Unregister<StartAnimationMessage>(this);

}

 

 

L’unico parametro richiesto è un riferimento alla classe che deve annullare la sottoscrizione del messaggio (anche in questo caso, tipicamente si usa this, dato che il metodo viene chiamato nella classe stessa che l’ha registrato).

 

Il dispatcher

L’utilizzo del dispatcher dovrebbe essere un concetto già noto, dato che non è specificatamente legato all’utilizzo del pattern MVVM. Perché nasce l’esigenza di usare il dispatcher?

L’interfaccia grafica delle Universal Windows app viene gestita da un unico thread, chiamato UI Thread. Questo thread deve essere lasciato il più libero possibile: se si iniziassero ad eseguire operazioni impegnative su questo thread, la reattività e la fluidità dell’interfaccia grafica ne risentirebbero notevolmente. Per questo motivo, il framework mette a disposizione una serie di API per poter eseguire porzioni di codice su thread differenti da quello della UI, così da lasciarlo il più libero possibile. In questi scenari, però, può capitare la necessità di riportare l’esecuzione sul thread della UI perché, ad esempio, dobbiamo mostrare i risultati in un controllo XAML.

Nella maggior parte dei casi, grazie alle novità introdotte in C# 5, non dovremo preoccuparci di questa necessità. La maggior parte delle API della Universal Windows Platform è implementata sfruttando l’approccio async / await, che si fa carico di eseguire operazioni onerose in un thread differente da quello della UI e di ripotare automaticamente il risultato al thread principale.

In alcuni casi, però, ci sono ancora scenari in cui è necessario gestire manualmente il passaggio tra i differenti thread. Prendiamo, come esempio, il seguente blocco di codice:

 

private RelayCommand _dispatcherCommand;

 

public RelayCommand DispatcherCommand

{

    get

    {

        if (_dispatcherCommand == null)

        {

            _dispatcherCommand = new RelayCommand(async () =>

            {

                await Task.Run(async () =>

                {

                    //eseguo delle operazioni

                    Message = "I'm a message";

                });

            });

        }

 

        return _dispatcherCommand;

    }

}

 

 

Questo command esegue una serie di operazioni, per poi assegnare un testo ad una proprietà di nome Message (collegata ad un controllo TextBlock presente nella View tramite binding). La particolarità è che il codice è incapsulato all’interno del metodo Task.Run() , che forza l’esecuzione delle operazioni su un thread differente da quello della UI. Questo fa sì che, se collegassimo questo command ad un pulsante nell’interfaccia grafica e lo premessimo, si scatenerebbe un’eccezione: abbiamo cercato di accedere al thread della UI da un thread secondario.

Proprio per questi scenari il framework mette a disposizione il dispatcher, ovvero una classe che consente di forzare l’esecuzione di alcune porzioni di codice sul thread della UI. L’utilizzo di base è piuttosto semplice:

 

await Dispatcher.RunAsync(() =>

{

    Message = "I'm a message";

});

 

 

All’interno del metodo RunAsync() è possibile specificare una Action (nell’esempio, è dichiarata tramite un anonymous method) che sarà eseguita sul thread della UI, indipendentemente da quale sia il thread di partenza.

Qual è il problema dell’utilizzo del pattern MVVM in questo scenario? Che la classe Dispatcher è accessibile solamente da code-behind e, di conseguenza, non potremmo usare il blocco di codice sopra riportato all’interno di un ViewModel.

Per questo motivo, la maggior parte dei toolkit e dei framework per l’implementazione del pattern MVVM offrono degli strumenti per semplificare l’accesso al dispatcher da parte di classi che non siano di code-behind e, di conseguenza, legate a doppio filo alla View.

MVVM Light non fa eccezione e mette a disposizione una classe chiamata DispatcherHelper. Prima di utilizzarla, però, è necessario inizializzarla. Possiamo farlo, ad esempio, all’interno del metodo OnLaunched() della classe App, sfruttando il metodo Initialize() come nell’esempio seguente:

 

protected override void OnLaunched(LaunchActivatedEventArgs e)

{

 

    DispatcherHelper.Initialize();

 

    Frame rootFrame = Window.Current.Content as Frame;

    if (rootFrame == null)

    {

       

        rootFrame = new Frame();

 

        rootFrame.NavigationFailed += OnNavigationFailed;

 

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)

        {

            //TODO: Load state from previously suspended application

        }

 

        // Place the frame in the current Window

        Window.Current.Content = rootFrame;

    }

 

    if (rootFrame.Content == null)

    {

        // When the navigation stack isn't restored navigate to the first page,

        // configuring the new page by passing required information as a navigation

        // parameter

        rootFrame.Navigate(typeof(MainView), e.Arguments);

    }

    // Ensure the current window is active

    Window.Current.Activate();

}

 

 

Da questo momento in poi possiamo utilizzare la classe DispatcherHelper anche all’interno dei ViewModel, come nell’esempio seguente:

 

private RelayCommand _dispatcherCommand;

 

public RelayCommand DispatcherCommand

{

    get

    {

        if (_dispatcherCommand == null)

        {

            _dispatcherCommand = new RelayCommand(async () =>

            {

                await Task.Run(async () =>

                {

//eseguo delle operazioni

                    DispatcherHelper.RunAsync(() =>

                    {

                        Message = "I'm a message";

                    });

                });

            });

        }

 

        return _dispatcherCommand;

    }

}

 

 

La sintassi è la stessa che abbiamo visto in precedenza: chiamiamo il metodo RunAsync() della classe DispatcherHelper, specificando come Action le righe di codice che vogliamo eseguire sul thread della UI.

 

MVVM Light e Template10

Sulle pagine di questo stesso blog abbiamo imparato a conoscere Template10, un progetto open source a cura di Microsoft per offrire un nuovo punto di partenza per lo sviluppo di applicazioni Windows 10. In uno dei post pubblicati abbiamo approfondito la relazione tra Template10 e il pattern MVVM, analizzando come anche questa libreria offra una serie di classi che semplificano l’implementazione del pattern.

Template10, però, non sostituisce in tutto e per tutto MVVM Light: alcune funzionalità (come la gestione delle dependency injection o dei messaggi) non sono disponibili. Poco male: la semplicità di MVVM Light lo rende facilmente integrabile con altre librerie e Template10 non fa eccezione. È sufficiente, perciò, installare tramite NuGet MVVM Light nel vostro progetto e sfruttare le funzionalità di uno o dell’altro in base alle vostre esigenze. Ad esempio, come classe base da cui far ereditare i vostri ViewModel è consigliabile continuare ad usare la classe ViewModelBase di Template10 piuttosto che quella di MVVM Light, dato che vi espone anche gli eventi di navigazione della pagina OnNavigatedTo() e OnNavigatedFrom() . Allo stesso tempo, però, sarete in grado di sfruttare anche la dependency injection tramite la classe SimpleIoc oppure lo scambio di messaggi tramite la classe Messenger, che sono invece specifiche di MVVM Light.

 

In conclusione

Con questo post concludiamo il viaggio alla scoperta del pattern Model-View-ViewModel nelle Universal Windows app. Il consiglio che posso darvi per assimilare al meglio i concetti che abbiamo trattato nei vari post è: fate pratica! La teoria che abbiamo trattato è fondamentale per capire le motivazioni che stanno dietro a questo approccio, ma l’unico modo per capire veramente come funziona il pattern e toccare con mano i vantaggi rispetto all’utilizzo del code-behind è quello di realizzare un progetto reale con MVVM.

Un ottimo punto di partenza è quello di prendere un vostro progetto già esistente realizzato con l’approccio tradizionale e convertirlo a MVVM. Oppure, se siete in procinto di sviluppare una nuova applicazione, approfittatene per svilupparla in MVVM. All’inizio potreste avere qualche difficoltà e dovrete soprattutto vincere la “pigrizia mentale” di uscire dalla “comfort zone”, in quanto dovrete utilizzare un approccio diverso da quello che avete sempre usato fino ad oggi.

Ben presto, però, vi renderete conto di come l’adozione di questo pattern semplifichi l’evoluzione e il mantenimento del vostro progetto e vi sembrerà quasi impossibile tornare indietro al vecchio approccio basato sul code-behind. Come sempre, vi ricordo che potete trovare alcuni esempi (anche legati agli argomenti trattati in questo post) su questo repository GitHub https://github.com/qmatteoq/UWP-MVVMSamples/ Happy coding!