wtorek, 5 października 2010

Własna kontrolka do przeglądania katalogów

Jedną z funkcjonalności Chupacabry będzie możliwość przeglądania zdjęć znajdujących się na dysku i aby to osiągnąć konieczny jest mechanizm nawigowania pomiędzy katalogami. Oczywiście można znaleźć gotowe rozwiązania zaimplementowane przez kogoś, gotowe do wykorzystania w WPF'ie, ale pomyślałem, że stworzenie takiej kontrolki samemu będzie dobrym sprawdzianem oraz ćwiczeniem z wykorzystania samego WPF'a jak i MVVM Light Toolkit w praktyce.
Od razu napiszę, że nie wymyśliłem samemu jak to ładnie połączyć, tylko oparłem się na artykule z CodeProject i jedynie uprościłem go nieco oraz dostosowałem do wykorzystania z MVVM Light Toolkit.


DirectoryTreeView
Tak właśnie nazywać się będzie ta kontrolka. Możliwości jakie chciałbym zaimplementować to przeglądanie struktury dysku twardego i zwracanie na bieżąco aktualnie wybranego katalogu, możliwość przeskoczenia do zarządanego przez użytkownika folderu oraz opcje pozwalające na niepokazywanie na przykład ukrytych katalogów. Chciałbym, żeby kontrolka była w jak największym stopniu zgodna z zasadami MVVM.
Do rzeczy. Do reprezentowania drzewiastej struktury katalogów oczywistym wyborem jest kontrolka TreeView. Oczywiście mógłbym wykorzystać jej zdarzenia i reagować na każde rozwinięcie gałęzi wczytaniem nowej listy katalogów i dodaniem do listy obiektów TreeViewItem, ale nie o to chodzi w MVVM. Dlatego idea jest następująca: stworzenie obiektu ViewModel powiązanego z plikiem XAML kontrolki, który będzie przechowywał listę innych obiektów ViewModel, z których każdy reprezentuje pojedynczą gałąź ze struktury katalogów i sam odpowiada za wczytywanie swoich podkatalogów. Może to trochę skomplikowane, ale niedługo powinno się wyjaśnić.


Model
Nie wiem czy można jedną prostą klasę nazwać modelem, ale na potrzeby podbudowania swojego programistycznego ego tak właśnie zrobię. Modelem będzie to co chcemy właściwie od tej kontrolki się dowiedzieć, czyli pełna ścieżka do katalogu oraz sama nazwa katalogu. Można to zawrzeć w takiej o to trywialnej klasie:
public class DirectoryItem
{
 public string Name { get; set; }
 public string Path { get; set; }
}
Instancja tej klasy będzie dostępna w każdym ViewModelu reprezentującym jedną gałąź.


DirectoryTreeViewModel
Właściwie powinien się on nazywać DirectoryTreeViewViewModel, ale jakoś za bardzo kłuło mnie to w oczy. Sama klasa dziedziczy z ViewModelBase, aby móc wykorzystywać MVVM Light Toolkit. Instancja tej klasy będzie przypisana do właściwości DataContext widoku kontrolki. Wspomniana wcześniej lista obiektów reprezentujących pojedynczy katalog udostępniania jest przez właściwość:
public const string RootPropertyName = "Root";
private ObservableCollection _root = null;

public ObservableCollection Root
{
 get
 {
  return _root;
 }

 set
 {
  if (_root == value)
   return;

  _root = value;

  RaisePropertyChanged(RootPropertyName);
 }
}
W konstruktorze pobierana jest lista dostępnych dysków i dla każdego z nich tworzony jest nowy obiekt DirectoryItemViewModel oraz następnie dodawany do listy. Nic szczególnego się nie dzieje.
public DirectoryTreeViewModel()
{
 Root = new ObservableCollection<directoryitemviewmodel>(getRootDirectories());
}

