niedziela, 14 listopada 2010

“Daj się poznać” – zakończenie :/

15 tygodni konkursu minęło. Czas na małe podsumowanie prac i trochę przemyśleń. Udało mi się spełnić wymagania konkursowe i mój projekt bierze udział w głosowaniu. Pomimo tego nie jestem zadowolony z tego co udało mi się osiągnąć. Plany miałem wielkie, a wyszło jak zawsze. Nie udało mi się wykonać nawet zadowalającej części z zaplanowanych funkcjonalności, nie pisałem tak często jak chciałem na początku. Pomimo tych niepowodzeń absolutnie nie żałuję decyzji o wzięciu udziału w konkursie. Nauczyłem się dzięki niemu o wiele więcej, niż byłbym w zrobić bez takiej motywacji. Na początek przemyśleń chciałbym pożalić się co było złe i godne pożałowania.

Porażki

Najważniejsza i najbardziej bolesna porażka – nie udało się wykonać większości z zaplanowanych funkcji. Jedyne co na chwilę obecną udostępnia Chupacabra to przeglądanie zdjęć znajdujących się na dysku. Nie została wykorzystana baza danych razem z mapowaniami NHibernate’a, nie ma możliwości grupowania zdjęć w albumy, nie można edytować i retuszować zdjęć, nie ma mowy o funkcjach społecznościowych. Wybujałe plany przegrały z szarą rzeczywistością. Z brakiem czasu i nierzadko energii, z brakiem motywacji i przekonania, że ta praca ma sens, z prostymi, często trywialnymi błędami, które zatrzymywały postępy na kilka godzin i odbierały ochotę do dalszej pracy.

Zawiodło również regularne aktualizowanie bloga. Co prawda, udało się w sam raz wpasować w wymaganą regulaminem ilość postów, jednak pozostaje niedosyt. Planowałem pisać często i ciekawie. Zamiast tego posty wychodziły rzadko, a do tego niektóre były bardzo długie, co z pewnością odstraszyło wielu potencjalnych czytelników. Rozczarowałem się webowym edytorem Bloggera. Pisanie postów nie było intuicyjne i kilka razy formatowanie rozjechało się po wstawieniu kodów źródłowych. Na szczęście tego posta (i mam nadzieję, że także następne) piszę z Windows Live Writer, który spełnia zadanie znakomicie. Być może coś jeszcze o nim napiszę.

Nie do końca jestem także zadowolony ze swojego wkładu w pozostałe projekty. Początkowo planowałem na bieżąco śledzić poczynania innych uczestników, komentować ich posty i być może postarać się czasem pomóc. Niestety tu też nie wyszło idealnie, przez długi czas udawało mi się w miarę uważnie przeczytać większość nowych postów, ale pod koniec wiele artykułów pominąłem, nie wspominając już o aktywnym komentowaniu. Jestem pod wielkim podziwem niektórych projektów i dziękuję ich autorom za inspiracje i ciekawe materiały.

Sukcesy

Największym sukcesem zdecydowanie jest wiedza, którą zdobyłem dzięki uczestnictwu w konkursie. Wiedza, której najprawdopodobniej nie zdobyłbym, gdyby tego konkursu nie było. Krótkie podsumowanie czego udało mi się dowiedzieć w trakcie tych tygodni konkursu:

  • Mercurial – użycie go do zarządzania kodem projektu sprawiło, że zakochałem się w rozproszonych systemach kontroli wersji i teraz na każdym kroku go wykorzystuję, do notatek, sprawozdań i wszystkich innych kodów.
  • NHibernate – idea wszelakich ORMów również przypadła mi bardzo do gustu i mam zamiar pogłębić ten temat. Co prawda moja znajomość tego frameworka jest na razie mocno podstawowa, jednak najważniejsze już wiem i przejście do bardziej zaawansowanych spraw nie powinno być problemem
  • WPF i MVVM – to moja porażka wśród sukcesów. Poznałem podstawy WPF’a i wykorzystywałem wzorzec MVVM, jednak jeszcze nie czuję do końca wszystkich mechanizmów i zachowań. Potrzebuję przeczytać książkę o WPFie od podstaw i wszystko sobie usystematyzować.
  • Coderush Express – znakomite narzędzie, które z pewnością będę nadal stosował, aż do momentu kiedy będzie mnie stać na wykupienie pełnej wersji :)
  • NUnit – już od dawna zbierałem się do wykorzystania testów jednostkowych w projekcie i wreszcie nadszedł ten czas.

