Prism – CommandBehaviors

Analizując wcześniejsze przykłady pokazujące użycie DelegateCommand można odnieść wrażenie, że mimo tego iż są one bardzo użyteczne,ich użycie jest niestety bardzo ograniczone.
Jedynie niektóre komponenty WPF-a i Silverlight-a mają właściwość Command do której można zbindować nasz obiekt DelegateCommand.Ponadto właściwość ta reaguje jedynie na wybrane zahardcodowane w kontrolce zdarzenie (zdarzenie Click). Co w przypadku gdybyśmy chcieli zareagować np. na zdarzenie MouseMove za pomocą komendy? Nasz problem możemy rozwiązać poprzez:

  • Interactivity
  • Interactions
  • CommandBehaviors

W tym poście zostanie przedstawione rozwiązanie trzecie – CommandBehaviors

Pierwszą rzeczą jaką należy zrobić w celu stworzenia komendy reagującej na inne zdarzenie niż Click jest stworzenie klasy dziedziczącej po CommandBehaviorBase. Załóżmy, że zrobimy komendę reagującą na zdarzenie MouseMove

Piersza część już za nami, ale pojawia się teraz pytanie jak z tego skorzystać. Żeby mieć możliwość użycia powyżej klasy wykorzytamy attached dependency property. Musimy sobie stworzyć pomocniczą statyczną klasę, w której to zarejestrujemy nasze właściwości. Zacznijmy od zarejestrowania dependency property o nazwie Command – do tego obiektu będziemy bindowali komendy z ViewModelu (komendy reagujące na zdarzenie MouseMove)

zauważmy, że podłączyliśmy event handlera reagującego na zmianę naszej nowo stworzonej właściwości Command. Funkcja OnSetCustomCommandCallback wygląda następująco:

Funkcja ta po prostu w razie potrzeby tworzy nowy obiekt typu MouseMoveCommandBehavior (gdy ktoś zbinduje komende) i dopina go do naszej kontrolki. Pomocnicza funkcja GetorCreateMouseMoveBehavior wygląda tak:

Obiekt rejestrowany poprzez funkcję GetorCreateMouseMoveBehavior jest dopinany do kolejnej attached property, która również została zadeklarowana w tym pliku

Mając zdefiniowane wszystkie powyższe właściwości możemy już wykorzystać naszą komendę. Jednakże przydałoby się dodać jeszcze jedną właściwość (opcjonalnie), mianowicie właściwość CommandParameters

W tym propertisie również reagujemy na przypisnie obiektu.Handler OnSetCustomCommandParameterCallback wygląda następująco

Mając już zdefiniowane wszystkie niezbędne właściwości możemy użyć ich w XAML-u. Po pierwsze musimy dodać alias do namespaca gdzie znajduje się nasza statycza klasa MouseMove. W moim przypadku wygląda to następująco:

Następnie bindujemy jakąś kontrolkę do naszego attached property Command. Wygląda to w następujący sposób:

Od teraz za każdym razem jak najedziemy na txtButton wywoła się komenda SomeCommandFromViewModel.
Podsumowując trzeba się całkiem sporo napisać żeby zmusić do działania CommandBehaviors . W następnym poście przedstawię dwa sposoby na osiągnięcie tego samego efektu mniejszym kosztem.

Prism – CommandBehaviors

Prism – Komendy

1. DelegateCommands

DelegateCommands w Prismie są to obiekty, które implementują interfejs ICommand, służą one do interakcji widoku z ViewModelem. Korzystając z Prisma nie musimy już pisać własnej klasy implementującej ten interfejs (tak jak to zrobiliśmy tutaj). Konstruktor klasy DelegateCommand przyjmuje dwa parametry:

  • Action execteMethod – funkcja odpalania przy wywołaniu komendy,
  • Func canExecuteMethod – funkcja sprawdzająca czy daną komendę można wywołać

DelegateCommands z Prisma używamy w taki sam sposób w jaki używaliśmy komend w tym poście.

2. CompositeCommands

CompositeCommands są to obiekty, które przechowują kolekcją obiektów typu DelegateCommand. Tworzenie CompositeCommand jest bardzo proste.
Po pierwsze musimy stworzyć obiekt typu CompositeCommand

następnie rejestrujemy w tym obiekcie obiekt implementujący interfejs ICommand, czyli w naszym przypadku jakiś obiekt typu DelegateCommand. Robimy to w następujący sposób:

