WPF – MVVMa czas zacząć!

W poprzednim poście opisałem (bardzo ogólnie) dlaczego już nie lubię WinForms i dlaczego używam WPF. Przez używanie WPFa mam na myśli WPF+MVVM ponieważ szczerze mówiąc to uważam, że stosowanie WPFa bez MVVMa to strata czasu, ale to tylko moje zdanie (jak większość opinii wyrażona na tym blogu). Czym więc jest ten cały MVVM? Otóż jest to ni mniej ni więcej wzorzec projektowy.

Model-View-ViewModel

Na początek trochę teorii (niestety bez niej się nie da).

Model

Obiekty biznesowe, serwisy i wszystko to co jest związane z danymi i procesem ich przetwarzania.

View

User Interface tylko tyle i aż tyle. UI, który nie jest w żaden sposób powiązany z modelem. W dogmatycznym ujęciu pusty code-behind. I mimo faktu, że dogmaty zwalniają z myślenia w tym konkretnym przypadku ma to sens. Choć muszę przyznać, że w początkowym okresie niemiłosiernie kusiło człowieka aby wstawić coś w code-behind (na szczęście z czasem to przechodzi).

ViewModel

ViewModel zostawiłem na koniec ponieważ jest najtrudniejszy do opisu. W skrócie można powiedzieć, że jest to warstwa pośrednia pomiędzy View a Modelem. Tylko, że to tylko część prawdy. ViewModel stanowi reprezentację tego co się dzieje w View, ale brak jest referencji do View. Do komunikacji wykorzystuje się binding. ViewModel zawiera referencje do modelu (ale model nie ma pojęcia o ViewModelu).

Może to trochę za skomplikowane i nie czytelne, ale takie rozdzielenie ma niewątpliwe zalety. Najważniejszą z pewnością jest możliwość przetestowania ViewModelu. A ponieważ View jest powiązany z ViewModel mechanizmem bindingu to testujemy też View (chyba że coś skopaliśmy w bindowaniu).

Porównanie

No to może mały przykład. Prościutka aplikacja wpisujemy imię, nazwisko, adres a aplikacja się z nami wita (i powiedzmy zapisuje dane do bazy sprawdzając wcześniej czy istnieje już użytkownik o takich parametrach).
Sampleapp view

Bez MVVMa

Na początku przykład bez MVVM z całością w Code-Behind. Kod dostępny na GitHubie .
Kod widoku (w celu poprawy czytelności usunąłem wpisy o kolumnach):

<TextBox x:Name="FirstName"  />
<TextBox x:Name="LastName"  />
<TextBox x:Name="Address" />
<Button x:Name="SayHi" Content="Say Hi!" Click="SayHi_Click" />

a teraz code-behind

private void SayHi_Click(object sender, RoutedEventArgs e)
{
  Person person = new Person
  {
    FirstName=FirstName.Text,
    LastName=LastName.Text,
    Addres=Address.Text
  };
  if (!PersonExists(person))
  {
    MessageBox.Show(string.Format("Hi {0} {1}!", person.FirstName, person.LastName));
    SavePerosn(person);
  }
  else
  {
    MessageBox.Show(string.Format("Hey {0} {1}, you exists in our database!", person.FirstName, person.LastName));
  }
}

Z MVVMem

A teraz ten sam przypadek z wykorzystaniem MVVMa (kompletny kod na GitHubie).
Na początku czyścimy nasz View

<TextBox x:Name="FirstName"  />
<TextBox x:Name="LastName"  />
<TextBox x:Name="Address" />
<Button x:Name="SayHi" Content="Say Hi!" />

a później czyścimy code-behind.
Od tego momentu code-behind ma pozostać pusty i nieużywany. Gdzie więc podziała się logika naszej aplikacji? I w jaki sposób nastąpi przesłanie danych z View? Tutaj do akcji wkracza nasz ViewModel. Na początek tworzymy klasę viewmodelu. Ja ją nazwę MainWindowViewModel (przyzwyczajenie z Caliburn.Micro, ale o tym później).
Następnie informujemy widok o istnieniu DataContext do którego (i z którego wszak to TwoWayBinding) będziemy bindować

