niedziela, 19 września 2010

Podstawy MVVM Light Toolkit [część 1]

Instalacja MVVM Light Toolkit
Właściwie jedyne co jest niezbędne do pracy z MVVM LT, to same biblioteki w formie plików DLL, jednak aby naprawdę wygodnie korzystać z tego narzędzia dobrze jest skorzystać z małych wspomagaczy przygotowanych przez autora. Pobieramy paczkę stąd i następnie wypakowujemy to co nam potrzeba. Jest tego całkiem sporo, ponieważ przygotowane są wersje zależne od wykorzystywanej technologii i oprogramowania:

  • binarki - biblioteki w postaci plików DLL w postaciach przygotowanych dla WPF'a w wersji 3.5 i 4.0, Silverlight'a 3.0 oraz 4.0 a także dla Windows Phone 7
  • szablony projektów - szablony w wersjach przeznaczonych zarówno dla Expression Blend (również wersja 3 i 4) oraz dla Visual Studio (osobno dla 2008 i 2010, także w wersji Express); 
  • szablony składników projektu (Item Templates) - pozwalają na łatwe dodanie składników takich jak nowy widok lub ViewModel
  • snippety - zbiór fragmentów kodu pozwalających wygenerować składniki narzędzia takie jak Dependency Property czy Locator Property

Przed rozpakowaniem warto pamiętać o odblokowaniu archiwów (przynajmniej w Winows 7, nie wiem jak w innych systemach), opcja dostępna we właściwościach pliku. 
Ogólnie zasada jest prosta: pliki szablonów rozpakowuje do katalogu szablonów, a snippety do katalogu snippetów :).  Ich położenie można znaleźć w ustawieniach Visual Studio. Dla szablonów sprawdzamy w Tools -> Options -> Project and Solutions, natomiast dla snippetów Tools -> Code Snippet Manager -> My Code Snippets. Po rozpakowaniu i restarcie Visual Studio możemy cieszyć się nowymi elementami.

Przykładowy projekcik
Aby przedstawić jak tworzy się programy z wykorzystaniem MVVM Light Toolkit, napiszę małą gierkę w "Zgadywanie liczby". Jedno z pierwszych zadań programistycznych, gdy uczymy się nowego języka. Program losuje jakąś liczbę z zadanego przedziału, a użytkownik ma ją odgadnąć z pomocą wskazówek typu "za dużo" lub "za mało"
Zacznę od zwykłego projektu WPF Application, żeby pokazać z jakich składników budujemy aplikację opartą na MVVMLT, takie podejście ma również zastosowanie, gdy chcemy przystosować do stosowania narzędzia już istniejący projekt. Oczywiście w normalnym przypadku po prostu stworzyłbym projekt z gotowego szablonu. Na samym początku warto dodać referencję do biblioteki GalaSoft.MvvmLight.WPF4.dll.

ViewModel
Zaczynam od stworzenia obiektu ExampleViewModel, który będzie powiązany z głównym widokiem, dla wygody i porządku umieszczam go w katalogu ViewModel. Jest to prosta klasa dziedzicząca po ViewModelBase, w której to autor toolkita zawarł wiele wspomagaczy. Ta klasa będzie odpowiedzialna za przechowywanie danych i odpowiednie reagowanie na zachowania użytkownika. 
Dodam właściwość SecretNumber typu int, reprezentującą liczbę, którą należy odgadnąć Suggestion typu string, w której zapisywane będą sugestie odnośnie wybranej liczby oraz dwie właściowści typu bool IsInProgress oraz IsSolved, przedstawiające czy aktualnie użytkownik jest w trakcie zgadywania oraz czy liczba została odgadnięta (posłużą one do pokazywania i ukrywania odpowiednich komunikatów). We wszystkich podczas ustawiania wartości wywołam funkcję RaisePropertyChanged() z klasy bazowej, opakowuje ona funkcjonalność interfejsu INotifyPropertyChanged. Dzięki temu będzie możliwe skorzystanie z dobrodziejstw Data Bindingu i przy każdej zmianie wartości zmiennej zarówno w kodzie, jak i przez użytkownika, jej wartość będzie na bieżąco aktualizowana. Aby nie wpisywać wszystkiego "z palca" najlepiej jest skorzystać ze snippetu mvvminpc
#region SecretNumber Property
public const string SecretNumberPropertyName = "SecretNumber";
private int _secretNumber = 0;

public int SecretNumber
{
 get
 {
  return _secretNumber;
 }

 set
 {
  if (_secretNumber == value)
   return;

  _secretNumber = value;

  RaisePropertyChanged(SecretNumberPropertyName);
 }
} 
#endregion

#region Suggestion Property
public const string SuggestionPropertyName = "Suggestion";
private string _suggestion = "";

public string Suggestion
{
 get
 {
  return _suggestion;
 }

 set
 {
  if (_suggestion == value)
   return;

  _suggestion = value;

  RaisePropertyChanged(SuggestionPropertyName);
 }
}
#endregion