Dzięki konkursowi wreszcie odważyłem się założyć własnego bloga. To bardzo duży sukces. Szczególnie, gdy spojrzałem na statystyki (nie przyszłoby mi na myśl je sprawdzić, gdybym nie zauważył tego w podsumowaniach innych uczestników :D). Jak dla mnie były one wręcz niebywałe, nie spodziewałem się, że tyle osób odwiedzało tego bloga (co prawda nie znam statystyk jak długo pozostawali na stronie i ile osób faktycznie przeczytało artykuły). Znakomitą większość wejść zawdzięczam serwisowi dotnetomaniak, kilka z moich postów się tam znalazło i wygenerowały całkiem spory ruch. W sumie ponad 4200 wejść i kilka postów z ponad 300 odwiedzinami. I jako ciekawostka: po wpisaniu w Google hasła mvvm, mój post pojawia się na piątym miejscu :P. Dla mnie jest bardzo miłe zaskoczenie.

Co dalej?

Blog mam nadzieję nie umrze tuż po zakończeniu konkursu. Co prawda nie planuję już pisać o projekcie konkursowym, ale chciałbym dodawać posty o tym o czym ten blog ma być. O programowaniu w dotnecie. Chciałbym uczyć się nowych zagadnień. Może ASP.NET? Albo Silverlight? A może na początek ugruntowanie wiedzy z WPF’a? Coś się na pewno znajdzie. Dlatego też cieszyłbym się, gdyby wszyscy nie usunęli RSS’a z mojego bloga tuż po zakończeniu konkursu. Może uda mi się coś wartościowego napisać.

Podziękowania

Czas na podziękowania. Przede wszystkim należą się one Maćkowi Aniserowiczowi za zorganizowanie tego konkursu i za nakłonienie nie tylko mnie, ale i wielu innych uczestników, do pracy i samodoskonalenia. Ponadto jestem winny podziękowania wszystkim, którzy mnie przekonali, żebym wziął udział w konkursie, że jednak warto i nic przez to nie stracę. I oczywiście wszystkim, którzy tego bloga czytali i komentowali, podsyłając kilka ciekawych propozycji i sugestii.

Dziękuję i zapraszam do odwiedzania bloga w przyszłości.

niedziela, 7 listopada 2010

Przeglądanie zdjęć na dysku

Czas żeby wykorzystać napisaną w ostatnim poście kontrolkę do wyświetlania obrazków przedstawioną w poprzednim poście. Na razie wykorzystywana będzie jedynie podczas przeglądania struktury dysku. Sprzężenie między aktualnym katalogiem a zdjęciami również będzie tymczasowe.

Aby w prosty sposób móc przekazać listę zdjęć do kontrolki ImageListView wystawiona na zewnątrz niej zostanie jedna DependencyProperty typu ObservableCollecion<Photo>. Dzięki temu bindowanie może być zrobione z poziomu XAML'a. 
public ObservableCollection<Photo> Photos
{
 get
 {
  return (ObservableCollection<Photo>)GetValue(PhotosProperty);
 }
 set
 {
  SetValue(PhotosProperty, value);
 }
}

public static readonly DependencyProperty PhotosProperty = DependencyProperty.Register(
 PhotosPropertyName,
 typeof(ObservableCollection<Photo>),
 typeof(ImageListView),
 new PropertyMetadata(new ObservableCollection<Photo>(), new PropertyChangedCallback(OnPhotosListChanged)));
Przy każdej zmianie zawartości listy zdjęć zostanie wywołana funkcja OnPhotosListChanged. Jej jedynym zadaniem będzie wysłanie komunikatu poprzez niedawno stworzoną statyczną klasę AppMessages.
private static void OnPhotosListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
 AppMessages.PhotoSourceChangedMessage.Send(e.NewValue as ObservableCollection<Photo>);
}
Wiadomość ta jest odbierana w konstruktorze kontrolki i po prostu przypisuje nowo otrzymaną listę to właściwości użytej do przechowywania listy:
AppMessages.PhotoSourceChangedMessage.Register(this,
 p => Photos = p);