w razie potrzeby możemy usunąć komendę z kolekcji komend przy pomocy następującej składni:

Mając już stworzony obiekt typu CompositeCommand oraz zarejestrowane w nim obiekty DelegateCommand możemy zastosować bindowanie.

Od teraz, w chwili naciśnięcia przycisku, najpierw sprawdzane jest czy dana CompositeCommand może zostać wykonana. CompositeCommand może zostać wykonana tylko wtedy, gdy wszystkie komendy do niej podpięte mogą zostać wykonane. Jeżeli warunek ten zostanie spełniony następuje wywoływanie po kolei wszystkich metod execute z komend podpiętych do danej CompositCommand. CompositeCommand znajduje zastosowanie w przypadku gdy pozwalamy użytkownikowi na edycję wielu elementów jednocześnie i potrzebujemy w prosty sposób przeprowadzić walidację na poszczególnych widokach.
Komendy
CompositeCommand może również wykorzystana np. do zapisania tylko aktywnego widoku. Dzięki temu, że DelegateCommand implementuje interfejs IActiveAware, CompositeCommand wie, który widok jest aktualnie zaznaczony i zapisze tylko wybrany widok.

Interfejs IActiveAware wygląda w następujący sposób

  • bool IsActive – wskazuje czy dana komenda jest aktualnie aktywna
  • event EventHandler IsActiveChanged – informuje o zmianie aktywności komendy

W celu wykorzystania tego interfejsu przez CompositeCommand należy skorzystać z drugiego dostępnego konstruktora tej klasy

Od teraz po naciśnięciu przycisku zbindowanego do tej komendy, wywołana zostanie jedynie aktywna komenda – czyli taka, której właściwość IsActive została ustawiona na true. Niestety o ustawianie aktywności danej komendy musimy zadbać sami.

Prism – Komendy

Kolekcje wspierające sortowanie, grupowanie oraz filtrowanie

1. ListViewCollection (WPF)

ListViewCollection jest to kolekcja wprowadzona do WPF-a, która wspiera sortowanie, grupowanie oraz filtrowanie. Kolekcję taką tworzymy w następujący sposób

czyli po prostu w konstruktorze przesyłamy obiekt implementujący interfejs IEnumerable
Filtrowanie
W celu przefiltrowania kolekcji wystarczy podać odpowiedni obiekt typu Predicate(czyli tak naprawdę wystarczy napisać funkcję zwracającą bool oraz przyjmującą w parametrze obiekt). Załóżmy, że chcemy pokazać na widoku jedynie tych graczy, który nazywają się “Tomek”. W tym celu wystarczy, że zrobimy coś takiego:

po zaaplikowaniu predicata widok zbindowany do danej listy automatycznie się odświeży. W razie czego można zawsze wymusić oświeżenie przez wywołanie funkcji

Grupowanie
Kolekcja ListViewCollection wspiera również grupowanie. W celu pogrupowania listy po jakimś elemencie wystarczy do naszej listy dorzucić tzw. group description. Odbywa się to w taki sposób:

Po dodaniu PropertyGroupDescription widok automatycznie pogrupuje nam elementy po danych właściwościach.
Sortowanie
Nasza kolekcja wspiera również sortowanie. W celu posortowania wystarczy stworzyć obiekt implementujący interfjes IComparer, a następnie przypisać go do właściwości CustomSort

po takiej akcji widok automatycznie się odświeży i pokaże posortowane dane.

2. PagedCollectionView (Silverlight)

PagedCollectionView jest to uboższa wersja ListCollectionView z WPF-a. Grupowanie oraz filtrowanie dobywa się w ten sam sposób, jednakże sortowanie zostało w znaczny sposób utrudnione. Sortować możemy jedynie po właściwościach, nie możemy stworzyć własnego comparera, który zostałby użyty do wykonania operacji sortowania. Wykonanie operacji może wyglądać w następujący sposób:

W tym wypadku nasza kolekcja zostanie posortowana po właściwości “Name”

Kolekcje wspierające sortowanie, grupowanie oraz filtrowanie

Walidacja

Walidacja jest to technika sprawdzająca, czy dany obiekt spełnia pewne założenia poprawności danych. W WPF-ie oraz Silverlighcie istnieją trzy sposoby walidacji obiektów:

  • walidacja poprzez rzucanie wyjątków,
  • walidacja z użyciem interfejsu IDataErrorInfo,
  • walidacja z użyciem interfejsu INotifyDataErrorInfo

