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

Trigery – Triggers

Trigery jest to mechmizm WPF-a służący reakcji UI na jakieś zdarzenie. Wyróżniamy cztery rodzaje triggerów

  • Property triggers – używane do zareagowanie na zmiany dependency property w danej kontrolce,
  • Data triggers – używane do zareagowanie na zmiany w bindowanych obiektach- można podłączyć się do właściwości z DataContextu jak i z danej kontrolki,
  • MultiDataTriggers – używane do reagowania na zmiany kilku właściwości,
  • MultiTrigger – używane do reagowania na zmiany kilku dependecy property w danej kontrolce,
  • Event triggers – używane do reagowania na jakieś zdarzenie – służą do odpalania animacji

1. Property triggers

Oto przykład wykorzystania Property Triggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

Widzimy, że dany trigger jest powiązany z właściwością IsMouseOver.W przypadku gdy wartość tej właściwości zostanie ustawiona na true, tło przycisku zostanie zmienione na czarne. Natomiast gdy IsMouseOver powróci do stanu false, tło przycisku wróci do wartości sprzed zmiany.

2. Data triggers

Oto przykład wykorzystania Data Triggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

W tym przypadku nasz trigger reaguje na zmiany właściwości Text. Jako, że nie ustawiliśmy źródła bindowania, właściwość Text będzie wyszukiwana w DataContext danej kontrolki. W przypadku gdy DataContext nie zostanie ustawiony, właściwość ta będzie wyszukiwana w kontrolce będącej wyżej w hierarchii.W przypadku gdy wartość właściwości Text będzie wynosiła “Kolor” tło kontrolki zostanie zmienione na kolor beżowy.

3. MultiData triggers

Oto przykład wykorzystania MultiDataTriggera. W ResourceDictionary tworzymy styl, który zostanie przypisany do przycisku.

Tym razem nasz trigger zareaguje, tylko i wyłącznie gdy wartość właściwości “Text”(znajdującej się w DataContext) będzie równe “Kolor”, oraz wartość właściwości “Title” będzie równe “Spider-Man”. W chwili gdy warunek ten zostanie spełniony, tło kontrolki zmieni się na kolor czarny.

4. MultiTrigger

Zasada działania MultiTriggera jest analogiczna z zasadą działania MultiDataTriggera. Różnica między ni mi jest taka, że w MultiTriggerze możemy jedynie odnosić się do propertisów danej kontrolki (kontrolki dla której styl tworzymy,oraz kontrolek wchodzących w skład naszej kontrolki). Przykładowy kod MultiTriggera może wyglądać w następujący sposób:

Tym razem nasz trigger zareaguje, tylko i wyłącznie gdy tło naszej kontrolki będzie czarne, oraz kursor myszy będzie znajdował się nad przyciskiem. W chwili gdy warunki ten zostaną spełnione, napis na przycisku zmieni się na “MultiTrigger

5. Event triggers

Event triggery wykraczają poza zakres tej części tutoriala, gdyż są one ściśle związane z animacjami.

Trigery – Triggers

Style

Style jest to mechanizm wprowadzony w WPF-ie, a następnie w Silverlighcie, dzięki któremu w łatwy sposób można ujednolicić wygląd elementów naszej aplikacji.
Style z reguły definiowane są w zasobach. Mogą to być zarówno zasoby całej aplikacji, danego okna lub nawet wybranej kontrolki.
Przykładowy styl może wyglądać w następujący sposób:

Każdy styl jest rozpoznawany dzięki unikalnemu kluczowi – parametr x:Key. Możliwe jest pominięcie klucza, w przypadku gdy ustawiona zostanie właściwść TargetType. Jednakże zdefiniowanie właściwości TargetType spowoduje, że dany styl będzie automatycznie zastosowany do wszystkich kontrolek danego typu. W przypadku gdybyśmy jednak chcieli wyłączyć dany styl dla pojedynczej kontrolki możemy zawsze przypisać mu wartość x:Null
Jak widać styl posiada kolekcję obiektów typu Setter. Najważniejszymi propertisami każdego Settera są:

  • Property – określa nazwę właściwości, którą dany styl będzie modyfikować,
  • Value – określa wartość właściwości określonej w elemencie Property

Dla przykłady – następujący styl:

ustawi rozmiar czcionki na buttonie na 22 pkt. Jako, że nie została ustawiona właściwość TargetType dla stylu, musimy podawać pełną nazwę właściwości(czyli nazwę klasy oraz nazwę właściwości).
Oczywiście możemy również ustawiać bardziej skomplikowane właściwości

