wtorek, 15 listopada 2011

Wzorzec MVVM

Wzorzec MVVM (Model-View-ViewModel) umożliwia oddzielenie warstwy prezentacji od warstwy logiki aplikacji. Dzięki temu można w łatwy sposób stworzyć aplikację która jest testowalna, prosta w rozbudowie i pozwala na uzyskanie wysokiej reużywalności kodu w przyszłych projektach.
MVVM jest bardzo popularny w środowisku programistów WPF i Silverlight gdyż umożliwia wykorzystanie najważniejszych atutów tego frameworku: bindowania, szablonów wyświetlania danych, komend (command), zachowań (behaviors).

Widok
W idealnym rozwiązaniu kod widoku zawiera tylko konstruktor wywołujący metodę InitializeComponent(). Można spotkać się jednak z kodem który nie jesteśmy w stanie wyrazić za pomocą XAMLa np. skomplikowaną animacją.
Widok porozumiewa się z modelem za pomocą ViewModel. Komponenty widoku są bindowane np. za pomocą komend z odpowiednimi metodami w kodzie ViewModel. ViewModel jest połączony z View za pomocą właściwości DataContext którą udostępnia View.

ViewModel
Reprezentuje dane wysyłane do widoku - nie mając przy tym żadnej referencji do widoku. Widok za pomocą komend (command) oraz bindowania do właściwości odwołuje się do składników ViewModel. Takie rozwiązanie umożliwia testowanie ViewModelu odrębnie od części prezentacyjnej oraz modelu. Często w ViewModel-u wprowadza się dodatkową walidację czy też dodatkową logikę przedstawianą w warstwie prezentacji dla użytkownika.
Jak rozpoznać czy daną operację umieścić w widoku czy też w ViewModel? Odpowiedź jest prosta:
View - wszystko co jest związane z prezentacją danych użytkownikowi
ViewModel - wszystko co związane z logiką i danymi

Model
Odpowiada w omawianym wzorcu za logikę biznesową aplikacji. Często jest tworzony przy użyciu obiektowych mapperów baz danych (np. nHibernate, Entity Framework, LINQ2SQL, WCF Services itp). Klasy w modelu najczęściej implementują interfejs INotifyPropertyChanged, który współpracuje z bindingiem wykorzystywanym w WPF oraz Silverlight.
Korzystając z interfejsu IDataErrorInfo możemy zaimplementować sprawdzanie poprawności wprowadzanych danych przez użytkownika.
Patrząc z perspektywy View i ViewModel, Model nie ma referencji do żadnej z tych warstw - jest od nich całkowicie niezależny.

Bindowanie danych w WPF
WPF oferuje bardzo rozbudowany system bindowania danych do kontrolek wizualnych. Aby wykorzystać możliwości WPF należy wcześniej poznać niektóre przydatne interfejsy i klasy, dzięki którym będziemy mogli powiadomić użytkownika o zmianach w danych w niższych warstwach i odzwierciedlić je w widoku.
INotifyPropertyChanged
Implementując ten interfejs umożliwiamy powiadomienie użytkownika o zmianie wartości pola. Przykład takiej implementacji:


Code:
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _firstName;

        public string FirstName
        {
            get { return _firstName; }
            set
            {
                _firstName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("FirstName"));
                }
            }
        }

        private string _lastName;

        public string LastName
        {
            get { return _lastName; }
            set
            {
                _lastName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("LastName"));
                }
            }
        }
    }

Implementacja jest prosta i intuicyjna - dla każdej właściwości podczas zmiany jej wartości sprawdzamy czy ktoś nie "nasłuchuje". W przypadku gdy tak jest wysyłamy powiadomienie o tej sytuacji. Niestety rozwiązanie ma jedną wadę: mnożymy kod dwa zmieniając nazwę zmiennej musimy pamiętać o zmianie w argumencie PropertyChangedEventArgs - jest to często powód wielu ukrytych błędów które później mogą dać nam po palić. W Prism przedstawiono klasę po której możemy dziedziczyć i wykorzystać aby wysyłać powiadomienia korzystając z Lambda Expression:


Code:
    public class Computer : NotificationObject
    {
        private string _processor;

        public string Processor
        {
            get { return _processor; }
            set
            {
                _processor = value;
                RaisePropertyChanged(() => Processor);
            }
        }

        private string _graphicCard;

        public string GraphicCard
        {
            get { return _graphicCard; }
            set
            {
                _graphicCard = value;
                RaisePropertyChanged(() => GraphicCard);
            }
        }
    }

ICollectionView
Jest to kolejny, ciekawy interfejs specjalnie przygotowany dla kolekcji obiektów które mają być wyświetlane w kontrolkach typu listbox, grdiview itp. Interfejs ten umożliwia m.in. filtrowanie, sortowanie i grupowanie danych. W WPF mamy do dyspozycji klasę która implementuje ten interfejs - ListCollectionView, a w Silverlight PagedCollectionView.
Przykład:


