Guest post: Template10, un nuovo approccio nello sviluppo di Universal Windows app – Il pattern MVVM


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

Il pattern Model-View-ViewModel (da ora in poi, MVVM) è sicuramente il più diffuso in ambito di sviluppo di applicazioni XAML. La spiegazione dettagliata del pattern esula dallo scopo di questo post e, a tal proposito, vi rimando alle numerose risorse che si trovano in rete. Quello che è importante sapere è che lo scopo del pattern MVVM è di migliorare la testabilità, la manutenibilità e la progettazione di una Universal Windows app, in quanto aiuta a raggiungere uno dei principi chiave che guida lo sviluppo software (di qualsiasi tipo): la separazione dei ruoli, ovvero rendere ben distinte le parti che si occupano di definire l’interfaccia grafica e quelle, che invece, si fanno carico di gestire la logica applicativa (caricamento dei dati, interazione con il cloud o un database, ecc.).

Il pattern MVVM è particolarmente adatto per le Universal Windows app e, più in generale, per tutte le applicazioni basate su XAML, in quanto ne sfrutta largamente i meccanismi base come il binding, le dependency property e l’interfaccia INotifyPropertyChanged. Il pattern prevede la suddivisione del progetto in tre componenti:

1. Il model, ovvero le entità e i servizi che manipolano i dati dell’applicazione in maniera “grezza”, senza dipendenze da come devono essere presentati.

2. La view, ovvero l’interfaccia grafica con cui l’utente interagisce. Nel mondo delle Universal Windows app, le view sono rappresentate dalle pagine XAML.

3. I ViewModel, ovvero il collante tra il model e la view. I ViewModel sono delle classi che si fanno carico di recuperare i dati dal model, di prepararli per la presentazione ed i passarli alla view tramite il binding.

L’utilizzo del pattern MVVM, pur apportando tantissimi vantaggi, comporta comunque un tempo di configurazione e preparazione del progetto maggiore: è necessario, infatti, ogni volta mettere in piedi l’architettura necessaria in termini di servizi e classi di utilità per il collegamento dei vari “strati”. Nello sviluppo di una Universal Windows app, infatti, esistono tantissimi scenari (la gestione della navigazione, il ciclo di vita, i contratti, ecc.) che sono semplici da implementare quando si utilizza il code-behind, ma devono essere ripensati in ottica MVVM, in quanto andremo a scrivere gran parte del codice nel ViewModel (che è una classe indipendente). Uno scenario esemplificativo è la gestione del ciclo di vita di una pagina: tipicamente, viene affidata ai metodi OnNavigatedTo() e OnNavigatedFrom() che possono essere gestiti all’interno del code-behind. Questa gestione è resa possibile dal fatto che la classe di code-behind, essendo legata alla view, eredita direttamente dalla classe Page, che rappresenta la singola pagina dell’applicazione. Tale opportunità, invece, non è disponibile all’interno di un ViewModel, in quanto si tratta di una classe che non ha un legame diretto con la view come, invece, avviene con la classe che funge da code-behind.

Per tale scopo, diversi sviluppatori hanno realizzato delle librerie e dei framework che semplificano e velocizzano il loro lavoro, fornendo tutti gli strumenti base necessarie. I più popolari sono sicuramente MVVM Light di Laurent Bugnion, Caliburn Micro di Rob Eisenberg e Prism, originalmente creato dalla divisione Pattern & Practice di Microsoft e ora gestito da un gruppo di MVP.

Ognuno di questi framework ha le sue peculiarità e le sue caratteristiche: MVVM Light è il più semplice e flessibile di tutti, ma lascia molto lavoro allo sviluppatore in quanto non offre alcuno strumento nativo per gestire gli scenari tipici di una Universal Windows app. Caliburn Micro e Prism, invece, sono più complessi e più “rigidi”, ma offrono molti servizi già pronti per gestire gli scenari più comuni, come la gestione del ciclo di vita, la navigazione, ecc.

Template10 non si vuole porre come l’ennesimo framework di MVVM (anche se, come vedremo, al momento offre già molti strumenti base presenti anche nelle altre librerie), ma vuole essere un aiuto per tutti gli sviluppatori che utilizzano questo pattern e che vogliono essere produttivi sin da subito e non vogliono “reinventare la ruota” ogni volta che vogliono iniziare un nuovo progetto.

