Wprowadzenie do wzorca projektowego Model-View-ViewModel na przykładzie aplikacji WPF  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2012-01-20

Wprowadzenie

Wzorce projektowe mają na celu rozwiązanie najczęściej spotykanych problemów związanych z pisaniem kodu. W przypadku warstwy prezentacji można wykorzystać m. in. następujące rozwiązania: MVC, MVP czy Model-View-ViewModel. Ze względu na mechanizm wiązań (binding), programistom WPF oraz Silverlight, polecany jest wzorzec MVVM – jest to technologia umożliwiająca bardzo łatwą implementację wzorca.

Korzyści

Przed omówieniem wzorca, warto zastanowić się, po co utrudniać sobie zadanie poprzez wykorzystywanie MVVM, zamiast pisać aplikację w klasyczny sposób (za pomocą code-behind)? W końcu wdrożenie praktycznie każdego wzorca projektowego wymaga trochę większych początkowych nakładów pracy.

Podejście Code-Behind (autonomous view – AV) ma poważną wadę – nie gwarantuje elastyczności oraz testowalności. Podsumowując, wprowadzenie wzorca umożliwia:

  • niezależność logiki od sposobu wyświetlania danych,
  • niezależność kodu od technologii, w której wykonana jest warstwa prezentacji,
  • wykonywanie testów – za pomocą MVVM czy MVP możliwe jest wykonanie testów zautomatyzowanych (np. jednostkowych),
  • łatwą zamianę widoków (brak sztywnych powiązań między widokiem a logiką).

Zasada działania

Wzorzec MVVM, jak sama nazwa wskazuje, składa się z 3 elementów. Są to:

  • Model,
  • View (widok),
  • ViewModel – model przystosowany do współpracy z widokiem.

Powyższe elementy zostaną wyjaśnione w dalszej części artykułu wraz z przykładową aplikacją WPF wstawiającą proste dane (imię, nazwisko) do bazy danych.

Widok

Zadaniem widoku jest wyświetlanie danych – pełni on wyłącznie funkcję prezentacyjną. Nie powinien wykonywać żadnej logiki (biznesowej, czy związanej z przepływem screen’ów). W przypadku WPF i Silverlight, widokiem jest plik XAML. Nie powinno się dodawać kodu w code-behind – jest to sprzeczne z zasadami MVVM, ponieważ wszelka logika (kod) jest zawarty w Model lub ViewModel. Widok w MVVM jest aktywny, ponieważ musi znać budowę ViewModel po to, aby poprawnie skonfigurować wiązania (wymagane są tutaj np. nazwy właściwości).

Przykład:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="163" Width="282">
    <Grid>
        <StackPanel>
            <TextBox Height="23"   Name="FirstName" Text="{Binding FirstName}" Width="120" />
            <TextBox Height="23"   Name="LastName" Text="{Binding LastName}" Width="120" />
            <Button Command="{Binding SaveCmd}" Height="23" Width="80">Zapisz</Button>
        </StackPanel>
    </Grid>
</Window>

Powyższy przykład zawiera dwa pola edycyjne, przeznaczone do wpisania imienia i nazwiska, oraz pojedynczy przycisk, po kliknięciu którego dane zostaną zapisane. Warto zwrócić uwagę, że właściwość Text została powiązana z danymi, które znajdą się w ViewModel. Przycisk również jest powiązany z odpowiednią komendą. W pliku cs (code-behind) nie należy wykonywać żadnej obsługi. W standardowym podejściu, należy stworzyć zdarzenie dla przycisku (OnClick) i w code-behind wykonać jego obsługę (zapis do bazy). W MVVM za zapis będzie odpowiedzialny ViewModel (osobna klasa), dzięki czemu nie będzie potrzeby mieszania logiki prezentacji z biznesową w code-behind. Ułatwi to także odseparowanie widoku od modelu.

ViewModel oraz Model

ViewModel stanowi klasę, która eksponuje model dla widoku. Czysty model, zawierający logikę biznesową, prawdopodobnie nie będzie nadawał się do użycia w widoku – np. z powodu braku pewnych publicznych właściwości. Ponadto ViewModel, oprócz danych oraz logiki, zawiera komendy powiązane z elementami GUI w widoku. Innymi słowy, ViewModel zawiera to, co wcześniej było umieszczone w code-behind.

Przykład:

public class SampleViewModel:INotifyPropertyChanged
{
        public SampleViewModel()
        {
            SaveCmd = new RelayCommand(pars => Save());
        }
        public event PropertyChangedEventHandler PropertyChanged = null;
        public ICommand SaveCmd { get; set; }

        private string _FirstName = null;
        public string FirstName
        {
            get
            {
                return _FirstName;
            }
            set
            {
                _FirstName = null;
                OnPropertyChanged("FirstName");
            }
        }
        private string _LastName = null;
        public string LastName
        {
            get
            {
                return _LastName;
            }
            set
            {
                _LastName = null;
                OnPropertyChanged("LastName");
            }
        }
        private void Save()
        {
            // logika odpowiedzialna za zapis
        }
        virtual protected void OnPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }       
    }