#region IsInProgress Property
public const string IsInProgressPropertyName = "IsInProgress";
private bool _isInProgress = false;

public bool IsInProgress
{
 get
 {
  return _isInProgress;
 }

 set
 {
  if (_isInProgress == value)
   return;

  _isInProgress = value;

  RaisePropertyChanged(IsInProgressPropertyName);
 }
}
#endregion

#region IsSolved
public const string IsSolvedPropertyName = "IsSolved";
private bool _isSolved = false;

public bool IsSolved
{
 get
 {
  return _isSolved;
 }

 set
 {
  if (_isSolved == value)
   return;

  _isSolved = value;

  RaisePropertyChanged(IsSolvedPropertyName);
 }
}
#endregion
ViewModel Locator
W MVVM Light Toolkit klasa ViewModel Locator odpowiada za przetrzymywanie referencji do wszystkich obiektów ViewModel wykorzystywanych w projekcie. Dzięki takiemu zabiegowi można łatwo tworzyć dowiązania do właściwości i komend w różnych obiektach ViewModel. Pozwala to także w Expression Blend stworzyć Object Data Source wskazujący właśnie na tę klasę i wszystkie bindingi tworzyć przez wygodny interfejs lub proste drag&drop. 
Tym razem również można skorzystać z przygotowanych szablonów pochodzących z paczki MVVM Light Toolkit. Również do katalogu ViewModel dodajemy nową pozycję MVVMViewModelLocator (WPF), a następnie w ciele klasy korzystamy ze snippetu mvvmlocatorproperty. W ramach pól snippetu podajemy nazwę klasy będącej powiązaniem między widokiem a modelem (ExampleViewModel) oraz nazwę właściwości jaka ma być dostępna na zewnątrz (Example). Po tych czynnościach budujemy projekt. 
Aby powiązać obiekt ViewModel Locator z naszą aplikacją najlepiej dodać ją jako zasób w pliku App.xaml, poprzez to będzie on dostępny w całej aplikacji, a poszczególne obiekty ViewModel będzie można przypisać jako Data Context bezpośrednio w widoku odwołując się do głównego Locatora. Można to zrobić na dwa sposoby: wpisać "z palca" w kodzie lub wykorzystać wygodny interfejs Expression Blend. Jeśli mamy do dyspozycji tylko Visual Studio w pliku App.xaml w tagu <Application.Resources>
 

pamiętając jednocześnie o wprowadzeniu nowej przestrzeni nazw:
xmlns:vm="clr-namespace:MVVMLightToolkit.ViewModel"
Teraz w każdym widoku możemy zbindować odpowiedni obiekt ViewModel:
 
  
Jeśli obiekt nie ustawi własnego DataContext, to jest on dziedziczony z kontrolki nadrzędnej przez co ustawiając go dla obiektu Window zapewniamy ten sam kontekst dla wszystkich potomków.
Aby uzyskać taki sam efekt korzystając z Blend'a, w zakładce Data, wybieramy Create Object Data Source i nadajemy mu nazwę Locator. 
Następnie możemy już przeciągnąć z listy obiekt Example, reprezentujący nasz aktualny obiekt ViewModel, na przykład na obiekt Window.

Tworzenie komend
Na początek stworzymy dwie komendy: Start, która wylosuje nową liczbę do odgadnięcia i jednocześnie ukryje lub pokaże odpowiednie kontrolki, oraz CheckGuess, która będzie miała za zadanie sprawdzenie czy podana przez użytkownika liczba jest prawidłowa i ewentualne podanie wskazówek.
W MVVM Ligth Toolkit za reprezentowanie komend odpowiada klasa RelayCommand, która opakowuje interfejs ICommand, dając jednocześnie wygodny sposób tworzenia nowych komend, bez potrzeby zagłębiania się w implementację. Klasa ta w konstruktorze oczekuje obiektu typu Action, czyli prościej mówiąc delegacji która nie przyjmuje parametrów oraz nic nie zwraca, który będzie odpowiadał funkcji Execute() z interfejsu ICommand. Metoda ta jest wykonywana w momencie wywołania komendy. Można również podać drugi argument będący obiektem typu Predicate (delegacja nie przyjmująca parametrów i zwracająca bool), który będzie implementował metodę CanExecute(), określa ona czy spełnione są warunki, aby móc wykonać daną komendę. Istnieje również wersja generyczna obiektu RelayCommand, pozwala ona na przekazywanie parametru do komendy.
Do implementacji prostych komend można skorzystać z wyrażeń lambda:
#region Start
private RelayCommand _startCommand;

public RelayCommand Start
{
 get {
  if (_startCommand == null)
  {
   _startCommand = new RelayCommand(
    () =>
    {
     SecretNumber = rand.Next(10) + 1;
     IsSolved = false;
     IsInProgress = true;
     Suggestion = "";
    }
   );
  }
  return _startCommand; }
}