Vediamo in dettaglio quali sono le funzionalità più utili, fermo restando che quanto è stato descritto nei post precedenti (bootstrapper, extended splash screen, i nuovi controlli, ecc.) si può tranquillamente applicare anche ai progetti basati su MVVM.

 

Gli strumenti base per implementare il pattern

Anche se Template10 non si pone come alternativa ai framework già esistenti, offre comunque gli strumenti base indispensabili per implementarlo nella maniera più corretta. Non è obbligatorio usarli: potete decidere tranquillamente, ad esempio, di aggiungere MVVM Light al vostro progetto e di usare le classi offerte dal toolkit al posto di quelle che ora andrò a descrivere.

La prima importante classe offerta da Template10 si chiama ViewModelBase ed è la classe base da cui far ereditare i ViewModel della nostra applicazione. Oltre ad offrire l’accesso ad una serie di utili funzioni che vedremo in seguito, vi fornisce gli strumenti base per l’implementazione del pattern. Uno dei più importanti è sicuramente il metodo Set(), che vi permette di gestire la propagazione delle modifiche effettuate alle proprietà del ViewModel alla UI, tramite l’implementazione dell’interfaccia INotifyPropertyChanged. Ecco come appare la definizione di una proprietà all’interno di un ViewModel:

 

publicclassMainViewModel : Template10.Mvvm.ViewModelBase

{

 

    public MainViewModel()

    {

       

    }

 

    privatestring _name;

 

    publicstring Name

    {

        get { return _name; }

        set { Set(ref _name, value); }

    }

}

Il metodo Set(), chiamando all’interno del setter della proprietà, fa sì che, oltre a cambiarne il valore, venga inviata una notifica alla View: in questo modo, qualsiasi controllo sia in binding con tale proprietà, si aggiornerà per mostrare il nuovo valore. Ecco, ad esempio, come tale proprietà venga collegata ad un controllo TextBlock così che, ad ogni variazione della stessa, il nuovo valore venga immediatamente mostrato nell’interfaccia utente.

<TextBlock Text=”{Binding Path=Name}” />

Se avete già esperienza con MVVM Light questo approccio vi sarà famigliare: anche il toolkit di Laurent Bugnion offre un omonimo metodo Set() con lo stesso scopo.

Un’altra funzionalità molto importante messa a disposizione da Template10 in ottica MVVM è la classe DelegateCommand, che consente di implementare in maniera semplice i command. Di cosa si tratta? Per gestire l’interazione dell’utente con l’interfaccia grafica, in un’applicazione tradizionale, si utilizzano gli event handler, ovvero metodi che vengono legati ad un evento sollevato da un controllo, come il tap su un pulsante o la selezione di un elemento da una lista. Tali event handler, però, sono legati a doppio filo con la view: di conseguenza, possono essere dichiarati solamente all’interno del code-behind. In un’applicazione MVVM, invece, abbiamo la necessità di gestire le interazioni dell’utente direttamente nei ViewModel, dato che sono loro a contenere tutta la logica. Per questo scopo sono nati i command, ovvero la possibilità di esprimere un metodo tramite una proprietà, che ci dà la possibilità di sfruttare il binding per collegarla ad un controllo. Un command è una proprietà che definisce:

1. Le azioni da eseguire quando il comando viene invocato.

2. Le condizioni che devono essere soddisfatte affinché il comando sia abilitato. Tale condizione fa sì che lo stato visuale del controllo cambi in automatico in base allo stato del comando. Ad esempio, se il command è disabilitato, anche il pulsante ad essa collegato apparirà non abilitato all’utente.

Il Windows Runtime include un’interfaccia base per l’implementazione dei command (chiamata ICommand): la realizzazione vera e propria, però, è a carico dello sviluppatore. Per velocizzare questa fase Template10 offre la classe DelegateCommand, che implementa tale interfaccia e che si inizializza semplicemente passando nel costruttore le due informazioni sopra indicate. Ecco un esempio:

 

privateDelegateCommand _setNameCommand;

 

publicDelegateCommand SetNameCommand

{

    get

    {

        if (_setNameCommand == null)

        {

            _setNameCommand = newDelegateCommand(() =>

            {

                Result = $”Hello {Name};

            }, () => !string.IsNullOrEmpty(Name));

        }

 

        return _setNameCommand;

    }

}