<Window.DataContext>
  <local:MainWindowViewModel />
</Window.DataContext>

Zastanówmy się teraz co jest nam potrzebne do prawidłowego(takiego samego) działania aplikacji. Musimy w jakiś sposób wartości z trzech TextBoxów przenieść do obiektu typu Person oraz obsłużyć Button. Zacznijmy od TextBoxów i xaml:

<TextBox x:Name="FirstName" Text="{Binding FirstName}" />
<TextBox x:Name="LastName"  Text="{Binding LastName}" />
<TextBox x:Name="Address" Text="{Binding Address}" />

Informujemy w ten sposób, że należy szukać property o określonych nazwach w naszym DataContext. Tworzymy więc takie property, które mają zapisyawać i odczytywać do obiektu typu Person. Niestety zrobienie tego tak jak poniżej nie zagwarantuje nam powodzenia:

public string FirstName
{
  get { return _person.FirstName; }
  set
  {
    _person.FirstName = value;
  }
}

Musimy bardziej się nagimnastykować. Po pierwsze nasz ViewModel musi implementować interfejs INotifyPropertyChanged. Interfejs ten wymusza aby w naszej klasie znalazł się event PropertyChanged. To teraz została do dopisania metoda za pomocą, której podepniemy handlery do obsługi zdarzenia.

private void OnPropertyChanged(string propertyName)
{
  PropertyChangedEventHandler handler = PropertyChanged;
  if (handler != null)
  {
    handler(this, new PropertyChangedEventArgs(propertyName));
  }
}

Teraz property FirstName wygląda tak:

public string FirstName
{
  get { return _person.FirstName; }
  set
  {
    _person.FirstName = value;
    OnPropertyChanged("FirstName");
  }
}

No to teraz obsługa buttona.

<Button x:Name="SayHi" Content="Say Hi!" Command="{Binding SayHi}" />

A teraz ViewModel tworzymy property typu ICommand, ale żeby nie było tak różowo to musimy jeszcze stworzyć klasę implementującą ICommand. Nie będę oryginalny i po prostu skopiowałem taką klasę z internetu (niestety zrobiłem tak dawno temu że nie mogę znaleźć źródła).

public class RelayCommand : ICommand
{
  private readonly Func<Boolean> _canExecute;
  private readonly Action _execute;

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

  public RelayCommand(Action execute, Func<Boolean> canExecute)
  {
    if (execute == null)
      throw new ArgumentNullException("execute");
    _execute = execute;
    _canExecute = canExecute;
  }

  public event EventHandler CanExecuteChanged
  {
    add
    {
      if (_canExecute != null)
        CommandManager.RequerySuggested += value;
    }
    remove
    {
      if (_canExecute != null)
        CommandManager.RequerySuggested -= value;
    }
  }

  public Boolean CanExecute(Object parameter)
  {
    return _canExecute == null ? true : _canExecute();
  }

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

Radzę zwróćić uwagę na to co przekazujemy do konstruktora(ów). Po pierwsze metodę, którą chcemy wykonywać a po drugie informację czy możemy tą metodę wywołać.

Będąc już tak przygotowani możemy powoli kończyć budowanie naszego ViewModelu. W celu obsłużenia zdarzenia wywołanego przez Button SayHi uzupełniamy nasz ViewModel w następujący sposób:

public ICommand SayHi { get { return new RelayCommand(SayHiExcute, CanSayHiExcute); } }

private void SayHiExcute()
{
  if (!PersonExists(_person))
  {
    MessageBox.Show(string.Format("Hi {0} {1}!", _person.FirstName, _person.LastName));
    SavePerosn(_person);
  }
  else
  {
    MessageBox.Show(string.Format("Hey {0} {1}, you exists in our database!", _person.FirstName, _person.LastName));
  }
}

private bool CanSayHiExcute()
{
  //Some logic
  return true;
}

Jeśli ktoś się zastanawia dlaczego nie dałem sprawdzania czy dana osoba istnieje w bazie danych w metodzie CanSayHiExcute() to pragnę poinformować go, że miałem swój powód. Powodem tym była chęć doprowadzenia obu wariantów (bez i z MVVM) do takiego samego zachowania. Gdyby metoda CanSayHiExcute zwróciła false Button SayHi byłby nieaktywny!

Podsumowanie

Porównując oba sposoby (z MVVM i bez) na pierwszy rzut oka ten bez zastosowania MVVM wydaje się prostszy. Na plus dla MVVM przemawia testowalność i oddzielenie widoku od modelu. Za metodą standardową przemawia na pewno konieczność wklepania mniejszej ilości kodu. Specjalnie użyłem słowa wklepania bo do tego to się sprowadza: do ręcznego wpisania (przyklejenia) sporej ilości kodu. I nieważne jak R# mógłby w tym pomóc to dalej byłaby robota głupiego.
Jeśli ktoś tak sądzi to … muszę się z nim zgodzić. Jednakże całą tą “zabawę” z property skutecznie ogranicza Fody. A na ICommand mam jedną odpowiedź (wspominałem już o tym wcześniej): Caliburn.Micro. Ale o tym w następnych postach.

12 thoughts on “WPF – MVVMa czas zacząć!