#endregion

#region CheckGuess Command
private RelayCommand<int> _checkGuess;

public RelayCommand<int> CheckGuess
{
 get {
  if (_checkGuess == null)
   _checkGuess = new RelayCommand<int>(
    x =>
    {
     if (x == _secretNumber)
     {
      IsInProgress = false;
      IsSolved = true;
     }
     else if (x < _secretNumber)
      Suggestion = "to low";
     else
      Suggestion = "to high";
    });

  return _checkGuess; 
 }
}
#endregion
Komenda Start, jedynie losuje nową sekretną liczbę i ustawia właściwości określające stan zabawy na odpowiednie wartości logiczne. W komendzie CheckGuess, wykorzystujemy przekazany parametr do sprawdzenia czy użytkownik zgadł ukrytą liczbę, jest ona przekazywana poprzez binding z kontrolki.

Interfejs
Na potrzeby przykładu stworzyłem jak najprostszy interfejs użytkownika pozwalający na wprowadzenie danych i interakcję. 
Uproszczony kod XAML, który go generuje:

 
  
  
 
 
 
  
   
   
  
  
  
 


Tworzenie bindingów
Najciekawszą czynnością jest utworzenie odpowiednich powiązań pomiędzy widokiem i obiektem ViewModel. Niektóre są naprawdę proste i nie wymagają większego komentarza, gdyż jedynie wiążą jedną właściwość lub komendę, jak na przykład w przypadku przycisku rozpoczynającego grę:

To samo jest z polami tekstowymi txblSecret oraz txblSugestion, powiązanymi odpowiednio z właściwościami SecretNumber oraz Sugestion.
Nieco bardziej skomplikowane jest powiązanie przycisku odpowiedzialnego za sprawdznie czy użytkownik podał właściwą liczbę:
Binding ustawia komendę wykonywaną w momencie wciśnięcia przycisku na CheckGuess, jako jej parametr podaje właściwość Text, pola tekstowego txbUserGuess. Jednak komenda CheckGuess przyjmuje jako parametr obiekt typu int, a właściwość Text jest stringiem, dlatego też musi nastąpić konwersja pomiędzy oba typami. Do tego celu stworzyłem prosty obiekt StringToIntConverter, który dokonuje takiej zamiany w obie strony. Dziedziczy on po interfejsie IValueConverter i implementuje jego dwie metody: Convert oraz ConvertBack:
[ValueConversion(typeof(string), typeof(int))]
class StringToIntConverter : IValueConverter
{
 #region IValueConverter Members

 public object Convert(object value, Type targetType,
   object parameter, System.Globalization.CultureInfo culture)
 {
  string str = value.ToString();

  if (string.IsNullOrEmpty(str))
   return 0;
  else
   try
   {
    int result = 0;

    int.TryParse(str, out result);
    return result;
   }
   catch (ArgumentException)
   {
    return 0;
   }
 }

 public object ConvertBack(object value, Type targetType,
   object parameter, System.Globalization.CultureInfo culture)
 {
  return value.ToString();
 }

 #endregion
}
Konwerter ten dodajemy jako zasób obecnego widoku:

Dodatkowo, żeby nie zdradzać użytkownikowi naszej sekretnej liczby, należy ukryć w odpowiedni sposób oba panele. Można tego dokonać poprzez odpowiednie powiązanie ich właściwości Visibility z właściwościami z naszego obiektu ViewModel: IsInProgress oraz IsSolved:
//dla panelu pierwszego

//dla panelu drugiego

Korzystamy przy tym z konwertera BooleanToVisibilityConverter dostępnego domyślnie.

Podsumowanie
Na tym małym przykładzie (choć tekst rozrósł się bardziej niż przypuszczałem) można zobaczyć jak wykorzystać podstawowe składniki MVVM Light Toolkit, czyli właściwości implementujące INotifyPropertyChanged, komendy (zarówno z parametrami jak i bez) oraz ViewModel Locator. W następnym poście przedstawię jeszcze obiekt Messenger do komunikacji między składowymi aplikacji oraz EventToCommand, pozwalający na podpięcie w łatwy sposób dowolnych zdarzeń do komend.

Źródła tego przykładu można pobrać stąd.

2 komentarze:

  1. Szukałem wzorca MVVM, który nie straszy liczbą funkcji "na wejście" i padnie pewnie właśnie na MVVM LT...
    Czekam na kolejne wpisy na ten temat ;]...

    PS:
    W akapicie "Tworzenie bindingów", w drugim zdaniu chyba czegoś brakuje ;] ("Niektóre są naprawdę i nie wymagają większego komentarza")

    OdpowiedzUsuń
  2. Dzięki za znalezienie błędu. Chodziło oczywiście o to, że są naprawdę proste.

    Druga część tego posta będzie niedługo, mam teraz spore zamieszanie z początkiem roku akademickiego, ale wkrótce powinienem się odrobić.

    OdpowiedzUsuń