private IList<directoryitemviewmodel> getRootDirectories()
{
 IList<directoryitemviewmodel> result = new List<directoryitemviewmodel>();

 foreach (var s in Directory.GetLogicalDrives())
 {
  DirectoryItem item = new DirectoryItem
  {
   Name = s,
   Path = s
  };

  result.Add(new DirectoryItemViewModel(item));
 }

 return result;
}
DirectoryItemViewModel
To tu dzieje się większość magii związanej z przeglądaniem katalogów. Instancje tych obiektów będą używane jako DataContext dla obiektów TreeViewItem. Za pomocą data bindingu przekazywać będą dane do wyświetlania, a także reagować na zmiany stanu kontrolki TreeView.
Każdy ViewModel tego typu przechowuje instancję "modelu" odpowiadający aktualnemu katalogowi oraz listę jego podfolderów w postaci listy innych obiektów DirectoryItemViewModel.
private ObservableCollection _children;
private DirectoryItem _fileitem;
Na potrzeby data bindingu istotne dane udostępniane są przez właściwości:
public string Name
{
 get { return _fileitem.Name; }
}

public ObservableCollection<directoryitemviewmodel> Children
{
 get { return _children; }
}
I teraz bardzo istotna kwestia. Wczytywanie wszystkich katalogów na dysku, aby zbudować odpowiednią hierachię nie ma najmniejszego sensu. Dlatego należy zastosować coś na kształt "lazy loadingu", to znaczy wczytywania katalogów tylko wtedy, gdy zarząda tego użytkownik. Nie należy też wczytywać listy katalogów za każdym razem, gdy rozwijana będzie gałąź. Można zastosować coś co w artukule z którego korzystałem zostało określone jako "dummy object" (być może pasuje tłumaczenie tego jako manekin). Jego jedynym przeznaczeniem jest reprezentowanie nic nie znaczącej gałęzi, dzięki czemu będzie można wykryć czy gałąź była już rozwijana oraz czy posiada jakieś elementy. Poprzez stworzenie statycznej instancji będzie on wspólny dla wszystkich obiektów:
readonly static DirectoryItemViewModel DummyChild = new DirectoryItemViewModel();
Klasa posiada też dwa konstruktory, jeden prywatny tylko na potrzeby powyższej linijki kodu, a drugi inicjalizujący właściwy obiekt.
private DirectoryItemViewModel()
{ }

public DirectoryItemViewModel(DirectoryItem fileItem)
{
 _fileitem = fileItem;
 _children = new ObservableCollection<DirectoryItemViewModel>(new List<DirectoryItemViewModel>());

 _children.Add(DummyChild);            
}
Jedyne co konstruktor robi to przypisuje otrzymaną intancję modelu i tworzy listę podkatalogów z jednym elementem manekinem, symbolizującym, że gałąź przypisana do bieżącego obiektu nie została jeszcze rozwinięta.
Teraz czas na dwie istotne właściwości, które będą zbindowane do właściwości obiektu TreeViewItem, mianowicie IsSelected oraz IsExpanded. Ich znaczenia są raczej oczywiste, mają wartość true, gdy odpowiednio gałąź jest zaznaczona lub rozwinięta. Właściwość IsSelected jest zwykłą właściwością typu bool, która podczas przypisywania wartości wywołuje RaisePropertyChanged. Natomiast w IsExpanded zawarta jest obsługa wczytywania kolejnych katalogów:
public const string IsExpandedPropertyName = "IsExpanded";
private bool _isExpanded = false;

public bool IsExpanded
{
 get
 {
  return _isExpanded;
 }

 set
 {
  if (_isExpanded == value)
   return;

  _isExpanded = value;

  RaisePropertyChanged(IsExpandedPropertyName);

  if (HasDummyChildren)
  {
   _children.Remove(DummyChild);
   loadChildren();
  }
 }
}
Po uaktualnieniu wartości, sprawdzam czy czasem dany węzeł nie posiada tylko jednego potomka, którym jest właśnie nasz dummy object. Jeśli tak jest to usuwany jest on z listy podkatalogów a na jego miejsce dodawane są nowe odpowiadające strukturze aktualnego katalogu:
public bool HasDummyChildren
{
 get
 {
  return (_children.Count == 1 && _children[0] == DummyChild);
 }
}