1. Walidacja poprzez rzucanie wyjątków

Walidacja poprzez rzucanie wyjątków odbywa się w następujący sposób. W seterze danej właściwości dodajemy warunek sprawdzający czy wpisane dane są poprawne. Jeżeli nie to najzwyczajniej w świecie rzucamy wyjątek, w którym podajemy komunikat błędu. Przykładowy properties z walidacją może wyglądać w ten sposób:

W celu “wyłapania” tego wyjątku i pokazania odpowiedniego komunikatu,w bindingu musimy ustawić właściwość ValidatesOnExceptions na wartość true.

Taki sposób walidowania jest jednak rzadko stosowany i wielu programistów twierdzi, że rzucanie wyjątków powinno się odbywać tylko w przypadku nieprawidłowego działania aplikacji. Ponadto walidowane propertisy nie mogą być autopropertisami, co dodatkowo wydłuża czas tworzenia klas.

2. Walidacja z użyciem interfejsu IDataErrorInfo

W celu wyłapywania błędów w widoku, należy w bindingu ustawić ValidatesOnDataError = true

Przykładowa klasa implementująca interfejs IDataErrorInfo może wyglądać następująco:

Najważniejszą metodą w powyższej klasie jest indekser

to właśnie tutaj mogą zostać sprawdzone wszystkie właściwości danego obiektu – propertyName oznacza nazwę propertisu, który walidujemy. W przypadku, gdy wartość jakiejś właściwości jest nieprawidłowa, w pole result wpisujemy komunikat błędu. Komunikaty te “wyłapywane” są przez widok, a następnie wyświetlane w postaci komunikatów przy odpowiednich kontrolkach. Jeżeli wszystko jest OK zwracamy string.Empty.Walidacje przy pomocy interfejsu IDataErrorInfo idealnie nadają się do walidowania modelu.

3. Walidacja z użyciem interfejsu INotifyDataErrorInfo

Interfejs INotifyDataErrorInfo prezentuje się w następujący sposób:

  • bool HasErrors – określa czy dany obiekt zawiera błędy
  • event EventHandler ErrorsChanged– zdarzenie informujące o zmienie ilości błędów w obiekcie
  • IEnumerable GetErrors(string propertyName) – funkcja pobierająca kolekcję błędów dla danego propertisa

W celu “wyłapania” błędów przez widok należy w bindingu ustawić properties
NotifyOnValidationError = true

Jak już wcześniej wspomniano funkcja GetErrors(string propertyName) zwraca kolekcję błędów dla danej właściwości. Zatem do klasy, która będzie implementowała interfejs INotifyDataErrorInfo należy dodać kolekcję przechowującą obiekty typu ValidationResult. Przykładowa implementacja interfejsu może wyglądać w następujący sposób:

Funkcja Validate() najpierw czyści wszystkie poprzednie wyniki walidacji, a następnie przy pomocy klasy Validator oraz funkcji TryValidateObject waliduje wszystkie właściwości, które zostały oznaczone atrybutem dziedziczącym po klasie ValidationAttribute.Z kolei funkcja Validate(string propertyName) waliduje tylko konkretną właściwość.
Przykładowe walidowanie właściwości przy pomocy atrybutów może wyglądać w następujący sposób:

W celu stworzenia własnych regół walidacji, nie uwzględnionych w zapewnionych przez framework atrybutach należy stworzyć własną klasę dziedziczącą po klasie ValidationAttribute, a następnie przeciążyć metodę IsValid. Przykładowa klasa może wyglądać w następujący sposób:

Walidacja przy użyciu interfejsu INotifyDataErrorInfo idealnie nadaje się (wg mnie) do walidowaniu całych ViewModeli. Przy zamykaniu okna wystarczy wywołać funkcję Validate() z bazowego ViewModelu, a w przypadku gdy zwróci ona false zatrzymać zamykanie okna. Jako, że walidacja zostanie przeprowadzona na wszystkich wybranych przez nas propertisach, widok automatycznie się zaktualizuje i pokaże komunikaty błędów na odpowiednich kontrolkach okna.

Walidacja

Routed Events – nowy rodzaj zdarzeń w WPF-ie