Il primo parametro rappresenta una Action (in questo caso, definita con una funzione anonima), che definisce le operazioni da eseguire quando il comando viene eseguito. Il secondo parametro (opzionale), invece, è una funzione che deve restituire un valore booleano e che determina se il comando sia abilitato o meno. Nell’esempio, il comando è abilitato solo nel caso in cui il contenuto della proprietà Name non sia vuoto.

Una volta che avete definito il vostro command, potete collegarlo ad i controlli XAML tramite l’omonima proprietà. Tutti i principali controlli che sono in grado di gestire l’interazione con l’utente (ad esempio, Button) espongono infatti una proprietà di nome Command, che può essere collegata tramite binding alla proprietà di tipo DelegateCommand appena definita, come nell’esempio seguente:

 

<Button Content=”Click me” Command=”{Binding Path=SetNameCommand}” />

Anche in questo caso, se avete già esperienza con MVVM Light l’approccio vi sarà famigliare: l’unica differenza è che la classe messa a disposizione dal toolkit si chiama RelayCommand.

 

Navigazione e ciclo di vita della pagina

Come anticipato nell’introduzione, uno degli scenari più frequenti da gestire in una Universal Windows app è il ciclo di vita di una pagina: gli eventi di navigazione, infatti, vengono spesso usati per gestire operazioni fondamentali come il caricamento dei dati. Di conseguenza, avere accesso a tali eventi anche all’interno di un ViewModel è fondamentale, visto che tale classe costituisce proprio il punto di contatto tra View e Model.

A tale scopo, la casse ViewModelBase offre una serie di funzionalità aggiuntivo proprio per questo scenario. La prima è l’implementazione di un’interfaccia chiamata INavigable, che permette di accedere agli eventi di navigazione della pagina direttamente nel ViewModel, come nell’esempio seguente.

 

publicclassDetailViewModel : ViewModelBase

{

    publicoverridevoid OnNavigatedTo(object parameter, NavigationMode mode, IDictionary<string, object> state)

    {

if (parameter != null)

       {

              int id = (int) parameter;

              //load data

       }

    }

 

    publicoverrideTask OnNavigatedFromAsync(IDictionary<string, object> state, bool suspending)

    {

        returnbase.OnNavigatedFromAsync(state, suspending);

    }

}

Il metodo OnNavigatedTo() ci permette di scoprire la modalità di navigazione (tramite il parametro mode), così da poter differenziare le operazioni da fare. Ad esempio, è plausibile che il caricamento iniziale dei dati venga fatto solo nel caso in cui riceviamo il valore New dell’enumeratore NavigationMode, che rappresenta la prima inizializzazione della pagina. Senza questa discriminante, i dati sarebbero ricaricati ad ogni accesso alla pagina stessa.

Un’altra informazione utile è contenuta nell’oggetto parameter, che ci permette di gestire il passaggio di parametri da una pagina all’altra. Nel caso in cui, navigando verso questa pagina, avessimo scelto di passare un parametro (ad esempio, l’identificativo dell’elemento scelto in una lista), lo ritroveremo all’interno di questa proprietà.

Esistono poi altre utili proprietà (come state) che vedremo in dettaglio nel paragrafo successivo, dedicato alla gestione dello stato della pagina.

Rimanendo in ambito della navigazione, un’altra funzionalità messa a disposizione della classe ViewModelBase è l’accesso al NavigationService, ovvero un helper che vi permette di gestire la navigazione da una pagina all’altra direttamente dal ViewModel. Si tratta, infatti, di un altro degli scenari che può essere complicato da gestire in ottica MVVM: normalmente, la navigazione viene affidata alla classe Frame, che però è accessibile solamente dal code-behind.

La classe NavigationService è molto semplice da utilizzare, in quanto funge semplicemente da wrapper della classe Frame. Ad esempio, se vogliamo portare l’utente ad un’altra pagina dobbiamo usare il metodo Navigate(), che accetta come parametri il tipo di pagina di destinazione e un eventuale parametro.

 

NavigationService.Navigate(typeof(DetailPage), person?.Id);

 

