Patterns & Practices

Model-View-ViewModel: un esempio in WPF

Nello sviluppo di applicazioni enterprise vige una legge che definirei di sopravvivenza: separazione delle responsabilità, indicata spesso con la sigla SoC (Separation of Concern). Cosa significa? Semplicemente individuare le isole funzionali del sistema e definire il compito che ognuna deve assolvere. Non è sempre facile ma fortunatamente ci sono i design pattern ad aiutarci.
 
Un design pattern non è altro che una soluzione generale a un problema ricorrente, il che significa che dei tanti problemi di sviluppo che incontriamo ogni giorno molti sono ricorrenti e in quanto tali l’esperienza ha portato a soluzioni generiche ben collaudate per risolverli. Un pattern non va necessariamente preso così come è definito, possiamo adottarlo in parte o in pieno a seconda che risolva in parte o in pieno il nostro problema.
 
Nell’ottica di separare le responsabilità, lo sviluppo delle interfacce utente trae grande benefico da molti pattern che tendono a disaccoppiare la logica di interfaccia da quella di dominio. Il più famoso è forse Model-View-Controller, in cui la logica di dominio (Model) e quella di interfaccia (View) vengono “orchestrate” da un’entità di controllo (Controller) che a secondo del flusso applicativo decide la sequenza di View da mostrare all’utente. Un pattern simile anch’esso molto conosciuto è Model-View-Presenter che, a differenza di MVC, non presenta alcun “gestore” internedio tra la richiesta della View e il metodo che ne espone la logica implementativa. Quando invece tra la View e il Model utilizziamo un oggetto che si preoccupa di “adattare” il Model alle esigenze della View, stiamo parlando di Presentation Model. Martin Fowler saprà sicuramente illustrarvi meglio di me nei dettagli il funzionamento di quest’ultimo pattern: http://martinfowler.com/eaaDev/PresentationModel.html.

 

Model-View-ViewModel

Grazie alla potenza del motore di binding e commanding di WPF possiamo in maniera molto agevole adottare Presentation Model come pattern di disaccoppiamento della user interface dal modello, in particolare l’uso dello XAML, il linguaggio XML che ci permette di definire le viste in WPF,  rende l’interfacciamento tra model e view una pura questione dichiarativa.
 
Quando usato con WPF, Presentation Model assume un nome diverso: Model-View-ViewModel, che forse meglio esprime come operativamente andiamo ad implementare questo pattern. Nella sua definizione è tutto abbastanza semplice: inseriamo una classe, che chiameremo ViewModel, tra la View e il Model, dandogli la responsabilità di esporre ala view le parti del model di cui necessita
 

 
Questa classe dovrà avere alcune caratteristiche:

  • Implementare l’interfaccia INotifyPropertyChanged, in modo da essere in grado di notificare alla view gli aggiornamenti dei dati esposti
  • Esporre mediante proprietà pubbliche i dati da rendere disponibili alla view per la presentazione all’utente
  • Esporre le funzionalità disponibili mediante Command, istanze di classi che implementano l’interfaccia ICommand.
  • (Opzionalmente) Implementare l’interfaccia IDataErrorInfo per la validazione della view.

Essendo disegnata sulle esigenze di una determinata vista è chiaro che in generale avremo tante classi ViewModel quante sono le View dell’applicazione. Considerato che dal punto di vista della manutenibilità del codice questa cosa ci aiuta non poco possiamo non considerarla necessariamente uno svantaggio dell’uso del pattern, inoltre possiamo comunque crearci una serie di classi base da estendere che quantomeno ci svincolano dalle parti più noiose dei punti elencati precedentemente.

INotifyPropertyChanged

Implementare l’interfaccia INotifyPropertyChanged significa operativamente dichiarare un evento PropertyChanged di tipo PropertyChangedEventHandler:

public event PropertyChangedEventHandler PropertyChanged;

il cui delegate è così definito:

public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);

in particolare PropertyChangedEventArgs ci permette di indicare la proprietà che è stata aggiornata direttamente nel costruttore:

public PropertyChangedEventArgs(string propertyName);

ICommand

Come detto precedentemente possiamo esporre le funzionalità della nostra view esponendo delle istanze di classi che implementano l’interfaccia ICommand, che significa operativamente implementare due metodi molto esplicativi, CanExecute ed Execute: 

public bool CanExecute(object parameter) { … }

public void Execute(object parameter) { … }

La prima restituisce un booleano che informa la view della disponibilità di esecuzione del comando, la seconda invece ne implementa la logica applicativa. Viene anche definito un evento:

public event EventHandler CanExecuteChanged;

che ci permette di notificare agli ascoltatori di rivalutare CanExecute.

IDataErrorInfo