Powyższy kod jest implementacją prostego modelu. Eksponuje dwie właściwości (imię, nazwisko) oraz komendę – SaveCmd. RelayCommand jest prostą klasą, implementującą komendę (źródło MSDN Magazine):

public class RelayCommand : ICommand
    {
        #region Fields
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;
        #endregion // Fields

        #region Constructors

        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }
        #endregion // Constructors

        #region ICommand Members

        [DebuggerStepThrough]
        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        #endregion // ICommand Members
    }

RelayCommand po prostu przekazuje wykonanie komendy do metody wskazanej w konstruktorze. Warto zastanowić się nad hierarchią klas. W praktyce tworzony jest bazowy ViewModel, który zawiera wspólne części (np. OnPropertyChanged) oraz pochodne dla konkretnych widoków.

Podsumowując powyższe rozważania , przy zastosowaniu MVVM zachodzi potrzeba przestrzegania następujących założeń:

  • unikania kodu w code-behind – w większości przypadków to, co kiedyś było robione w code-behind, można przenieść do ViewModel,
  • zdarzenia powinny zostać zastąpione komendami, np. zamiast podpinać zdarzenie Click, należy skorzystać z komendy; oczywiście istnieją przypadki, w których zdarzenia są jedynym rozwiązaniem,
  • ViewModel powinien implementować interfejs INotifyPropertyChanged,
  • dane z widoku powinny być powiązane z właściwościami w ViewModel,
  • w testach sam ViewModel powinien wystarczyć; widok jest tak naprawdę wizualizacją przeznaczoną dla użytkownika; użytkownik, chcąc skorzystać z logiki dostarczonej przez aplikację, wprowadza tekst np. za pomocą TextBox – w testach jednostkowych ustawiamy właściwość w VM i powinniśmy uzyskać taki sam efekt,
  • należy rozróżnić Model od ViewModel; model nie może zawierać żadnej logiki, związanej z widokiem; innymi słowy, model to czysta logika biznesowa, z kolei ViewModel zawiera już informacje o stanie widoku.

Połączenie ViewModel z widokiem

ViewModel musi zostać przekazany do DataContext np.:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new SampleViewModel();
        }
    }

Warto zastanowić się nad użyciem IoC (np. UnityContainer) w celu zastosowania ViewModel.

Zdarzenia a MVVM

Powyższe przykłady nie pokazują, jak wykorzystać zdarzenia z WPF – np. Click. Niestety WPF nie dostarcza łatwego mechanizmu, umożliwiającego wiązanie zdarzeń. Efekt można jednak uzyskać za pomocą zachowań (attached behaviour). Zachowania to obiekty dołączane do różnych kontrolek, które potrafią zmieniać ich zachowanie. Klasa implementująca zachowanie ma więc dostęp do kontrolki i może odpowiednio w tle podpiąć zdarzenie i wywołać komendę. Powstała już biblioteka o nazwie AttachedCommandBehavior. Biblioteka ta umożliwia podpięcie zdarzenia do komendy:

<Border Background="Aqua" 
local:CommandBehavior.Event="MouseDown" 
local:CommandBehavior.Command="{Binding SomeCommand}"
local:CommandBehavior.CommandParameter="{Binding}"/>

Frameworki

W sieci można znaleźć wiele bibliotek, które ułatwiają pracę z wzorcem MVVM. Szczególnie polecam zapoznanie się z Caliburn Micro oraz Prism. Caliburn Micro umożliwia automatyczne wiązanie na podstawie nazw zmiennych i kontrolek, np. właściwość FirstName zostanie automatycznie skojarzona z kontrolką o nazwie FirstName. Podobnie sprawa wygląda z zastosowaniem ViewModel w widoku, np. PersonViewModel zostanie umieszczony automatycznie w widoku o nazwie PersonView. Z kolei PRISM jest potężnym frameworkiem, przeznaczonym do tworzenia aplikacji WPF, i nie ogranicza się tylko do MVVM (zawiera m.in. mechanizm dynamicznego tworzenia interfejsu na podstawie bibliotek DLL).

Dodatkowo zobacz:

Zakończenie

Za pomocą wzorca MVVM, programista zyskuje kod, łatwy do testowania, z rozdzieloną logiką. Pomimo wielu wzorców (MVC, MVP), MVVM wydaje się naturalnym podejściem dla aplikacji WPF ze względu na mechanizm wiązania danych. Dostępne frameworki jeszcze bardziej upraszczają pisanie kodu, zgodnego z MVVM. Klasyczne podejście code-behind może wydawać się prostsze w początkowych fazach projektu, jednak z czasem, wraz z rozwojem aplikacji, czas poświęcany na utrzymanie kodu znacząco się wydłuża.


Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.