E’ importante sottolineare come, nonostante il metodo Navigate() accetti come secondo parametro un generico object, sia indispensabile passare dati di tipo semplice (una stringa, un numero, ecc.). Questo perché, affinché la gestione dello stato che vedremo nel paragrafo successivo funzioni correttamente, è necessario che i parametri di navigazione siano serializzabili, cosa che spesso non accade invece per i tipi complessi.

Possiamo usare il NavigationService, in alternativa, per manipolare lo stack delle pagine: ad esempio, possiamo usare il metodo GoBack() per portare l’utente alla pagina precedente, oppure il metodo ClearHistory() per azzerare lo stack.

 

Gestire lo stato della pagina

Uno degli scenari più importanti da gestire in una Universal Windows app è il ciclo di vita dell’applicazione. Come abbiamo visto in uno dei post precedenti, le applicazioni, quando non sono più in foreground, vengono sospese: il processo viene congelato in memoria (così che lo stato venga mantenuto), ma tutti i thread che possono consumare CPU, batteria, rete o altre risorse vengono terminati. Nel momento in cui l’applicazione viene ripristinata, non dobbiamo fare nulla: dato che lo stato era stato mantenuto in memoria, l’utente la ritroverà esattamente come l’aveva lasciata.

Il sistema operativo, però, può terminare un’applicazione sospesa in caso si stiano esaurendo le risorse. In tal caso, dato che il processo è stato completamente terminato, l’applicazione non ritornerà allo stato in cui l’utente l’aveva lasciata, ma verrà riavviata da capo. Come sviluppatori, dobbiamo gestire invece questa situazione: dato che per l’utente la procedura di terminazione è completamente trasparente, lui si aspetterà di ritrovare l’applicazione esattamente come l’aveva lasciata. È compito nostro, perciò, salvare e ripristinare lo stato delle pagine. Ipotizziamo, ad esempio, di avere una una pagina che contenga una serie di campi che l’utente deve compilare: nel momento in cui l’applicazione viene sospesa e poi riattivata, l’utente si aspetterà di ritrovare i campi esattamente come li aveva lasciati.

Si tratta di un’operazione fondamentale, ma complessa da gestire sfruttando il template base delle Universal Windows app, principalmente per due motivi:

1. Dato che non possiamo sapere a priori se l’applicazione sospesa sarà terminata o meno, dobbiamo farci carico di salvare lo stato (ad esempio, il contenuto dei vari campi del form) ad ogni navigazione. In ottica MVVM (senza l’uso di Template10), questo complica le cose perché non abbiamo accesso, dal ViewModel, agli eventi di navigazione della pagina.

2. Nel momento in cui l’applicazione viene sospesa, dobbiamo farci carico di salvare nello storage locale lo stato dell’applicazione: il contenuto dei vari campi, l’ultima pagina visitata, ecc. Dopodiché, in fase di avvio, dobbiamo determinare se l’applicazione è stata riaperta in seguito ad una terminazione e, di conseguenza, ripristinare lo stato e portare l’utente all’ultima pagina visitata. Il template base, purtroppo, non offre alcuno strumento per gestire questa operazione, lasciando allo sviluppatore la necessità di farsi carico di tutto.

Template10 semplifica notevolmente questo scenario: ciò che dovremo fare, come sviluppatori, sarà solamente salvare e caricare lo stato sfruttando le funzionalità messe a disposizione dal template.

Lo strumento che Template10 ci mette a disposizione è una collezione di coppie chiave – valore, a cui si ha accesso tramite un parametro degli eventi OnNavigatedTo() e OnNavigatedFrom() implementati dalla classe ViewModelBase. In fase di sospensione dell’applicazione (quindi nell’evento OnNavigatedFrom()) andremo ad aggiungere a questa collezione tutte le informazioni che ci servono per preservare lo stato (ad esempio, il valore di ogni campo della form). In fase di caricamento della pagina, invece (quindi nell’evento OnNavigatedTo()) andremo a recuperare queste informazioni, se presenti, e le utilizzeremo per ripristinare il valore originale delle varie proprietà del ViewModel.

Vediamo un esempio concreto. Ipotizziamo che la pagina della nostra applicazione contenga un controllo TextBox, all’interno del quale l’utente può inserire del testo, che viene salvato in una proprietà del ViewModel. Nel ViewModel avremo perciò una proprietà di tipo string così definita:

 

privatestring _name;

 

publicstring Name

{

    get { return _name; }

    set { Set(ref _name, value); }

}