W przypadku gdybyśmy pisali w Silverlighcie właściwość TargetType ustawia się troszeczkę inaczej. Mianowicie zamiast
TargetType{x:Type Button} piszemy TargetType{Button}

Właściwość TargetType określa nam dla jakiego typu obiektów(kontrolek) dany typ jest przeznaczony. Styl z pierwszego przykładu można zapisać następująco:

Dzięki użyciu właściwości TargetType, kompilator wie, że dany styl jest przeznaczony dla Buttona, dlatego też możemy w Setterze użyć skróconej nazwy właściwości(FontSize zamiast Button.FontSize). Ponadto dzięki użyciu TargetType, możemy pominąć definiowanie klucza po jakim nasz styl będzie rozpoznawany. Od tej chwili wszystkie buttony będą renderowane z użyciem powyższego stylu (oczywiście jeżeli styl ten będzie w resourcach danej kontrolki).

Dziedziczenie styli

Style mogą być dziedziczone. Oznacza to tyle, że styl pochodny będzie rozszerzał styl bazowy. W przypadku gdy styl bazowy ma zdefiniowany TargetType, styl dziedziczący nie może go zmienić. W celu zdefiniowania dziedziczenia używa się właściwości BasedOn. Przykładowe dziedziczenie styli pokazano poniżej

W stylu dziedziczącym można przesłaniać właściwości ze stylu bazowego. W przypadku gdy nie zostaną one przesłonięte, użyte zostaną propertisy ze stylu bazowego.
W celu zaaplikowania stylu do jakiejś kontrolki posługujemy się następującą składnią

myStyle – klucz stylu zdefiniowanego w zasobach.

Style

Commands – czyli interakcja widoku z ViewModelem cz.2

Tak jak wspomniałem w poprzednim wpisie, tym razem zajmiemy się tworzeniem własnej klasy implantującej interfejs ICommand .

1. Implementacja interfejsu ICommand – WPF

Klasa implementująca ICommand może wyglądać w następujący sposób

Klasa DelegateCommand przyjmuje w konstruktorze dwa parametry – delegaty do funkcji. Pierwszy z nich jest to delegat do funkcji, która ma się wykonać, gdy komenda może zostać wykonana.Drugi natomiast jest to delegat do funkcji, która sprawdza czy dana komenda może zostać wykonana.Ważnym elementem jest tutaj następujący kawałek kodu :

który podłącza naszą klasę do WPF-owego systemu komend.
Teraz wystarczy w ViewModelu stworzyć obiekt typu DelegateCommand przekazać do niego odpowiednie funkcje, a następnie zbindować go do widoku. Może to wyglądać w następujący sposób:

  • ViewModel
  • Widok (XAML)

2. Implementacja interfejsu ICommand Silverlight

Przedstawiony powyżej przykład skompiluje się jedynie w aplikacji WPF-owej. Silverlight niestety nie posiada CommandManagera, dlatego też gdy chcemy używać komend właśnie w tej technologii musimy zmodyfikować nasz kod na następujący:

Jak widać w funkcji

sami musimy zadbać o wywołanie zdarzenia

Ponadto dopisana została funkcja

funkcję tą musimy wywoływać zawsze gdy zmieni się jakaś właściwość, która ma mieć wpływ na stan przycisku zbindowanego do naszej komendy.

3. Przykład

Załóżmy,że na widoku mamy przycisk edytuj, który powinien być aktywny tylko i wyłącznie wtedy, gdy zaznaczono jakiś element na gridzie. Przycisk ten jest zbindowany do komendy EditCommand. Jak już wcześniej wspomniano, z powodu braku w Silverlighcie CommandManagera sami musimy wywołać zdarzenie informujące o potrzebie zmiany stanu przycisku. Dlatego też, w ViewModelu została stworzona właściwość SelectedItem

która reaguje na zmiany zaznaczenia na gridzie. W chwili, gdy zaznaczenie się zmienia, wywoływany jest seter z tej właściwości, w którym to wywołujemy funkcję EditCommand.RaiseCanExecute(). Dzięki czemu informujemy kontrolkę o ewentualnej potrzebie zmiany jej stanu.

4. Podsumowanie

Komendy w bardzo prosty i wygodny sposób pozwalają nam wywoływać metody bezpośrednio z ViewModelu. Niestety mają one również swoje ograniczenia:

  • reagują jedynie na zdarzenie Click,
  • można je wykorzystać jedynie na kontrolkach dziedziczących po klasie ButtonBase oraz kontrolkach typu MenuItem
Commands – czyli interakcja widoku z ViewModelem cz.2