private void loadChildren()
{
 foreach (var s in Directory.GetDirectories(_fileitem.Path))
 {
  DirectoryItem item = new DirectoryItem
  {
   Name = s.Substring(s.LastIndexOf(@"\") + 1),
   Path = s
  };
  _children.Add(new DirectoryItemViewModel(item));
 }
}
 
DirectoryTreeView.xaml.cs
Jedyną operacją jaką na razie trzeba zrobić w code behind kontrolki to przypisanie do właściwości DataContext nowej instancji odpowiedniego obiektu ViewModel:
DataContext = new ViewModel.DirectoryTreeViewModel();

DirectoryTreeView.xaml
Teraz należy odpowiednio połączyć wszystkie elementy poprzez bindingi:

 
  
   
  
 
 
  
 

Głównym źródłem elementów od którego należy zacząć jest właściwość Root z DirectoryTreeViewModel. Kolejne elementy pobierane są z właściwości Children obiektu DirectoryItemViewModel. Każdy element jest zwykłym polem tekstowym, w którym wyświetlana jest nazwa katalogu; jednoczeście istnieje możliwość ładnego dopasowania wyglądu kontrolki dodając na przykład ikonki przy każdym folderze. 
Bardzo istotny jest fragment zbindowania właściwości IsExpanded oraz IsSelected dla każdego obiektu TreeViewItem. To właśnie dzięki dwustronnemu powiązaniu wszystkie zmiany następują automatycznie bez zwracania na to uwagi. Dodatkowo jeśli dany element jest wybrany to będzie on wypisany grubszą czcionką. 

Co dalej?
W następnej kolejności chcę się zająć udostępnianiem na zewnątrz kontrolki aktualnie zaznaczonego katalogu, sprawdzaniem czy dany folder ma podkatalogi by ewentualnie nie wyświetlać strzałki do rozwijania oraz możliwością niepokazywania ukrytych katalogów.


Zaktualizowane źródła projektu dostępne są na BitBucket.

niedziela, 3 października 2010

Tajemnicze komunikaty debuggera


Podczas ostatnich prac z WPF'em napotykałem na strasznie irytujące sytuacje kiedy po uruchomieniu aplikacji wraz z debuggerem otrzymywałem nic nie mówiące mi komunikaty. Dla przykładu:
An unhandled exception of type 'System.Windows.Markup.XamlParseException' occurred in PresentationFramework.dll

Additional information: 'Add value to collection of type 'System.Windows.Controls.UIElementCollection' threw an exception.' Line number '6' and line position '10'.

I bądź tu mądry człowieku! Poczułem się jakbym wracał do dawnych czasów pisania programów w C/C++ kiedy otrzymywałem tylko komunikat segmentation failure i program kończył działania. Moja irytacja tym faktem była na tyle duża, że przerywałem pracę i zajmowałem się czymś innym dla ukojenia skołatanych nerwów.
Oczywiście wystarczyło trochę poszukać na niezawodnym StackOverflow. Rozwiązanie jest niesłychanie proste. Wybieramy kolejno Debug -> Exceptions i zaznaczamy haczyk przy opcji Common Language Runtime Exceptions. Po tej zmianie dalej otrzymujemy wyjątki, jednak teraz mają one jakąkolwiek wartość. Poprzednio wspomniany wyjątek zamienia się na:
A first chance exception of type 'System.InvalidOperationException' occurred in PresentationFramework.dll

Additional information: Window must be the root of the tree. Cannot add Window as a child of Visual.

Dodatkowe informacje są na tyle pomocne, że od razu znalazłem oczywisty błąd na który nie zwracałem uwagi i wszystko wróciło do normy. Jestem jednak mocno zdziwiony czemu takie informacje nie są wyśtwietlana "out of the box".

piątek, 1 października 2010

Podstawy MVVM Light Toolkit [część 2]

Drugiej części posta o MVVM Light Toolkit chciałem przedstawić bardzo ciekawe funkcjonalności, mianowicie: EventToCommand oraz obiekt Messenger. Postaram się przedstawić je na przykładzie, który będzie rozwinięciem aplikacji z poprzedniego posta.

EventToCommand
Jak już wcześniej wspominałem, w kontrolkach WPF'a komendy podpinane są domyślnie do jednego z góry przewidzianego zdarzenia, na przykład dla przycisku jest to odpowiednik OnClick. Nie ma możliwości podłączenia komendy do innych zdarzeń, jednak tu przychodzi z pomocą MVVM Light Toolkit. Nie jest to co prawda część samego wzorca MVVM, ale jeden z helperów, jednak jest to niesłychanie pomocna funkcjonalność. 
Dzięki niej istnieje możliwość podłączenia komend z obiektu ViewModel do dowolnych zdarzeń kontrolki bez angażowania do tego jakiegokolwiek Code Behind, czyli nadal żyjemy w zgodzie z założeniami MVVM. W moim przykładzie pod zdarzenie LostFocus podepnę sprawdzenie czy podana przez użytkownika liczba jest wylosowanym sekretnym numerem, dlatego też nie będzie konieczne klikanie w przycisk "Check guess", ale wystarczy na przykład naciśnięcie klawisza TAB, by przeskoczyć do następnej kontrolki. 
Najłatwiej wykonać połączenie między zdarzeniem i komendą z poziomu Expression Blend'a. Przed rozpoczęciem konieczne jest dodanie do projektu referencji do bilbioteki GalaSoft.MvvmLight.Extras.WPF4.dll. Następnie w zakładce Assets w grupie Behaviors należy odnaleźć pozycję EventToCommand i przeciągnąć ją na kontrolkę do której zdarzeń chcemy podpinać komendy. Pojawi nam się nowa pozycja w hierarchii obiektów, której właściwości możemy edytować. 
Z listy wybieramy nazwę zdarzenia, które chcemy wykorzystać (w naszym przypadku jest to LostFocus), a następnie podpinamy do niego komendę. Do właściwości Command poprzez binding podpinamy komendę CheckGuess z ExampleViewModel, natomiast do Command Parameter właściwość Text z kontrolki txbUserGuess, jednocześnie pamiętając o konieczności wykorzystania konwertera wartości z typu string do int, który został stworzony w poprzednim poście.
Oczywiście to samo można wykonać z poziomu kodu XAML, oto fragment odpowiedzialny z wykorzystanie mechanizmu EventToCommand, który został wygenerowany przez Blend'a.

 
  
   
  
 

Wykorzystuje on zaimportowane przestrzenie nazw:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:GalaSoft_MvvmLight_Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4" 
Znaczenie powyższego kodu jest bardzo intuicyjne: do istniejącego TextBox'a odpowiedzialnego za wprowadzanie przez użytkownika liczby podpinamy nowy event trigger (o pozostałych dwóch rozdzajach triggerów można na przykład poczytać tu). Jest to trigger reagujący na zajście zdarzenia LostFocus i cała magia MVVM Light Toolkit polega na tym, że za pomocą jednego znacznika możemy podpiąć komendę do tego triggera dokładnie w taki sam sposób jak wcześniej podpięliśmy ją do przycisku.
Dodanie kolejnego triggera to po prostu dopisanie dodatkowego znacznia i:EventTrigger. Dokładniej o triggerach w WPF'ie można poczytać w artykule.

Messenger
Z założenia obiekt Messenger został dodany do MVVM Light Toolkit jako narzędzie do wszechstronnej komunikacji i to nie tylko pomiędzy obiektami ViewModel, ale między dowolnymi klasami. Nie jest on częścią wzorca MVVM, ale jednym z helperów stworzonych przez autora.
Idea opiera się na statycznym obiekcie Messenger, który udostępnia mechanizmy do wysyłania komunikatów oraz rejestrowania akcji reagujących na konkretne komunikaty. Możliwych scenariuszy pracy z obiektem Messenger jest naprawdę sporo. Możemy wysyłać komunikaty prostych typów do wszystkich zarejestrowanych odbiorców, stworzyć własne typy specyfikowanych komunikatów, wysyłać komunikaty tylko do konkretnych obiektów lub oznaczać komunikaty tokenami pozwalającymi na ich rozróżnienie. Ciekawą możliwością jest także wysyłanie komunikatów zawierających funkcję zwrotną (callback), co pozwala na wygodną dwustronną komunikację. Opcji jest sporo, ja do tej pory próbowałem tylko kilku najprostszych, jednak mam nadzieję, że podczas dalszej pracy znajdę zastosowania także dla pozostałych funkcjonalności.
W przykładowej aplikacji umieszczę tylko najprostszy przypadek, aby pokazać ogólną ideę działania. Mianowicie stworzony zostanie nowy obiekt ViewModel, który będzie jakby panelem opcji programu. Jedyną opcją będzie maksymalny zakres z jakiego można losować sekretną liczbę. Po zatwierdzeniu zmian zaktualizowany zostanie maksymalny zakres liczb, a dzięki bindingowi zostanie także odświerzona informacja o aktualnym maksymalnym zakresie.
Zaczynamy od stworzenia nowej kotrolki użytkownika jako nowego widoku w aplikacji i stworzenia prostego interfejsu użytkownika (pominąłem nieistotne na razie fragmenty): 

    
        
            
            
        
        
Następnie dodajemy odpowiadający temu widokowi obiekt ViewModel o nazwie OptionsViewModel i dziedziczący oczywiście po ViewModelBase. Dodajemy jedną prostą komendę SaveOptions, która zostanie podpięta do przycisku:
private RelayCommand<int> _saveCommand;

public RelayCommand<int> SaveOptions
{
 get {
  if (_saveCommand == null)
   _saveCommand = new RelayCommand<int>(
    x =>
    {
     Messenger.Default.Send<ChangeOptionsMessage>(new ChangeOptionsMessage(x));
    });
  
  return _saveCommand; }
}

Jej jedynym przeznaczeniem jest przesłać dalej otrzymany argument za pomocą obiektu Messenger. Argument, który będzie ona otrzymywać wynika z bindingu samego przycisku i jest to po prostu zawartość pola tekstowego z maksymalną liczbą, odpowiednio przekonwertowany na liczbę całkowitą:
Przesyłany komunikat jest typu ChangeOptionsMessage, jest to nasz własny typ komunikatu, który możemy dostosować tak aby zawierał takie informacje jakie są nam potrzebne w aktualnym scenariuszu. Dla wygody dziedziczy on po typie MessageBase z MVVM Light Toolkit i zawiera jedną dodatkową właściwość przechowującą maksymalną liczbę do wylosowania.
public class ChangeOptionsMessage : MessageBase
{
 public int MaxRange { get; set; }

 public ChangeOptionsMessage(int _maxRange)
 {
  MaxRange = _maxRange;
 }
}
Teraz nie pozostaje nam nic innego jak odebrać wysłany komunikat w głównym oknie. W konstruktorze ExampleViewModel rejestrujemy akcję reagującą na wysłanie komunikatu typu ChangeOptionsMessage:
Messenger.Default.Register(this, HandleChangeOptions);
oraz piszemy prostą funkcję obsługującą komunikat:
private void HandleChangeOptions(Messages.ChangeOptionsMessage m)
{
 this.MaxRange = m.MaxRange;
 initGuess();
}
Funkcja initGuess() rozpoczyna nową turę zgadywania z już zmienionym zakresem.
Źródła z tym przykładem do pobrania stąd.


Krótkie podsumowanie
MVVM Light Toolkit jest bardzo wygodnym narzędziem, nie przeraża mnogością funkcjonalności, jest prosty i intuicyjny w użyciu oraz wprowadza sporo udogodnień podczas pracy (jak na przykład gotowe szablony i snippety). Muszę jednak przyznać, że jestem nieco rozczarowany brakiem dokumentacji z prawdziego zdarzenia. Jedyna na czym można się opierać to przykłady aplikacji stworzone przez autora oraz społeczności użytkowników na forum projektu oraz na StackOverflow. To dość spora wada, ale mam nadzieję, że to tylko początkowe problemy i znikną one po opanowaniu toolkita.

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&amp;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 &lt;Application.Resources&gt;
 

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(
    () =&gt;
    {
     SecretNumber = rand.Next(10) + 1;
     IsSolved = false;
     IsInProgress = true;
     Suggestion = "";
    }
   );
  }
  return _startCommand; }
}