Tale proprietà sarà collegata al controllo TextBox nella view (quindi nella pagina XAML) tramite binding:

 

<TextBlock Text=”{Binding Path=Name, Mode=TwoWay}” />

Possiamo notare come sia stata impostata la modalità TwoWay: in questo modo, ogni volta l’utente inserirà del testo all’interno della casella di testo, in automatico la proprietà Name nel ViewModel sarà aggiornata con il nuovo valore.

Se ora provassimo a sospendere l’applicazione e poi a riaprirla, potremmo notare come il testo inserito nella casella di testo sarà in automatico mantenuto: questo perché il processo, durante la sospensione, è stato solo congelato e non terminato. Di conseguenza, lo stato è mantenuto in memoria. Per simulare questo scenario possiamo sfruttare il menu a tendina Lifecycle events di Visual Studio, che permette di testare le varie fasi del ciclo di vita dell’applicazione.

clip_image002[12]

Se scrivessimo del testo nel controllo TextBox, sospendessimo l’app premendo il pulsante Suspend e poi la riaprissimo, troveremmo tutto come l’avevamo lasciato.

Se invece premessimo il pulsante Suspend and shutdown, che simula la terminazione da parte del sistema operativo, al riavvio il testo all’interno del controllo sarebbe andato perso: questo perché il processo è stato effettivamente terminato e, dato che noi non abbiamo salvato lo stato in un’area di memoria persistente come lo storage locale, non è stato possibile riportare l’applicazione allo stato originale.

In questo scenario subentra la collezione di coppie chiave-valore citata in precedenza: all’interno dell’evento OnNavigatedFrom() possiamo determinare se siamo in fase di sospensione oppure no e, in caso affermativo, salvare le informazioni che ci servono all’interno della collezione, come nell’esempio seguente.

 

publicclassDetailViewModel : ViewModelBase

{   

    privatestring _name;

 

    publicstring Name

    {

        get { return _name; }

        set { Set(ref _name, value); }

    }

 

 

    publicoverrideTask OnNavigatedFromAsync(IDictionary<string, object> state, bool suspending)

    {

        if (suspending)

        {

            state.Add(“Name”, Name);

        }

 

        returnbase.OnNavigatedFromAsync(state, suspending);

    }

}

Come potete vedere, sfruttiamo entrambi i parametri che ci vengono forniti dal metodo. Il primo è un booleano, che ci permette di capire se l’evento è scattato perché siamo in fase di sospensione o semplicemente perché ci stiamo spostando verso un’altra pagina. In caso di sospensione, andiamo a salvare nel secondo parametro (la collezione di nome state) il valore della proprietà Name, associandola ad una chiave dal nome omonimo. In questo modo, in fase di sospensione, l’infrastruttura di Template10 farà sì che, in automatico, il contenuto della collezione venga automaticamente serializzato in un file testuale e salvato nello storage locale. Da qui si evince uno dei requisiti fondamentali di questo approccio: i dati che memorizziamo all’interno della collezione devono essere serializzabili, ovvero devono poter essere tradotti in XML o JSON.

Il passaggio successivo è quello di gestire, invece, il caricamento della pagina: nel caso in cui ci siano dei dati all’interno della collezione state (che è resa disponibile anche all’interno del metodo OnNavigatedTo()), allora vuol dire che proveniamo da uno scenario di attivazione e dobbiamo, perciò, recuperarli. Anche in questo caso, il grosso del lavoro lo farà per noi Template10: all’avvio dell’applicazione, se si proviene da una terminazione del sistema operativo, il bootstrapper andrà a recuperare i dati serializzati nello storage locale e li ricaricherà all’interno della collezione. Ecco un esempio di codice che completa quello precedente:

 

publicclassDetailViewModel : ViewModelBase

{

    privatestring _name;

 

    publicstring Name

    {

        get { return _name; }

        set { Set(ref _name, value); }

    }

 

    publicoverridevoid OnNavigatedTo(object parameter, NavigationMode mode, IDictionary<string, object> state)

    {

        if (state.Any())

        {

            Name = state[“Name”].ToString();

            state.Clear();

        }

    }

 

    publicoverrideTask OnNavigatedFromAsync(IDictionary<string, object> state, bool suspending)

    {

        if (suspending)

        {

            state.Add(“Name”, Name);

        }

        returnbase.OnNavigatedFromAsync(state, suspending);

    }

}

 