Do przeglądania zdjęć na dysku potrzebne jest jedynie, aby lista zdjęć była odświeżona za każdym razem, gdy zmieni się zaznaczony folder. Można to osiągnąć przez zbindowanie aktualnie wybranego katalogu z kontrolki DirectoryTreeView do właściwości Photos kontrolki ImageListView. 
<my:ImageListView Photos="{Binding CurrentDirectory, Converter={StaticResource DirectoryToPhotosConverter}, ElementName=directoryTreeView}"/>
Oczywiście nie zrzucamy na kontrolkę wyświetlającą odpowiedzialności z pobranie listy zdjęć z dysku. Dlatego też użyty jest konwerter zamieniający ścieżkę do katalogu na listę obiektów Photo, wskazujących na zdjęcia z tego katalogu. 
[ValueConversion(typeof(string), typeof(ObservableCollection<Photo>))]
public class DirectoryToPhotosConverter : IValueConverter
{
 public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  string str = value as string;

  if (str != null)
  {
   IList<Photo> photos = ImageLoader.GetListOfPhotosFromPath(str);
   if (photos != null)
    return new ObservableCollection<Photo>(photos);
  }

  return new ObservableCollection<Photo>();
 }

 public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  throw new NotImplementedException();
 }
}
Konwerter ten wykorzystuje metodę GetListOfPhotosFromPath z klasy ImageLoader:
public class ImageLoader
{
 public static List<Photo> GetListOfPhotosFromPath(string path)
 {
  List<Photo> result = new List<Photo>();

  if (Directory.Exists(path))
  {
   result.AddRange(getPhotosByExt(path, "*.jpg"));
   result.AddRange(getPhotosByExt(path, "*.jpeg"));
   result.AddRange(getPhotosByExt(path, "*.png"));
   result.AddRange(getPhotosByExt(path, "*.gif"));
  }

  return result;
 }

 private static List<Photo> getPhotosByExt(string path, string ext)
 {
  List<Photo> result = new List<Photo>();

  string[] files = Directory.GetFiles(path, ext);
  foreach (var f in files)
  {
   result.Add(new Photo
   {
    Path = f,
    Description = Path.GetFileName(f)
   });
  }
  return result;
 }
}
Jest to trochę naiwna implementacja zakładająca, że istnieją tylko cztery rodzaje plików graficznych, ale to sprawdzenia poprawności działania w zupełności wystarcza.

Po połączeniu tych elementów można już przeglądać katalogi w poszukiwaniu zdjęć :P

piątek, 5 listopada 2010

Wyświetlanie listy zdjęć

Czas na kolejną kontrolkę, która jest jednym ze składników okna głównego. Mianowicie kontrolkę pozwalającą wyświetlać kolekcję zdjęć w najprostszej tabelarycznej formie. Może się ona przydać zarówno do wyświetlania zawartości albumów jak i do przeglądania struktury dysku i podglądu zdjęć z katalogów. 

Oprócz samego zdjęcia chciałbym wyświetlać także opis, jeśli taki jest przypisany do zdjęcia (jeśli nie ma to po prostu nazwę pliku). Dlatego też wygodnie będzie wykorzystać odpowiedni DataTemplate, który z listy obiektów typu Photo (dokładnie tego, który jest wykorzystywany jako jeden z modeli przy współpracy z NHibernate) utworzy szereg obrazów wraz z podpisem. Lista zdjęć przechowywana będzie w obiekcie ViewModel jako prosta właściwość implementująca IPropertyChanged:
private ObservableCollection<Photo> _photos = new ObservableCollection<Photo>();
public ObservableCollection<Photo> Photos
{
 get
 {
  return _photos;
 }

 set
 {
  if (_photos == value)
   return;

  _photos = value;

  // Update bindings, no broadcast
  RaisePropertyChanged(PhotosPropertyName);
 }
}
Na początek należy przygotować kontener, do którego zbindowane będą zdjęcia i wskazać mu szablon danych (który zostanie omówiony w następnej kolejności). 

 
  
   
    
   
  
 