Al fine di rendere agevole la validazione delle interfacce possiamo sfruttare il supporto nativo di WPF alla gestione dell’implementazione dell’interfaccia IDataErrorInfo, la quale ci obbliga ad implementare due property:
 
public string Error { get { … } }
 
public string this[string columnName] { get { … } }
 
La prima deve restituire una stringa contenente il messaggio di errore dell’oggetto (il ViewModel nel nostro caso), la seconda invece il messaggio di errore della proprietà indicata in columnName. L’oggetto è validato correttamente quando entrambe tornano una stringa vuota, non è il massimo dell’eleganza, ma ci permette di sfruttare un meccanismo nativo di validazione direttamente nel binding di WPF.

Facciamo un esempio

 
L’unico modo di chiarire eventuali dubbi è quello di fare un esempio: supponiamo di voler implementare con l’ausilio di questo pattern la classica finestra di autenticazione di un utente. Per prima cosa creiamo la nostra view:
 

 
Creata a partire dallo XAML seguente:


 


Il nostro model sarà rappresentato da una classe che chiameremo UserCredential , che conterrà sia i dati di autenticazione che la logica di validazione delle credenziali dell’utente:

public class UserCredential
{
       public string UserID { get; set; }
       public string Password { get; set; }
 
       public bool Login()
       {
              return UserID == Password;
       }
}

Quindi per noi un utente sarà autenticato quando UserID e Password coincidono. Creiamo il nostro Command per il login:

public class LoginCommand : ICommand
{
       UserCredential _uc = null;
 
       public LoginCommand(UserCredential userCredential)
       {
              this._uc = userCredential;
       }
 
       public bool CanExecute(object parameter)
       {
              return !string.IsNullOrEmpty(_uc.UserID) && !string.IsNullOrEmpty(_uc.Password);
       }
 
       public event EventHandler CanExecuteChanged
       {
              add { CommandManager.RequerySuggested += value; }
              remove { CommandManager.RequerySuggested -= value; }
       }
 
       public void Execute(object parameter)
       {
              if (_uc.Login())
                     MessageBox.Show("Benvenuto!", "MVVM", MessageBoxButton.OK, MessageBoxImage.Information);
              else
                     MessageBox.Show("UseID o password non validi!", "MVVM", MessageBoxButton.OK, MessageBoxImage.Error);
       }
}

Creiamo il nostro ViewModel, implementando le interfacce INotifyPropertyChanged e IDataErrorInfo:

public class LoginViewModel : INotifyPropertyChanged, IDataErrorInfo
{
        private UserCredential _uc = null;
        private LoginCommand _lc = null;
 
        public LoginViewModel()
        {
            _uc = new UserCredential();
            _lc = new LoginCommand(_uc);
        }

Dopo aver dichiarato e istanziato nel costruttore il command creato e l’oggetto che conterrà le credenziali dell’utente, possiamo esporre alla view le proprietà che saranno bindate sull’interfaccia:

        public string UserID
        {
            get { return _uc.UserID; }
            set { _uc.UserID = value; }
        }
 
        public string Password
        {
            get { return _uc.Password; }
            set { _uc.Password = value; }
        }
 
        public LoginCommand LoginCommand
        {
            get { return _lc; }
        }
        public string Error
        {
            get { return string.IsNullOrEmpty(_uc.UserID) || string.IsNullOrEmpty(_uc.Password) ? "UserID e Password sono obbligatori!" : String.Empty; }
        }
 
        public string this[string columnName]
        {
            get
            {
                switch (columnName)
                {
                    case "UserID":
                        return string.IsNullOrEmpty(_uc.UserID) ? "Lo UserID è obbligatorio!" : String.Empty;
 
                    case "Password":
                        return string.IsNullOrEmpty(_uc.Password) ? "La Password è obbligatoria!" : String.Empty;
 
                    default:
                        return String.Empty;
                }
            }
        }
}

Bindiamo View e ViewModel nello XAML:


 

A questo punto ci basta istanziare il ViewModel e settarlo come DataContext della nostra view e il gioco è fatto! Naturalmente è solo un esempio, non dobbiamo certo creare una classe command per ogni funzionalità e neanche implementare le varie interfacce usate nel ViewModel ogni volta. Inoltre esistono molti framework per semplificare l'adozione di questo pattern.

Conclusioni

Il pattern Model-View-View-Model, utilizzabili in moltissimi contesti, rende le nostre applicazioni molto più semplici da manutenere e testare, permettendoci di applicare nel layer di presentation il principio di singola responsabilità in maniera concisa ed elegante. Naturalmente l'implementazione presentata in WPF era solo un esempio per chiarire i concetti e poterli riapplicare in contesti diversi.