#endregion

#region CheckGuess Command
private RelayCommand&lt;int&gt; _checkGuess;

public RelayCommand&lt;int&gt; CheckGuess
{
 get {
  if (_checkGuess == null)
   _checkGuess = new RelayCommand&lt;int&gt;(
    x =&gt;
    {
     if (x == _secretNumber)
     {
      IsInProgress = false;
      IsSolved = true;
     }
     else if (x &lt; _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.

czwartek, 16 września 2010

Frustrujący MVVM

Od dawna nosiłem się z zamiarem poznania wzorca MVVM (Model-View-ViewModel), na którym opiera się WPF, jednak im głębiej go poznawałem tym bardziej wydawał mi się on irytujący. Problem z MVVM jest taki, że jest to tylko wzorzec, opisujący jedynie podstawy działania i przedstawiający założenia do ogólnej koncepcji jaką należy przyjąć w projekcie. W teorii wygląda to bardzo ładnie, oddzielenie interfejsu od logiki biznesowej miały pozwolić na rozdzielenie pracy programisty i designera. Zachowujemy porządek w projekcie, nie tworzymy żadnego kodu w tak zwanym Code Behind (to znaczy w plikach *.xaml.cs), który nie wykonuje się podczas edycji w Expression Blend.
Z drugiej strony spełnienie wszystkich wymagań dotyczących wzorca, implementowanie wymaganych składników jest niesłychanie nudne, wtórne i podatne na błędy. Gdy patrzyłem na te dziesiątki linii podobnego do siebie kodu zastanawiałem się, dlaczego tak utrudniono życie programistom? Przecież to niepodobne do twórców .NET Framework'a, do tej pory większość zmian ułatwiała życie i sprawiała, że kodowanie stawało się przyjemniejsze. Poza tym nie istnieją jednoznaczne wytyczne jak należy implementować poszczególne wymagania. Każdy programista udzielający porad w kwestiach MVVM ma swoje zdanie na ten temat, strasznie trudno znaleźć uniwersalne porady, które nie wymagałyby przepisania części naszej aplikacji w celu dostosowania do przykładów.
Dlatego też zacząłem przyglądać się różnym toolkitom (czy może po prostu narzędziom), które ponoć miały ułatwiać pracę z WPF i MVVM, jak sugerowano w tym temacie na StackOverflow. To co znalazłem zaczęło mnie przytłaczać natłokiem informacji koniecznych do przyswojenia przed rozpoczęciem pracy, ogromem możliwości, których pewnie i tak bym nie wykorzystał oraz przykładowymi aplikacjami, które zawierały całą masę funkcjonalności, jednak tak na prawdę nie pokazywały jak zacząć pracę z frameworkiem. Przypatrywałem się między innymi Prismowi, Cinchowi i MVVM Foundation.
Zainteresowałem się także MVVM Light Toolkit mając nadzieję, że nazwa nie jest bezpodstawna. Zacząłem od obejrzenia prezentacji autora z konferencji MIX10, przedstawiającej jego narzędzie. To co zobaczyłem całkowicie mnie przekonało. Dzięki niemu tworzenie aplikacji opartych o MVVM przestaje być monotonne (jak reklamuje sam autor "MVVM Light Toolkit is taking out some of the monotony of implementing MVVM"), a dodatkowo zyskujemy kilka możliwości trudno dostępnych z poziomu "gołego" wzorca (takich jak Messaging, czyli możliwość łatwego komunikowania między różnymi częściami aplikacji albo EventToCommand, pozwalającego na podpinanie dowolnych zdarzeń dostępnych w kontrolkach do obiektów typu Command). MVVM Toolkit skupia się także na tak zwanym Blendability, czyli możliwości łatwego tworzenia programu niezależnie przez programistę (na przykład w Visual Studio) oraz designera (oczywiście w Expression Blend).

Na chwilę obecną zdecydowałem się właśnie na MVVM Light Toolkit i zabieram się za dokładniejsze jego poznanie. Niedługo na pewno pojawi się notka z podstaw jego wykorzystania i wyjaśniająca niektóre z użytych w tym poście terminów.