Jako kontener wybrałem WrapPanel, ponieważ sam dobiera ilość wyświetlanych kolumn w zależności od dostępnego miejsca. 

Teraz pora na utworzenie szablonu itemsTemplate, który będzie definiował wygląd każdego elementu umieszczanego we WrapPanel'u. Pojedynczy element będzie składał się z obiektu Image, dla którego właściwość Source ustawiona będzie na ścieżkę dostępu do pliku ze zdjęciem oraz obiektu TextBlock wyświetlającego opis obrazka. Wewnątrz DataTemplate'u może być umieszczony tylko jeden element, dlatego też będą one opakowane w StackPanel. W uproszczeniu prezentuje się to następująco:

 
  
  
 

Dodatkowo chciałem, aby kliknięcie na zdjęcie albo podpis (ogólnie na cały StackPanel) powodowało podjęcie odpowiedniej akcji. Niestety okazało się to trudniejsze niż myślałem. Wszystkie elementy będące wewnątrz WrapPanel'u przejęły DataContext od właściwości ItemsSource z kontenera ItemsControl, czyli nie miały pojęcia o istnieniu ImageListViewModel i nie tym samym nie było możliwości wywołania odpowiedniej komendy. Rozwiązaniem okazało się zastosowanie bardzo rozbudowanego bindingu do obiektu EventToCommand pochodzącego z MVVM Light Toolkit:

 
  
   
   
  
 
 ...

Taki binding przeszukuje w górę hierarchię obiektów, aż do momentu znalezienia obiektu typu ItemsControl, wtedy wykorzystuje jego DataContext w celu wywołania komendy. Jako parametr przekazywany jest cały obiekt typu Photo przypisany do aktualnego elementu. Nie jest ona specjalnie skomplikowana, ma za zadanie jedynie przesłanie odpowiedniego komunikatu:
private RelayCommand<Photo> _onClickCommand = null;
public RelayCommand<Photo> OnClick
{
 get {
  if (_onClickCommand == null)
  {
   _onClickCommand = new RelayCommand<Photo>(
    p =>
    {
     AppMessages.PhotoSelectedChanged.Send(p);
    });
  }
  return _onClickCommand;
 }
}
Jest to bardzo podstawowy zakres funkcjonalności jakie spadają na tę kontrolkę. Nie mam do końca zdefiniowanej wizji co do ostatecznego połączenia kontrolek i ich funkcjonalności (na przykład na której powinien ciążyć obowiązek wyświetlania tagów i daty dodania). W zależności od tego co się wyklaruje być może ta kontrolka zostanie jeszcze rozszerzona.

piątek, 29 października 2010

Prosty wrapper Messengera z MVVM Light Toolkit

Od razu zaznaczam, że pomysł nie jest mój, ale jest zaczerpnięty z przykładowego projektu na CodeProject. Jednak spodobał mi się bardzo, bo wprowadza porządek do projektu, jest prosty i oczywisty. 