Code:
    public class PersonViewModel
    {
        public ICollectionView Computer { get; set; }

        public PersonViewModel()
        {
            Computer = new ListCollectionView(ComputerList.Computers());
            Computer.CurrentChanged += new EventHandler(Persons_CurrentChanged);
        }

        void Persons_CurrentChanged(object sender, EventArgs e)
        {
            var computer = Computer.CurrentItem as Computer;
            if (computer != null)
            {
                ...   
            }
        }
    }    public class PersonViewModel
    {
        public ICollectionView Computer { get; set; }

        public PersonViewModel()
        {
            Computer = new ListCollectionView(ComputerList.Computers());
            Computer.CurrentChanged += new EventHandler(Persons_CurrentChanged);
        }

        void Persons_CurrentChanged(object sender, EventArgs e)
        {
            var computer = Computer.CurrentItem as Computer;
            if (computer != null)
            {
                ...   
            }
        }
    }
Implementacja jak widać bardzo prosta: w klasie dodajemy pole typu naszego interfejsu i tworzymy klasę odpowiednią dla Silverlight bądź też WPF.

Polecenia (Commands)
Pominę w dalszej części polskie tłumaczenie tego słowa (angielskie wydaje mi się bardziej trafne). W największym uproszczeniu odpowiadają one eventą - np. Button_Click. W rzeczywistości stanowią bardziej abstrakcyjny byt, który nie jest zależny od interfejsu. Dzięki uzależnieniu commands od widoku, uzyskujemy kod który możemy wykorzystać w wielu miejscach naszego programu a dodatkowo jest on testowalny. Standardowa implementacja polega na zaimplementowaniu interfejsu ICommand:


Code:
    public class AddNewPersonCommand : ICommand
    {
        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
            ...Do something
        }
    }

Implementacja prosta, choć biblioteka Prism dostarcza jeszcze prostszą metodę na implementację command - DelegateCommand:


Code:
    public class PersonViewModel
    {
        public DelegateCommand AddPersonToRepository { get; set; }

        public PersonViewModel()
        {
            AddPersonToRepository = new DelegateCommand(() =>
                                                            {
                                                            ...Do something 
                                                            }, () => { return true; });
        }
    }

W metodzie podajemy co ma zrobić komenda oraz warunek determinujący kiedy ma zostać wykonana. Do komendy możemy przesłać także jakąś wartość - dlatego mamy możliwość stworzenia generycznej wersji DelegateCommand


IDataErrorInfo
Interfejs ten umożliwia walidację danych i powiadomienie użytkownika o nieprawidłowościach we wprowadzanych danych.

Code:
   public class Computer : NotificationObject, IDataErrorInfo
    {
        private string _processor;

        public string Processor
        {
            get { return _processor; }
            set
            {
                _processor = value;
                RaisePropertyChanged(() => Processor);
            }
        }

        private string _graphicCard;

        public string GraphicCard
        {
            get { return _graphicCard; }
            set
            {
                _graphicCard = value;
                RaisePropertyChanged(() => GraphicCard);
            }
        }

        public string Error
        {
            get
            {
                return null;
            }
        }

        public string this[string columnName]
        {
            get 
            {
                switch (columnName)
                {
                    case "Processor":
                        ...validate
                        break;
                }
            }
        }
    }
Metoda Error zwraca wszystkie błędy, zaś indexer błąd dla konkretnego pola w klasie. Należy więc rozważyć implementację metod - nie mogą być zbyt czasochłonne by nie obciążyć zbytnio aplikacji. Każda zmiana właściwości spowoduje wywołanie walidacji tak więc kod walidacji powinien być w miarę prosty i szybki.
Odwołanie w ViewModel do tak stworzonej walidacji wygląda następująco:


Code:
<TextBox VerticalAlignment="Center" HorizontalAlignment="Center"
            Text="{Binding Path=Computer.Processor, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True }"/>


Przedstawione tutaj zagadnienia to podstawy MVVM. Polecam zgłębiać wiedzę w tym zakresie, gdyż raz poznając MVVM nie będziemy chcieli tworzyć codebehind.

3 komentarze:

  1. Na wstępie jest mały błąd:

    "umożliwia oddzielenie warstwy prezentacji od warstwy prezentacji"

    Pozdrawiam

    OdpowiedzUsuń
  2. "Klasy w modelu najczęściej implementują interfejs INotifyPropertyChanged, który współpracuje z bindingiem wykorzystywanym w WPF oraz Silverlight."

    Nie modelu, tylko viewModel:

    "ViewModel powinien implementować interfejs INotifyPropertyChanged"
    http://msdn.microsoft.com/pl-pl/library/wprowadzenie-do-wzorca-projektowego-model-view-viewmodel-na-przykladzie-aplikacji-wpf.aspx

    "The view model typically does not directly reference the view. It implements properties and commands to which the view can data bind. It notifies the view of any state changes via change notification events via the INotifyPropertyChanged and INotifyCollectionChanged interfaces."
    http://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx

    OdpowiedzUsuń