RoutedEvent jest to nowy typ zdarzeń, który po raz pierwszy został zaprezentowany w WPF-ie. Głównym założeniem RoutedEventów jest to, że w momencie wywołania takiego zdarzenia może ono podróżować w górę, lub w dół drzewa wizualnego oraz drzewa logicznego. Każdy RoutedEvent może przyjmować jedną z trzech strategii poruszania się po drzewie:

  • Bubbling– zdarzenie najpierw jest wywoływane w elemencie źródłowym, a następnie podróżuje ono w górę drzewa wizualnego (od naszego elementu do korzenia drzewa), aż do roota (lub do momenty gdy nie zostanie obsłużone poprzez e.Handled = true)
  • Tunelling – zdarzenie wywoływane jest w korzeniu drzewa, a następnie podróżuje w dół drzewa, aż osiągnie element źródłowy (lub gdy nie zostanie obsłużone poprzez e.Handled = true)
  • Direct – zdarzenie jest wywoływane tylko i wyłącznie w elemencie źródłowym – czyli zdarzenie to zachowuje się tak samo jak standardowe .NET-owe zdarzenia

Definiowanie własnych RoutedEvents wygląda następująco. W pierwszym kroku przy pomocy EventManagera i jego funkcji RegisterRoutedEvent rejestrujemy nasze zdarzenie.

następnie piszemy wrapper RoutedEventa na zwykłe zdarzenie

Funkcja RegisterRoutedEvent przyjmuje cztery parametry:

  • Nazwa naszego zdarzenia – taka sama jak nazwa standardowego eventa opakowującego RoutedEvent
  • Strategia routingu – czyli czy nasze zdarzenie będzie typu Bubble, Tunnel lub Direct
  • Typ handlera – czyli typ delegata/funkcji jaki będzie można podłączyć do zdarzenia, żeby je obsłużyć
  • Typ właściciela – czyli typ klasy do której należy dany event

Jako, że stworzenie nowego RoutedEventa jest dość czasochłonne (w porównaniu ze zwykłym zdarzeniem) warto zassać sobie snippet, który znacząco przyśpiesza tworzenie routed eventów. Snippet taki można znaleźć tutaj RoutedEventSnippet
Mając już zdefiniowany nowy RoutedEvent można go wywołać w następujący sposób:

Funkcja RaiseEvent w parametrze przyjmuje obiekt typu RoutedEventArts. W przypadku gdyby nie był on dla nas wystarczający (potrzebujemy przesłać więcej parametrów itp.), musimy napisać klasę rozszerzającą klasę RoutedEventArts.

Routowanie zdarzenia może zostać zatrzymane, poprzez ustawienie flagi e.Handled na true. Gdzie e jest to obiekt klasy RoutedEventArgs. Przykładowe zatrzymanie routowania eventa może wyglądać w następujący sposób

UWAGA
Można zadeklarować zdarzenie w taki sposób aby wartość flagi e.Handled była pomijana. Jednak można to zrobić jedynie z poziomu kodu. Za pomocą metody AddHandler(RoutedEvent, Delegate, bool) jeżeli ustawimy ostatni parametr na true, wówczas nasza metoda wykona się nawet w przypadku gdy e.Hadnled == true. Metoda ta dostępna jest dla klasy UIElement i wszystkich klas po niej dziedziczących.
Attached events
Attached event działają podobnie jak attached dependency properties. Pozwalają one rozszerzyć kolekcję zdarzeń danej kontrolki o dodatkowe zdarzenia(nawet jeżeli nie mamy dostępu do źródeł danej kontrolki. Przykładowo, istnieje możliwość obsługi eventu click na kontrolce, która tak naprawdę takiego eventu nie posiada. Każdy RoutedEvent może zostać użyty jako attached event.

Na gridzie mamy zdefiniowane 3 przyciski, chcielibyśmy aby po naciśnięciu każdego z tych przycisków, wywoływała się odpowiednia funkcja.
Jednym ze sposobów napisania takiej funkcjonalności jest podłączenie eventa Click z każdego przycisku do jednego handlera. Rozwiązanie to jest jak najbardziej poprawne, jednakże w przypadku dużej ilości przycisków może być to czasochłonne. Można zrobić to prościej, przy wykorzystaniu attached events. Jak widzimy w listingu przedstawionym wyżej, do grida “przypięto” event Click z klasy Button. Od teraz
za każdym razem gdy jakikolwiek przycisk będący w obrębie Grida zostanie naciśnięty, zdarzenie to zostanie przechwycone przez handler zdefiniowany w gridzie – Grid_Click

Routed Events – nowy rodzaj zdarzeń w WPF-ie