  1. Pingback: dotnetomaniak.pl
  2. Ile przewidujesz wpisów na temat MVVM? Mógłbyś zrobić rozpiskę co będzie w kolejnych wpisach? Thx 🙂

    1. Co najmniej trzy wpisy o Caliburn.Micro. A jak się uda to jakieś porównanie z MVVM Light.
      Dzięki za pomysł z listą w następnym wpisie postaram się takową umieścić.

    1. CallerMemberName nie działa niestety pod .net 4.0. Dlatego ja preferuje PropertyChanged.Fody. Działa również w starszych wersjach .net a poza tym wymaga mniejszej ilości kodu (wygoda ponad wszystko)

  3. Co do pustego code-behind – jak wtedy radzić sobie z MessageBoxami, dodatkowymi oknami dialogowymi (np. SaveDialogBox), oknami właściwości itp?

      1. A jeśli rozdzielam solution na model (dll), widok (exe) i testy, to gdzie umieścić ViewModel? Rozumiem, że wtedy musi być z widokiem, bo musi “umieć” np. pokazać okno.
        No i jeszcze pytanie co z innymi zdarzeniami okna, bądź kontrolek wewnątrz okna (typu Loaded, Closed, MouseDown…)
        Nie do końca wyobrażam sobie jak projektować, żeby zupełnie pozbyć się code-behind, ale chętnie bym przeczytał w którymś kolejnym wpisie, albo w jakimś oddzielnym źródle na ten temat trochę więcej.

  4. A mi się marzy jakiś bardziej rozbudowany przykład użycia wzorca mvvm ponieważ w sieci istnieje wiele poradników i tutoriali ale brakuje takich bardziej kompleksowych gdzie byłoby pokazane jak utworzyć aplikację np. używającą kilku okien i wczytywania danych np, z bazy accesowej, tak aby były one dostępne we wszystkich okienkach itp., albo jak prawidłowo obsłużyć drag i drop w aplikacji wpf z użyciem mvvm.
    Dzięki za dotychczasowe wpisy na temat wzorca MVVM.

    1. Tutoriale już tak mają, że są ogólne. Na ich podstawie można budować szczegółowe rozwiązania (bo każdy może potrzebować czegoś innego).

      1. Właśnie to, że są ogólne powoduje trudności w odbiorze i pełnym zrozumieniu zagadnienia bo pomijane jest omówienie problemów, które mnożą się jeśli chcemy wykorzystać 2 lub więcej tutoriali.

        1. Niestety tak to już bywa. Omawiając konkretną ścieżkę postępowania skupiamy się tylko na wybranych problemach. W innej ścieżce problemy będą inne. Jeśli nagle w przykładzie MVVMa zobaczę obsługę czegoś czego kompletnie nie znam to zamiast próbować zrozumieć MVVMa muszę zrozumieć coś innego (czasem coś czego w danym momencie nie potrzebuję). Chodzi o to aby móc wybrać to co nas interesuje (to co nam jest potrzebne). Chociaż pomysł aby na koniec pokazać jakąś kompletną aplikację z pewnością jest ciekawy.

Comments are closed.