All’interno dell’evento OnNavigatedTo() verifichiamo la presenza di dati all’interno della collezione e, in caso affermativo, andiamo a recuperare il dato identificato dalla chiave Name e lo assegniamo all’omonima proprietà del ViewModel. Dopodiché procediamo a svuotare la collezione con il metodo Clear(), così da evitare che i dati vengano resi nuovamente disponibili anche in caso di semplici navigazioni e non di scenari di sospensione / attivazione.

Se ora riprovassimo a simulare la terminazione con il pulsante Suspend and shutdown e a rilanciare l’applicazione, sarà come se nulla fosse successo: l’applicazione si avvierà direttamente nell’ultima pagina aperta e la casella di testo conterrà il dato precedentemente inserito dall’utente.

Importante! Questo approccio deve essere utilizzato per salvare lo stato della pagina, non i dati veri e propri dell’applicazione. Un buon approccio allo sviluppo consiste nel salvare i dati generati dall’utente il prima possibile, così da minimizzare la possibile perdita di dati. Se cercassimo di salvare tutti i dati in fase di sospensione, il tempo a disposizione potrebbe non essere sufficiente. È, invece, corretto salvare solamente quei dati che consentono di ricreare l’illusione all’utente che l’applicazione non sia mai stata chiusa. Ad esempio, ipotizziamo che l’applicazione vista in precedenza consenta, con un pulsante, di salvare in maniera permanete il contenuto della casella di testo. In tal caso, il salvataggio (ad esempio, in un database) deve essere effettuato immediatamente. All’interno della collezione messa a disposizione dagli eventi OnNavigatedFrom() e OnNavigatedTo(), invece, andremo a salvare e a caricare solo il valore della proprietà Name associata al controllo TextBox: vogliamo evitare, infatti, che l’utente perda il dato nel caso in cui sospenda l’applicazione prima di premere il pulsante di salvataggio.

 

Accedere al thread della UI

In alcune situazioni può capitare, all’interno di un ViewModel, di effettuare alcune operazioni su thread differenti da quelli della UI. In tal caso, nel momento in cui dobbiamo interagire con i controlli presenti nella View (banalmente, perché dobbiamo cambiare il valore di una proprietà del ViewModel che è in binding con un controllo nella View), potremmo avere dei problemi: si scatenerà, infatti, un’eccezione, dovuta al fatto che stiamo tentando di accedere al thread della UI da un thread secondario.

In questi casi subentra una classe del Windows Runtime chiamata dispatcher che, come si evince dal nome stesso, funge da “postino” verso il thread della UI, indipendentemente dal thread in cui ci si trova. Anche in questo caso abbiamo il problema che l’accesso diretto al dispatcher viene fornito solamente all’interno della classe di code behind. A tale scopo, Template10 introduce un helper che consente di sfruttare il dispatcher anche da una classe qualsiasi, come un ViewModel. L’utilizzo è molto semplice, come potete vedere nell’esempio seguente:

 

await Dispatcher.DispatchAsync(() =>

{

    //do something on the UI thread

});

 

Si chiama il metodo DispatchAsync() della classe Dispatcher e, all’interno, si definiscono le operazioni che si vogliono eseguire sul thread della UI invece che su quello corrente.

 

In conclusione

Come potete vedere, in ottica di sviluppo con il pattern MVVM Template10 assume ancora maggiore importanza: ci permette, infatti, di gestire in maniera semplice molti scenari fondamentali, che però possono creare qualche grattacapo quando vengono approcciati con il pattern MVVM. Lo sviluppo di Template10, però, è solamente agli inizi e, nei prossimi mesi, crescerà notevolmente il numero di servizi che saranno messi a disposizione per integrare più facilmente le funzionalità di Windows all’interno dei ViewModel: tile, sharing, storage, ecc.

Per ora il nostro viaggio esplorativo di Template10 è terminato. Sono sicuro, però, che avremo occasione di riparlarne in futuro! Concludo ricordandovi il link ufficiale del progetto, dove trovate il codice sorgente, inclusi diversi progetti di esempio: https://github.com/Windows-XAML/Template10

Comments (1)

  1. Paolo Mazzon says:

    Speriamo sia un progetto continui,complimenti bell'articolo

Skip to main content