W czym problem?
Podczas korzystania z Messengera, problemem okazało się tworzenie unikalnych tokenów za pomocą, których można by jednoznacznie identyfikować komunikaty. Dzięki tokenom można przesyłać wiele komunikatów tego samego typu (na przykład w prostym przypadku string'ów) i jednocześnie móc je od siebie odróżnić. Pozwala to także uniknąć konieczności tworzenia własnych klas komunikatów nawet dla prostych przypadków. 
Używanie ciągów znakowych wpisywanych z palca jako tokenów (na przykład "SelectedDirectoryChanged") jest niewygodne, podatne na literówki i nie daje żadnego wsparcia dla IntelliSense'a. Chciałem rozwiązać ten problem przez przechowywanie takich tekstowych tokenów w zasobach, ale okazało się to niewygodne i kłopotliwe. 

Rozwiązanie
Aby poradzić sobie z tym problem i jeszcze dodatkowo za darmo otrzymać wygodniejszy sposób posługiwania się komunikatami należy stworzyć statyczną klasę (AppMessages), która będzie wrapperem na Messengera. Najpierw deklarujemy enum'a, który będzie przechowywał wszystkie typy komunikatów jakie będziemy przesyłać:
enum MessageTypes
{
 SelectedDirectoryChanged,
 PhotoSourceChanged,
 AddPhoto, 
 SavePhoto
}
Następnie dla każdego komunikatu należy stworzyć osobną statyczną klasę wewnątrz AppMessages, która będzie posiadać dwie funkcje Send oraz Register. Ich jedynym zadaniem jest wywołanie odpowiednich funkcji Messengera, np:
public static class SelectedDirectoryMessage
{
 public static void Send(Models.DirectoryItem item)
 {
  Messenger.Default.Send<models.directoryitem>(item, MessageTypes.SelectedDirectoryChanged);
 }

 public static void Register(object recipient, Action<models.directoryitem> action)
 {
  Messenger.Default.Register<models.directoryitem>(recipient, MessageTypes.SelectedDirectoryChanged, action);
 }
}
Dzięki takiemu podejściu zyskujemy bardzo prosty i czytelny sposób obsługi komunikatów:
//rejestrowanie akcji reagującej na komunikat
AppMessages.SelectedDirectoryMessage.Register(this,
 item =>
 {
  this.CurrentDirectory = item.Path;
 });
 
//wysyłanie komunikatu
AppMessages.SelectedDirectoryMessage.Send(_fileitem);
Unikamy wpisywania za każdym razem tasiemców, w których trzeba podawać typy przesyłanych danych, robimy to tylko raz pisząc klasę obsługi komunikatu. 

Snippet do generowania klas obsługi komunikatów
Tworzenie nowych klas dla nowych komunikatów jest wtórne i powtarzalne, dlatego zrobiłem sobie snippet który generuje nową klasę i pozwala wpisać wszystkie parametry. Nie ma w nim nic nadzwyczajnego, ale pomimo tego jakby ktoś chciał go użyć wrzuciłem go tutaj.

czwartek, 7 października 2010

DirectoryTreeView - nowe funkcje

Do kontrolki którą stworzyłem w poprzednim poście dodałem nowe funkcjonalności, które wcześniej zaplanowałem, a mianowicie wystawianie na zewnątrz kontrolki aktualnie wybranego katalogu, możliwość wskazania zadanego katalogu oraz ukrywanie niedostępnych katalogów.

Aktualnie wybrany katalog
Jest to miejsce, w którym moja aktualna wiedza zmusza mnie do złamania zasada wzorca MVVM. Chciałem mieć możliwość bindowania aktualnego katalogu do innych kontrolek. Nie udało mi się udostępnić odpowiedniego Dependency Property w obiekcie DirectoryTreeViewModel, ponieważ nie dziedziczy ona po DependencyObject, dlatego nie udostępnia metod GetValue oraz SetValue potrzebnych do implementacji. Dlatego storzyłem DependencyProperty typu string w CodeBehind kontrolki, to znaczy w pliku DirectoryTreeView.xaml.cs:
public string CurrentDirectory
{
 get { return (string)GetValue(CurrentDirectoryProperty); }
 set { SetValue(CurrentDirectoryProperty, value); }
}

public static readonly DependencyProperty CurrentDirectoryProperty =
 DependencyProperty.Register("CurrentDirectory", typeof(string), typeof(DirectoryTreeView), new UIPropertyMetadata(""));
Natomiast aktualizację aktualnie wybranego folderu zrealizowałem poprzez wykorzystanie mechanizmu komunikatów z MVMM Light Toolkit. W obiekcie DirectoryItemViewModel, który przypisany jest jako DataContext dla każdego obiektu TreeViemItem w drzewku katalogów, podczas aktualizacji właściwości IsSelected na wartość true zostaje wysłany komunikat o zmianę wartości własności. Nie tworzę własnego typu komunikatów, jedynie wysyłam komunikat typu DirectotyItem z tokenem, którym jest po prostu string zawierający treść "SelectedDirectoryItemChangedToken". Abym nie musiał w różnych miejscach wpisywać tego ciągu znaków z palca, umieściłem go w zasobach projektu. Całość sprowadza się do dodania we właściwości IsSelected, w sekcji set następującego kodu:
if (_isSelected)
 Messenger.Default.Send<DirectoryItem>(_fileitem, 
  Chupacabra.Controls.Properties.Resources.SelectedDirectoryItemChangedToken);
Teraz należy gdzieś odebrać wysłany komunikat i przypisać go do Dependency Property. I tu niestety następuje pogwałcenie zasad MVVM. Kod odpowiedzialny za to przechwycenie umieszczam w Code Behind kontrolki, a dokładniej w konstruktorze:
Messenger.Default.Register<DirectoryItem>(this, Properties.Resources.SelectedDirectoryItemChangedToken,
 item =>
 {
  this.CurrentDirectory = item.Path;
 });
Teraz możliwe jest zbindowanie właściwości CurrentDirectory do dowolnych elementów z poziomu XAML'a. Na przykład można do projektu dodać pole tekstowe na bieżąco pokazujące wybrany folder:
<my:DirectoryTreeView Name="directoryTreeView"/>
<TextBlock Text="{Binding CurrentDirectory, ElementName=directoryTreeView}" />

Rozwijanie drzewka do wybranego katalogu
Wydaje mi się, że ta opcja jest konieczna w tego typu kontrolce. Chodzi o to aby móc rozkazać kontrolce rozwinąć drzewo katalogów do określonego folderu i na koniec go zaznaczyć jako aktualny. Aby wykonać tę operację należy za pomocą obiektu Messenger wysłać komunikat nowego typu stworzonego specjalnie na potrzeby kontrolki:
public class OpenPathMessage : MessageBase
{
 public string Path { get; set; }

 public OpenPathMessage(string _path)
 {
  Path = _path;
 }
}
Jedyna specyficzna informacja do przekazania to pełna ścieżka do folderu jaki chcemy otworzyć. Można to osiągnąć na przykład następującą linijką:
Messenger.Default.Send<Controls.Messages.OpenPathMessage>(
 new Controls.Messages.OpenPathMessage(@"D:\Programy\WTW"));
W konstruktorze DirectoryTreeViewModel można odebrać ten komunikat przez:
Messenger.Default.Register<Messages.OpenPathMessage>(this, openPath);
W funkcji openPath należy przejść przez wszystkie katalogi na jakie składa się pełna ścieżka, ustawić dla każdego elementu właściwość IsExpanded na true, żeby ewentualnie wczytać listę katalogów. 
private void openPath(Chupacabra.Controls.Messages.OpenPathMessage m)
{
 string path = m.Path;
 string[] split = path.Split(new char[] { '\\' });
 var divm = findRootViewModel(split[0]);
 int i = 1;

 while (divm != null && i < split.Length)
 {
  divm.IsExpanded = true;
  divm = divm.FindViewModel(split[i++]);
  if (divm.Path == path)
   divm.IsSelected = true;
 }
}
Najpierw rozdzielam ścieżkę na poszczególne składowe i sprawdzam czy litera dysku (czyli pierwszy element z tablicy) znajduje się na liście, jeśli tak to zwracam odpowiadający mu obiekt ViewModel:
private DirectoryItemViewModel findRootViewModel(string name)
{
 foreach (var item in _root)
 {
  if (item.Name.Trim('\\') == name)
   return item;
 }
 return null;
}
Następnie iteruje przez pozostałe w ścieżce elementy i rozwijam kolejne gałęzie. Jeśli uda mi się osiągnąć zadaną ścieżkę to dodatkowo zaznaczam końcowy element jako aktualny. Funkcja zaimplementowana w DirectoryItemViewModel pozwalająca przeszukać wszystkich potomków:
public DirectoryItemViewModel FindViewModel(string path)
{
 foreach (var item in _children)
 {
  if (item._fileitem.Name.Trim('\\') == path)
   return item;
 }
 return null;
}
Pomijanie ukrytych folderów
Tak naprawdę jest to bardzo prosta funkcjonalność. Jedyne czego wymagała to zmienienia ciała funkcji loadChildren z DirectoryItemViewModel:
private void loadChildren()
{
 foreach (var s in Directory.GetDirectories(_fileitem.Path))
 {
  DirectoryInfo di = new DirectoryInfo(s);
  if ((di.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
  {

   DirectoryItem item = new DirectoryItem
   {
    Name = s.Substring(s.LastIndexOf(@"\") + 1),
    Path = s
   };
   _children.Add(new DirectoryItemViewModel(item));
  }
 }
} 

Zaktualizowane źródła można znaleźć w standardowym miejscu.

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.