You are on page 1of 319

Spis treści

Część I Wzorzec MVVM. Podstawy XAML .................................... 7


Rozdział 1. Szybkie wprowadzenie do XAML ....................................................... 9
Wzorzec widoku autonomicznego .................................................................................... 9
Tworzenie projektu ......................................................................................................... 10
Projektowanie interfejsu ................................................................................................. 11
Kilka uwag na temat kodu XAML opisującego interfejs okna ....................................... 15
Zdarzenia ........................................................................................................................ 16
Własności ....................................................................................................................... 20
Zapisywanie i odtwarzanie stanu aplikacji ..................................................................... 21
Rozdział 2. Wzorzec MVVM .............................................................................. 25
Model ............................................................................................................................. 25
Widok ............................................................................................................................. 26
Model widoku ................................................................................................................. 27
Rozdział 3. Implementacja modelu i model widoku ............................................ 29
Model ............................................................................................................................. 29
Warstwa dostępu do danych ........................................................................................... 30
Model widoku ................................................................................................................. 31
Alternatywne rozwiązania .............................................................................................. 33
Ratujemy widok ............................................................................................................. 35
Zadania ........................................................................................................................... 36
Rozdział 4. Wiązanie danych (data binding) ...................................................... 37
Instancja modelu widoku i kontekst danych ................................................................... 37
Alternatywne rozwiązanie .............................................................................................. 38
Wiązanie pozycji suwaków i koloru prostokąta ............................................................. 39
Zmiany w code-behind ................................................................................................... 40
Implementacja interfejsu INotifyPropertyChanged ........................................................ 41
Powiadomienia w alternatywnych modelach widoku ..................................................... 44
Interfejs INotifyDataErrorInfo ........................................................................................ 50
Klasa ObservedObject .................................................................................................... 50
Rozdział 5. Konwersja danych w wiązaniu ......................................................... 53
Prosta konwersja typów .................................................................................................. 53
Konwersja klas Color i SolidColorBrush ....................................................................... 55
Multibinding ................................................................................................................... 56
4 MVVM i XAML w Visual Studio 2015

Wiązanie między kontrolkami ........................................................................................ 57


Konwersje „wbudowane” ............................................................................................... 60
Zadania ........................................................................................................................... 60
Rozdział 6. Polecenia (commands) ................................................................... 61
Interfejs ICommand ........................................................................................................ 61
Przycisk uruchamiający polecenie .................................................................................. 62
Sprawdzanie możliwości wykonania polecenia .............................................................. 65
Resetowanie stanu suwaków po naciśnięciu klawisza .................................................... 66
Klasa RelayCommand .................................................................................................... 67
Zdarzenia a polecenia ..................................................................................................... 69
Zamykanie okna ............................................................................................................. 71
Zadanie ........................................................................................................................... 72
Rozdział 7. Zachowania, własności zależności i własności doczepione ............... 73
Zachowania (behaviors) ................................................................................................. 73
Własność zależności (dependency property) .................................................................. 75
Własność doczepiona (attached property) i zachowanie doczepione
(attached behavior) ....................................................................................................... 79
Zadania ........................................................................................................................... 81
Rozdział 8. Testy jednostkowe ......................................................................... 83
Testy jednostkowe w Visual Studio 2013 ....................................................................... 84
Projekt testów jednostkowych .................................................................................. 84
Przygotowania do tworzenia testów ......................................................................... 85
Pierwszy test jednostkowy ....................................................................................... 85
Testy jednostkowe w Visual Studio 2015 ....................................................................... 86
Uruchamianie testów ...................................................................................................... 88
Testy wielokrotne ........................................................................................................... 89
Dostęp do prywatnych pól testowanej klasy ................................................................... 90
Atrapy obiektów (mock objects) ..................................................................................... 92
Testowanie konwersji ..................................................................................................... 95
Testowanie wyjątków ..................................................................................................... 96
Rozdział 9. Powtórzenie ................................................................................... 99
Model ............................................................................................................................. 99
Prototyp widoku ........................................................................................................... 100
Model widoku ............................................................................................................... 102
Wiązanie ....................................................................................................................... 103
Konwerter ..................................................................................................................... 104
Wzorzec MVVM .......................................................................................................... 106
Zadania ......................................................................................................................... 107

Część II Zaawansowane zagadnienia budowania interfejsu


w XAML ..................................................................... 109
Rozdział 10. Budowanie złożonych kontrolek .................................................... 111
Konfiguracja przycisku w podoknie Properties ............................................................ 111
Pędzle ........................................................................................................................... 115
Formatowanie tekstu na przycisku ............................................................................... 118
StackPanel — liniowe ułożenie elementów .................................................................. 119
Projektowanie własnych kontrolek ............................................................................... 121
Spis treści 5

Rozdział 11. Style ............................................................................................ 123


Siatka i wiele kontrolek ................................................................................................ 123
Zasoby okna ................................................................................................................. 125
Style .............................................................................................................................. 127
Wyzwalacze .................................................................................................................. 129
Zasoby aplikacji ............................................................................................................ 130
Rozdział 12. Transformacje i animacje .............................................................. 133
Transformacje kompozycji i renderowania ................................................................... 133
Uruchamianie transformacji w wyzwalaczu stylu ........................................................ 140
Animacje ...................................................................................................................... 142
Animacja w stylu .......................................................................................................... 144
Funkcje w animacji ....................................................................................................... 145
Animacja koloru ........................................................................................................... 147
Rozdział 13. Szablony kontrolek ....................................................................... 149
Rozdział 14. Zdarzenia trasowane (routed events) ............................................ 153
Pojedyncza kontrolka ................................................................................................... 153
Zagnieżdżanie przycisków ............................................................................................ 155
Kontrola przepływu zdarzeń trasowanych .................................................................... 156
Przerwanie kolejki ........................................................................................................ 158
Bulgotanie (bubbling) i tunelowanie (tunneling) .......................................................... 158
Dynamiczne tworzenie przycisków zagnieżdżonych .................................................... 160
Rozdział 15. Kolekcje w MVVM i XAML ............................................................ 163
Model ........................................................................................................................... 163
Przechowywanie danych w pliku XML ........................................................................ 167
Model widoku zadania .................................................................................................. 169
Kolekcja w modelu widoku .......................................................................................... 172
Prezentacja kolekcji w widoku. Szablon danych (data template) ................................. 175
Style elementów kontrolki ListBox .............................................................................. 177
Konwertery ................................................................................................................... 179
Zapisywanie danych przy zamknięciu okna ................................................................. 182
Modyfikacje kolekcji .................................................................................................... 184
Sortowanie .................................................................................................................... 190
Zadania ......................................................................................................................... 192
Rozdział 16. Okna dialogowe w MVVM ............................................................. 193
Klasa bazowa okna dialogowego .................................................................................. 194
Polecenia wykonywane przed wyświetleniem i po wyświetleniu okna dialogowego ...... 196
Okno dialogowe MessageBox ...................................................................................... 199
Warunkowe wyświetlenie okna dialogowego ............................................................... 203
Okna dialogowe wyboru pliku ...................................................................................... 205
Łańcuch okien dialogowych ......................................................................................... 209
Okna dialogowe z dowolną zawartością ....................................................................... 210
Zadania ......................................................................................................................... 214
Rozdział 17. Grafika kształtów w XAML ............................................................ 215
Model widoku ............................................................................................................... 216
Widok ........................................................................................................................... 217
Zmiana kształtu okna .................................................................................................... 222
Zadania ......................................................................................................................... 226
6 MVVM i XAML w Visual Studio 2015

Rozdział 18. Aplikacja WPF w przeglądarce (XBAP) .......................................... 227

Część III Aplikacje uniwersalne (Universal Apps) ....................... 231


Rozdział 19. Kod współdzielony ........................................................................ 233
Projekt .......................................................................................................................... 234
Kod współdzielony: model i model widoku ................................................................. 235
Konwertery ................................................................................................................... 237
Zadanie ......................................................................................................................... 238
Rozdział 20. Warstwa widoku dla Windows 8.1 ................................................ 239
Widok ........................................................................................................................... 239
Logo aplikacji ............................................................................................................... 244
Zadanie ......................................................................................................................... 246
Rozdział 21. Cykl życia aplikacji i przechowywanie jej stanu ............................. 247
Cykl życia aplikacji ...................................................................................................... 247
Przechowywanie stanu ................................................................................................. 248
Zadanie ......................................................................................................................... 252
Rozdział 22. Kafelek ........................................................................................ 255
Rozdział 23. Tworzenie i testowanie pakietu AppX ............................................ 259
Rozdział 24. Warstwa widoku dla Windows Phone 8.1 ...................................... 265
Zadania ......................................................................................................................... 268
Rozdział 25. Kolekcje w aplikacji mobilnej ........................................................ 271
Dostęp do plików w katalogu lokalnym ....................................................................... 271
Współdzielony kod z warstwy widoku ......................................................................... 276
Lista zadań w widoku dla Windows Phone 8.1 ............................................................ 279
Zdarzenie CanExecuteChanged poleceń ....................................................................... 283
Zadanie ......................................................................................................................... 285
Rozdział 26. Pasek aplikacji (app bar) .............................................................. 287
Zadania ......................................................................................................................... 290
Rozdział 27. Okna dialogowe w aplikacjach Windows Phone ............................. 291
Standardowe okna dialogowe ....................................................................................... 291
Okna dialogowe z dowolną zawartością w Windows Phone ........................................ 301
Zadania ......................................................................................................................... 305
Rozdział 28. Aplikacje uniwersalne w Windows 10 ............................................ 307
Skorowidz .................................................................................. 315
Część I
Wzorzec MVVM
Podstawy XAML
Rozdział 1.
Szybkie wprowadzenie
do XAML

Wzorzec widoku autonomicznego


Osoby, które dopiero uczą się XAML i WPF, a mają wcześniejsze doświadczenia w pro-
gramowaniu aplikacji z użyciem biblioteki Windows Forms, mogą odczuwać pokusę ko-
rzystania z owianego złą sławą wzorca widoku autonomicznego (ang. autonomous view,
AV). Jest to wzorzec, w którym cała logika i dane odpowiedzialne za stan aplikacji
przechowywane są w klasach widoku, bez żadnej separacji, czyli tak, jak zwykle pro-
gramuje się aplikacje Windows Forms. Do określania tego, jak aplikacja ma reagować
na działania użytkownika, wykorzystywane są bardzo wygodne zdarzenia kontrolek.
Brak separacji poszczególnych modułów utrudnia testowanie kodu; w praktyce moż-
liwe jest tylko testowanie funkcjonalne całego produktu. To nie musi być złe rozwią-
zanie. W tym wzorcu aplikacje tworzy się szybko, szczególnie w początkowej fazie
projektu, tzn. zanim nie okaże się, że zamawiający chce go jednak znacząco rozbudować
lub zmienić. Nie tylko rozmiar projektu powinien decydować o wybieranym wzorcu
architektonicznym. Nie zawsze warto dbać o rozdzielanie modułów i najlepsze praktyki.
Czasem ważne jest, aby aplikacja powstała szybko i zadziałała w konkretnym przypadku.
Jeżeli na tym kończy się jej cykl życia, to wysiłek włożony w jej „czystość” w żaden
sposób nie zaprocentuje.

W tym rozdziale przedstawię przykład tak napisanej aplikacji. W kolejnych będę ją


natomiast stopniowo przekształcał w aplikację napisaną zgodnie ze wzorcem MVVM.
Ponieważ będzie to w gruncie rzeczy bardzo prosty projekt, obawiam się, że Czytelnik
odniesie wrażenie, iż użycie wzorca MVVM jest przerostem formy nad treścią. Może
i tak będzie w tym przypadku, ale łatwiej uczyć się złożonych rzeczy na prostych przy-
kładach, aby nie przykrywać zasadniczej idei dużą liczbą drugorzędnych szczegółów.
Rozdział 1.  Szybkie wprowadzenie do XAML 15

VerticalAlignment="Bottom"/>
<Slider x:Name="slider2" Margin="10,0,10,10" Height="22"
VerticalAlignment="Bottom"/>
</Grid>

Kilka uwag na temat kodu XAML


opisującego interfejs okna
Cały kod z pliku MainWindows.xaml widoczny jest na listingu 1.1 ze zmianami z li-
stingu 1.2. Elementem nadrzędnym jest element Window reprezentujący okno aplikacji.
W nim zagnieżdżony jest element Grid odpowiadający za organizację kontrolek w oknie.
W nim są pozostałe kontrolki: prostokąt i trzy suwaki. Zagnieżdżenie elementów ozna-
cza, że „zewnętrzna” kontrolka jest pojemnikiem, w którym znajdują się kontrolki re-
prezentowane przez „wewnętrzne” elementy1.

Warto zwrócić uwagę na atrybuty elementu Window. Atrybut x:Class tworzy pomost
między elementem Window, określającym opisywane w pliku okno, a klasą C# o nazwie
MainWindow w przestrzeni nazw KoloryWPF, której jeszcze nie edytowaliśmy, a która
znajduje się w pliku MainWindow.xaml.cs. Atrybut xmlns (od XML namespace) okre-
śla domyślną przestrzeń nazw używaną w bieżącym elemencie XAML — odpowiada
instrukcji using w kodzie C#. Z kodu wynika, że dostępnych jest pięć przestrzeni
nazw. Pierwsza jest przestrzeń domyślna, zadeklarowana jako http://schemas.
microsoft.com/winfx/2006/xaml/presentation. Zawiera ona definicje większości ele-
mentów XAML, między innymi Rectangle i Slider. Drugą ważną przestrzenią jest ta
dostępna pod nazwą x. To w tej przestrzeni jest na przykład domyślnie używany przez
edytor atrybut Name (dlatego w kodzie widzimy x:Name). Bardzo ważna jest też prze-
strzeń nazw local. Pod tą nazwą widoczna jest przestrzeń nazw KoloryWPF, w której
jest między innymi klasa okna. Ta przestrzeń jest automatycznie deklarowana dopiero
w VS2015.

Znaczenia pozostałych atrybutów elementu Window są bardziej oczywiste: Title okre-


śla tytuł okna widoczny na pasku tytułu, a Height i Width ― jego rozmiary. Możemy
je swobodnie zmieniać, przypisując im nowe wartości, na przykład:
Title="Kolory WPF" Height="480" Width="640">

W VS2015 element Window ma zdefiniowany jeszcze jeden atrybut, którego wprawdzie


nie będziemy używać, ale warto o nim wspomnieć:
mc:Ignorable="d"

1
Odpowiada to mechanizmowi rodziców w Windows Forms. W kontrolce-pojemniku, np. panelu,
pełniącej rolę rodzica, może znajdować się kilka innych kontrolek (wszystkie dodawane są do własności
Controls pojemnika). Natomiast każda kontrolka-dziecko ma zapisaną referencję do swojego rodzica
we własności Parent. W WPF ten mechanizm został zastąpiony przez hierarchię pliku XAML.
16 Część I  Wzorzec MVVM

Jest to atrybut zdefiniowany w przestrzeni nazw mc, czyli http://schemas.openxmlformats.


org/markup-compatibility/2006. Wskazuje on prefiks atrybutów, które mają być igno-
rowane przez kompilator, ale używane są w trakcie projektowania. Wykorzystuje to
środowisko projektowe Expression Blend, w którym można otworzyć każdy projekt
interfejsu napisany w XAML.

Zdarzenia
Załóżmy, że etap budowania interfejsu aplikacji jest już ukończony. Kolejnym eta-
pem projektowania aplikacji jest określenie jej „dynamiki”. Chcemy, aby suwaki
umożliwiały ustalanie koloru prostokąta, a konkretnie żeby możliwe było ustawianie
za ich pomocą wartości trzech składowych RGB koloru.

Proponuję zacząć od rzeczy z pozoru może mało istotnej, a jednak bardzo potrzebnej
— nazw kontrolek. Zmieńmy nazwy suwaków tak, żeby odpowiadały składowym
koloru, z którymi będą związane. Nazwijmy je sliderR, sliderG i sliderB. Musimy
też nadać nazwę prostokątowi, który w tej chwili w ogóle jej nie ma. Bez tego nie bę-
dziemy mogli modyfikować jego własności z poziomu kodu C#. Aby nadać nazwę ele-
mentowi XAML, trzeba ustalić wartości atrybutów x:Name tych czterech kontrolek:
<Rectangle x:Name="rectangle" Fill="#FFF4F4F5" Margin= ... />
<Slider x:Name="sliderR" Margin= ... />
<Slider x:Name="sliderG" Margin= ... />
<Slider x:Name="sliderB" Margin= ... />

Kolejnym krokiem będzie związanie z suwakami metody zdarzeniowej reagującej na


zmianę ich pozycji. Ta z pozoru naturalna operacja niepostrzeżenie kieruje nas do
wzorca, który odbiega od obecnych standardów projektowania aplikacji WPF, Win-
dows Store czy Windows Phone, a mianowicie do wspomnianego na początku roz-
działu wzorca widoku autonomicznego, uważanego przez wielu apologetów MVVM
za antywzorzec. Kod nowo utworzonej metody zdarzeniowej umieszczony zostanie
w klasie KoloryWPF.MainWindow związanej z oknem, czyli w klasie należącej do warstwy
widoku. Często używa się na jej określenie sformułowania code-behind, czyli „kod
stojący za widokiem”. Określenie to ma negatywny wydźwięk, bo zgodnie z najbar-
dziej rygorystyczną egzegezą wzorca MVVM projekt aplikacji WPF w ogóle nie po-
winien zawierać code-behind. A to oznacza rezygnację z używania mechanizmu zdarzeń.
Wyprzedzając nieco rozwój wydarzeń, zdradzę, że w rozdziale 6. pokażę, jak można
przemycić zdarzenia do projektu opartego na wzorcu MVVM.

Póki co jednak ze zdarzenia skorzystamy. Dwukrotne kliknięcie najwyższego suwaka


na podglądzie okna tworzy domyślną metodę zdarzeniową i przenosi nas do edytora
kodu C#, ustawiając kursor w nowo utworzonej metodzie klasy MainWindow (plik
MainWindow.xaml.cs). Metoda zostanie nazwana sliderR_ValueChanged (łączy nazwę
kontrolki i nazwę zdarzenia). Jeżeli wrócimy do kodu XAML, zobaczymy, że jedno-
cześnie do elementu Slider dodany został atrybut ValueChanged, którego wartość ustalona
zostaje na nazwę metody sliderR_ValueChanged:
Rozdział 1.  Szybkie wprowadzenie do XAML 19

<Slider x:Name="sliderB"
Margin="10,0,10,10" Height="22" VerticalAlignment="Bottom"
Maximum="255"
ValueChanged="sliderR_ValueChanged"/>

Listing 1.5. Kolor prostokąta zależy od pozycji suwaków


private void sliderR_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
Color kolor = Color.FromRgb(
(byte)sliderR.Value,
(byte)sliderG.Value,
(byte)sliderB.Value);
rectangle.Fill = new SolidColorBrush(kolor);
}

Aby zsynchronizować początkowy kolor prostokąta z pozycją suwaków, po urucho-


mieniu programu wywołajmy metodę sliderR_ValueChanged z konstruktora klasy
MainWindow (listing 1.6).

Listing 1.6. Inicjowanie koloru prostokąta po uruchomieniu aplikacji


public MainWindow()
{
InitializeComponent();
sliderR_ValueChanged(null, null);
}

Osoby, które zaczynają naukę C#, znając już C++, mogą mieć poważne wątpliwości
co do metody z listingu 1.5, widząc w niej źródło wycieku pamięci. Wprawdzie
w platformie .NET zarządzaniem pamięcią zajmuje się garbage collector (odśmiecacz),
to jednak i w C# nie jest to najlepsze rozwiązanie. Tworzenie nowego obiektu typu
SolidColorBrush (typ referencyjny) przy każdym poruszeniu suwakiem jest sporym
wyzwaniem dla garbate collectora, który musi zwalniać z pamięci poprzednio uży-
wane obiekty. Proste testy przeprowadzone za pomocą Menedżera zadań pokazują, że
tylko na tej jednej własności potrafi powstać narzut 4 MB po kilku przesunięciach
suwaka od wartości minimalnej do maksymalnej. Warto zatem zmodyfikować kod tak,
aby tworzyć jedną trwałą instancję klasy SolidColorBrush i tylko zmieniać jej własność
Color. To powoduje, że kod stanie się nieco mniej przejrzysty, ale na pewno będzie
bliższy optymalnemu. W tym celu do konstruktora klasy MainWindow należy przenieść
polecenie tworzące obiekt:
rectangle.Fill = new SolidColorBrush(Colors.Black);

A w metodzie sliderR_ValueChanged należy obecne polecenie zastąpić poleceniem


modyfikującym własność Color (listing 1.7). Dodatkowo można uzupełnić je o wery-
fikację, czy obiekt po rzutowaniu operatorem as nie jest równy null.
Rozdział 1.  Szybkie wprowadzenie do XAML 21

Listing 1.9. Tworzenie własności KolorProstokąta


private Color KolorProstokąta
{
get
{
return (rectangle.Fill as SolidColorBrush).Color;
}
set
{
(rectangle.Fill as SolidColorBrush).Color = value;
}
}

Definicja własności może zawierać dwie sekcje (musi zawierać przynajmniej jedną
z nich). Sekcja get powinna zwracać obiekt typu Color zadeklarowany w sygnaturze
własności. Natomiast sekcja set otrzymuje taki obiekt w postaci predefiniowanej
zmiennej value. Zwykle własności towarzyszy prywatne pole, które przechowuje jej
wartość. W naszym przypadku własność jest tylko opakowaniem dla własności Color
przechowywanej w obiekcie rectangle.Fill, o którym zakładamy, że jest typu Solid
ColorBrush (takiego typu obiekt tworzymy w konstruktorze).

Dzięki tej własności ostatnie polecenie w metodzie sliderR_ValueChanged z listingu


1.7 może być zamienione po prostu na KolorProstokąta = kolor; (por. listing 1.10).

Listing 1.10. Zmieniona metoda zdarzeniowa


private void sliderR_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
Color kolor = Color.FromRgb(
(byte)(sliderR.Value),
(byte)(sliderG.Value),
(byte)(sliderB.Value));
KolorProstokąta = kolor;
}

Zapisywanie i odtwarzanie
stanu aplikacji
Zachowaniem, którego często oczekujemy od nowoczesnych aplikacji, jest odtwarzanie
stanu aplikacji po jej zamknięciu i ponownym otwarciu. W przypadku tak prostej
aplikacji jak nasza, w której stan aplikacji to w istocie trzy wartości typu byte, do za-
pisania jej stanu w zupełności wystarczy mechanizm ustawień aplikacji. Należy go wcze-
śniej odpowiednio skonfigurować.
1. Z menu Project wybieramy KoloryWPF Properties... i przechodzimy na
zakładkę Settings (rysunek 1.8).
Rozdział 1.  Szybkie wprowadzenie do XAML 23

Po kompilacji plik App.config kopiowany jest do katalogu bin/Debug, gdzie znaj-


dziemy go pod nazwą KoloryWPF.exe.config. Dzieje się to niezależnie od ustawienia
własności Copy to Output Directory, która w tym wypadku powinna pozostać usta-
wiona na Do not copy. Zresztą tak naprawdę plik KoloryWPF.exe.config to wcale nie
jest miejsce, w którym przechowywane będą ustawienia o zakresie użytkownika, lecz
jedynie ich wartości początkowe. Wyjaśnię to za chwilę.

Pomimo że ustawienia są łatwo dostępne poprzez wspomniany obiekt Properties.


Settings.Default, to żeby ich odczyt i zapis jeszcze bardziej uprościć, przygotujemy
dwie realizujące te zadania metody statyczne, umieszczone w osobnej klasie statycznej
Ustawienia. Takie rozwiązanie ułatwia ewentualną zmianę sposobu przechowywania
ustawień.
1. Z menu Project wybieramy polecenie Add Class... i dodajemy do projektu klasę
o nazwie Ustawienia (plik Ustawienia.cs). Jej kod modyfikujemy zgodnie
ze wzorem z listingu 1.11. Proszę zwrócić uwagę na dodaną przestrzeń nazw,
w której zdefiniowana jest klasa Color używana w WPF. Proszę także zauważyć,
że inaczej tworzę obiekt Color. Zamiast statycznej metody FromRgb, której
używaliśmy wcześniej, użyłem konstruktora domyślnego wraz z inicjatorem
obiektu (ang. object initializer). Nie stoi za tym żadna głębsza filozofia poza
chęcią pokazania innej możliwości.

Listing 1.11. Odczyt i zapis danych z ustawień aplikacji


using System.Windows.Media;

namespace KoloryWPF
{
static class Ustawienia
{
public static Color Czytaj()
{
Properties.Settings ustawienia = Properties.Settings.Default;
Color kolor = new Color()
{
A = 255,
R = ustawienia.R,
G = ustawienia.G,
B = ustawienia.B
};
return kolor;
}

public static void Zapisz(Color kolor)


{
Properties.Settings ustawienia = Properties.Settings.Default;
ustawienia.R = kolor.R;
ustawienia.G = kolor.G;
ustawienia.B = kolor.B;
ustawienia.Save();
}
}
}
24 Część I  Wzorzec MVVM

2. Korzystając z metody Ustawienia.Zapisz, zapiszmy do ustawień kolor prostokąta


w momencie zamykania okna, a tym samym zamykania całej aplikacji. Użyjemy
do tego zdarzenia Closed okna. Postępując podobnie, jak w przypadku zdarzenia
Window.KeyDown, stwórzmy metodę związaną ze zdarzeniem Window.Closed
i umieśćmy w niej polecenia widoczne na listingu 1.12.

Listing 1.12. Zapisywanie ustawień tuż przed zamknięciem aplikacji


private void Window_Closed(object sender, EventArgs e)
{
Ustawienia.Zapisz(KolorProstokąta);
}

3. Trochę więcej zamieszania będzie z odczytywaniem ustawień po uruchomieniu


aplikacji. Łatwo możemy zmienić kolor prostokąta w momencie tworzenia
pędzla, co ma miejsce w konstruktorze klasy KoloryWPF. Nie możemy jednak
zapomnieć o ustaleniu położeń suwaków (listing 1.13). A to oznacza, że czy
tego chcemy, czy nie, trzy razy niepotrzebnie wywoływana będzie metoda
zdarzeniowa sliderR_ValueChanged związana z ich zdarzeniem ValueChanged.
Można tego uniknąć, definiując zmienną logiczną, tak zwaną flagę, którą
podnosilibyśmy na czas wykonywania kodu konstruktora, a która blokowałaby
wykonywanie zawartości metody zdarzeniowej. Ale już chyba nie warto. I tak
docelowo cały kod z klasy MainWindow zostanie w następnym rozdziale usunięty.
Listing 1.13. Zmodyfikowany konstruktor klasy MainWindow
public MainWindow()
{
InitializeComponent();

Color kolor = Ustawienia.Czytaj();


rectangle.Fill = new SolidColorBrush(kolor);
sliderR.Value = kolor.R;
sliderG.Value = kolor.G;
sliderB.Value = kolor.B;
}

Ponieważ wszystkie ustawienia aplikacji, które zapisujemy w programie, należą do


ustawień użytkownika, wykonanie metody Ustawienia.Zapisz spowoduje, że platforma
.NET utworzy dla nich plik XML w katalogu domowym użytkownika (np. C:\Users\
Jacek\ lub C:\Documents and Settings\Jacek), a dokładniej w jego podkatalogu
AppData\Local\ (względnie Ustawienia lokalne\Dane aplikacji). Powstanie tam ka-
talog o nazwie aplikacji, z podkatalogiem oznaczającym konkretny plik wykonywalny
i jeszcze jednym podkatalogiem zawierającym numer wersji aplikacji. Dopiero w tym
miejscu powstanie plik XML o nazwie user.config. Plik user.config zawiera sekcję
userSettings, czyli ustawienia aplikacji z zakresu użytkownika. Taki sam zbiór usta-
wień znajdziemy w pliku KoloryWPF.exe.config, który umieszczony jest w katalogu
aplikacji i powinien być z nią rozpowszechniany. Ustawienia z pliku user.config są
jednak dynamicznie modyfikowane przez metodę z listingu 1.11, podczas gdy plik
KoloryWPF.exe.config przechowuje tylko ustawienia domyślne, jakie wprowadziliśmy
w projekcie. Do pliku user.config nie są natomiast zapisywane ustawienia z zakresu
aplikacji (element applicationSettings) — pozostają one dla aplikacji ustawieniami
tylko do odczytu.
26 Część I  Wzorzec MVVM

kandydatami na nazwy podstawowych klas modelu. Z kolei czasowniki towarzyszące


tym rzeczownikom będą prawdopodobnie nazwami kluczowych metod. Przy czym
w DDD nie chodzi oczywiście tylko o wybieranie nazw klas i metod, a przede wszystkim
o ich zawartość i wyznaczenie relacji między klasami. Ma ona odzwierciedlać relacje
pojawiające się w języku używanym przez eksperta. To oczywiście trywializacja, ale
dobrze oddaje ideę DDD.

Modele domenowe powinny być możliwie proste i „lekkie”. Nie powinny korzystać
z żadnych konkretnych mechanizmów platformy .NET — najlepiej, gdyby jedyną
używaną w nich przestrzenią nazw była przestrzeń System1. W tym podejściu klasy
modelu powinny stanowić tylko proste nośniki danych przekazywanych z bazy danych
lub innego źródła danych do wyższych warstw aplikacji. Klasy modelu nie mogą, i to
jest bardzo ważne, znać żadnych szczegółów dotyczących owych wyższych warstw —
powinny być całkowicie autonomiczne. W takim podejściu klasy modelu muszą być bar-
dzo proste, a tym samym łatwe do testowania2. Klarowne są też relacje między nimi.

Kluczowy w projektowaniu warstwy modelu jest podział odpowiedzialności — należy


jasno ustalić, za co odpowiedzialna jest która klasa. Część odpowiedzialności może,
lub nawet powinna, być wydzielona do osobnych modułów w warstwie modelu. Za
zapis danych można uczynić odpowiedzialną podwarstwę dostępu do danych (ang.
data access layer, DAL), która na przykład w postaci klasy statycznej przyjmuje instan-
cje klas domenowych i zapisuje ich stan. Podobnie logika modelu może być wydzie-
lona do osobnego modułu tak zwanej logiki biznesowej (ang. buissness logic layer,
BLL), która operuje na instancjach domenowych klas modelu.

Widok
Widok odpowiedzialny jest za kontakt z użytkownikiem. W WPF, a także w aplikacjach
Windows Phone i WinRT, widokiem jest kod XAML opisujący graficzny interfejs
użytkownika (ang. graphical user interface, GUI). Z widokiem związana jest klasa
okna, w której w poprzednim rozdziale umieszczaliśmy metody zdarzeniowe. Tworzy
ona tak zwany kod zaplecza widoku, czyli code-behind. Zgodnie z zaleceniami wzorca
MVVM kod ten powinien być ograniczony do minimum, a najlepiej, żeby go w ogóle
nie było. W tym sensie wzorzec MVVM całkowicie odwraca wzorzec widoku auto-
nomicznego. Głównym powodem unikania kodu C# w warstwie widoku, a przynajmniej
w klasie okna, jest to, że kod ten, jako silnie związany z kontrolkami, jest trudny do
przetestowania. Ponadto zanurzenie logiki prezentacyjnej w widoku znacząco utrudnia
współpracę między projektantami interfejsu tworzącymi widok a programistami odpo-
wiedzialnymi za niższe warstwy aplikacji. Zmniejsza też elastyczność projektu, utrudniając
tym samym jego zmiany.

1
Klasy tego typu nazywane są POCO, od ang. „plain-old” CRL objects. To popularne określenie
w slangu programistów C#.
2
Testowanie klas POCO nie ma jednak sensu, jeżeli zawierają one same własności.
Rozdział 2.  Wzorzec MVVM 27

Model widoku
Model widoku jest abstrakcją widoku. Jeżeli możemy sobie wyobrazić kilka wariantów
graficznego interfejsu użytkownika naszej aplikacji, dla różnych środowisk i platform, to
model widoku w tych wszystkich przypadkach powinien pozostawać taki sam. Myśląc
przez analogię: możemy sobie wyobrazić różne stoły, różnej wielkości i o różnych
kształtach, z trzema lub czterema nogami. Nie zmienia to jednak definicji stołu jako
miejsca, przy którym można usiąść i coś na nim położyć. Podobnie wiele może być
projektów widoku. Ale model widoku musi być jak definicja stołu, jego zapisana idea
— powinien być jak najprostszy, lecz kompletny. Powinien wobec tego zawierać tylko
to, co konieczne do określenia, do czego widoki mają być użyte. Warto podjąć wysiłek,
żeby doprowadzić kod modelu widoku do jak najwyższego poziomu abstrakcji. Z po-
wyższych górnolotnych rozważań wynika, że najlepszym sprawdzianem poprawności
modelu widoku są zmiany wprowadzane w widoku. Tych w trakcie rozwijania pro-
jektu zwykle nie brakuje. Jeżeli model widoku jest dobrze zaprojektowany, takie zmia-
ny widoku powinny się obyć bez jego modyfikacji. Pamiętajmy jednak, że ― jak wiele
dobrych praktyk w informatyce ― jest to raczej cel, do którego dążymy, niż twarde
wymaganie, stawiane osobie projektującej model widoku.
Funkcją modelu widoku jest udostępnienie widokowi instancji klas z warstwy modelu
(na rysunku 2.1 odpowiada to ruchowi do góry) oraz zmienianie stanu tych instancji
w wyniku działań użytkownika wykrytych w warstwie widoku (ruch w dół). W tym
drugim przypadku model widoku odpowiedzialny jest między innymi za weryfikację
przekazywanych danych. Model widoku pełni więc rolę pośrednika między warstwami
modelu i widoku, a jednocześnie adaptera dla przekazywanych danych. Owo pośred-
niczenie najczęściej odbywa się w taki sposób, że obiekty modelu są prywatnymi polami
modelu widoku. Model widoku udostępnia je lub ich części w swoich własnościach,
jest wobec tego świadomy warstwy modelu, nie powinien być natomiast świadomy
warstwy widoku ― to widok powinien być świadom modelu widoku. Połączenie między
modelem widoku a widokiem jest zwykle bardzo „luźne”. Oparte jest nie na odwoła-
niach w kodzie C#, lecz na wiązaniach danych umieszczonych w kodzie XAML. To
luźne wiązanie ułatwia niezależną pracę nad widokiem i modelem widoku i znakomicie
ułatwia wprowadzanie zmian w poszczególnych warstwach, z całkowitym ich prze-
budowywaniem włącznie. Ta druga zaleta jest szczególnie warta docenienia, choć jest
ona w większym lub mniejszym stopniu zaletą wszystkich wzorców z wyraźnie roz-
dzielonymi warstwami (modułami).
W modelu widoku zapisana jest cała logika prezentacyjna określająca procedury kon-
taktu z użytkownikiem z uwzględnieniem weryfikacji danych. Mimo tego pozostaje
łatwa do testowania, nie ma w niej bowiem odwołań do kontrolek ani założonej bez-
pośredniej interakcji z użytkownikiem.

Doskonale zdaję sobie sprawę, że dla osób, które nie miały jeszcze kontaktu ze
wzorcem MVVM albo chociażby z MVP lub MVC, większość powyższych zdań o mode-
lu widoku jest trudna do zrozumienia. Zadaniem kolejnych rozdziałów z pierwszej
części książki będzie wyjaśnienie tego na konkretnym przykładzie. Po przeczytaniu
dalszych rozdziałów warto wrócić do niniejszego i przeczytać go jeszcze raz, w ca-
łości lub przynajmniej w części dotyczącej modelu widoku. To powinno pomóc po-
układać sobie w głowie wiedzę o MVVM przedstawioną w pierwszej części.
28 Część I  Wzorzec MVVM

W przypadku aplikacji KoloryWPF modelem może być prosta klasa opisująca kolor,
zawierająca tylko trzy składowe typu byte. Odpowiedzialność za zapis stanu modelu
pozostawimy osobnej klasie statycznej należącej do warstwy modelu. Prostota naszej
aplikacji spowoduje, że model widoku będzie z początku równie prosty i w istocie
bardzo podobny do samego modelu. Z czasem dodamy do niego jednak elementy cha-
rakterystyczne dla klas modelu widoku, między innymi polecenia i mechanizm po-
wiadomień. A ponieważ podstawowym celem aplikacji jest możliwość kontrolowania
trzech składowych koloru, model widoku musi udostępniać własności reprezentujące
te składowe. Oprócz tego wyposażymy go w metodę, którą potem przekształcimy w tak
zwane polecenie, umożliwiające zapis stanu aplikacji (czyli de facto stanu modelu).

To nie jest oczywiście jedyna architektura, jaką można sobie wyobrazić dla tej aplika-
cji. Dobrym modelem mogłaby być przecież klasa Properties.Settings stworzona
przez Visual Studio w momencie określania ustawień aplikacji. Przy takim założeniu
naszym jedynym zadaniem pozostaje napisanie modelu widoku, który tę klasę udo-
stępniłby widokowi. Można również rozważyć klasę System.Windows.Media.Color,
jako klasę modelu, ale nie uważam, żeby korzystanie z klas przeznaczonych do budowa-
nia interfejsu było dobrym pomysłem na tworzenie modelu. Dlatego pozostaniemy
przy rozwiązaniu „kanonicznym”, lecz pamiętając, że wzorzec MVVM pozwala na
pewne wariacje.

Ostrzegałem już, że aplikacja, którą od tego momentu będziemy przebudowywać, jest


bardzo prosta. W kontekście uczenia się wzorca MVVM to jest jednak moim zdaniem
zaleta. Brak szczegółów związanych z bardziej skomplikowanym projektem pozwoli
Czytelnikowi łatwiej dostrzec istotę wzorca.
Rozdział 3.
Implementacja modelu
i model widoku

Model
Zacznijmy od zaprojektowania modelu. Jak pisałem w poprzednim rozdziale, przy
projektowaniu modelu dobrze jest skorzystać z metodologii DDD. W przypadku apli-
kacji KoloryWPF doprowadziła mnie ona do prostego pomysłu: model będzie się składał
tylko z jednej klasy opisującej kolor. W zasadzie sposób przechowywania w niej koloru
jest dowolny: mogą to być trzy lub cztery własności typu byte przechowujące składowe
koloru albo jedna liczba typu int (czterobajtowa) — tak przechowywane są kolory
w WinAPI. Zdecydowanie unikałbym jednak używania klasy System.Windows.Media.
Color jako zbyt zależnej od biblioteki WPF. Jak już podkreślałem w poprzednim
rozdziale, klasa modelu powinna być w jak największym stopniu wolna od wszelkich
zależności. Najlepiej, aby dało się ją skompilować w dowolnym typie projektu .NET.
Z tego powodu, to jest aby zachować czystość modelu, warto z niego wydzielić pod-
warstwę dostępu do danych.

Proponuję do przechowania składowych użyć trzech liczb typu byte o nazwach R, G i B.


Dobrym pomysłem jest utworzenie dla warstw modelu i modelu widoku dwóch osobnych
projektów bibliotek DLL lub PCL. Nie chcę jednak tego robić w pierwszym projekcie,
aby go dodatkowo nie komplikować. Ponadto konieczne byłoby wówczas ponowne
definiowanie ustawień aplikacji w bibliotece modelu, czego też chcę uniknąć. Odrębność
warstw zaznaczać będziemy, umieszczając ich pliki w osobnych folderach — Visual
Studio odzwierciedla nazwy folderów w przestrzeniach nazw klas.
1. Korzystając z podokna Solution Explorer, stwórzmy podkatalog o nazwie
Model.
2. Następnie do tego katalogu dodajmy plik klasy, który nazwiemy Kolor.cs.
3. W efekcie klasa o nazwie Kolor powinna znaleźć się w przestrzeni nazw
KoloryWPF.Model.
30 Część I  Wzorzec MVVM

4. Zmieniamy jej zakres na publiczny, dodając do deklaracji klasy modyfikator


public.
5. Następnie zdefiniujmy w tej klasie trzy automatycznie implementowane
własności (ang. auto-implemented properties) typu byte przechowujące
składowe o nazwach R, G i B.
6. Dodajmy także konstruktor pozwalający ustalać wartości wszystkich trzech
składowych.

Cała ta prosta klasa widoczna jest na listingu 3.1. Należy pamiętać, aby ustalić jej zakres
dostępności na public.

Listing 3.1. Jedyna klasa modelu w aplikacji Kolory


namespace KoloryWPF.Model
{
public class Kolor
{
public byte R { get; set; }
public byte G { get; set; }
public byte B { get; set; }

public Kolor(byte r, byte g, byte b)


{
this.R = r;
this.G = g;
this.B = b;
}
}
}

Jak widać, klasa modelu nie wie nic ani o widoku, ani o modelu widoku. Korzysta
tylko z przestrzeni nazw System, w której zdefiniowany jest typ System.Byte (a po-
nieważ w kodzie obecny jest tylko jej alias byte, nie jest konieczne nawet polecenie
using System;). W bardziej skomplikowanym projekcie zależności między klasami
modelu muszą się oczywiście pojawić, ale powinny być ograniczone do tej jednej
warstwy. Nawet formalnie należące do warstwy modelu klasy obsługujące trwały za-
pis stanu modelu (zaraz je zdefiniujemy) powinny raczej korzystać z klas modeli, a nie
odwrotnie.

Warstwa dostępu do danych


Nazwa „warstwa dostępu do danych” (ang. data access layer, DAL) używana jest za-
pewne w przypadku naszej aplikacji na wyrost. Tworzy ją bowiem tylko jedna klasa.
Tak samo będzie jednak w przypadku pozostałych warstw. Do przechowywania danych
nadal będziemy używać klasy Ustawienia, ale zmodyfikujemy ją tak, żeby zamiast klasy
System.Windows.Media.Color obsługiwała klasę KoloryWPF.Model.Kolor (listing 3.2).
32 Część I  Wzorzec MVVM

1. Ponownie dodajemy do projektu folder, tym razem o nazwie ModelWidoku.


2. Zaznaczmy go w podoknie Solution Explorer i z menu Project wybierzmy
polecenie Add Class... Dodajmy w ten sposób do projektu plik EdycjaKoloru.cs.
3. Nowy plik powinien zawierać klasę o nazwie EdycjaKoloru znajdującą się
w przestrzeni nazw KoloryWPF.ModelWidoku.
4. W nowej klasie tworzymy prywatne pole-instancję klasy modelu. to jest klasy
KoloryWPF.Model.Kolor.
5. Stan tego pola udostępnimy za pomocą trzech zdefiniowanych w klasie
EdycjaKoloru własności typu byte o nazwach R, G i B.
6. Oprócz tego tymczasowo zdefiniujemy własność Color udostępniającą kolor
skonwertowany do typu System.Windows.Media.Color. Samą konwersję
umieścimy w metodzie rozszerzającej ToColor zdefiniowanej w klasie
Rozszerzenia w tym samym pliku.
7. Do trwałego zapisu i odczytu danych wykorzystamy zmodyfikowane przed
chwilą metody klasy Ustawienia, których wywołania będą znajdowały się
w modelu widoku.

Wszystkie te elementy widoczne są na listingu 3.3.

Listing 3.3. Klasa modelu widoku EdycjaKoloru i klasa Rozszerzenia


using System.Windows.Media;

namespace KoloryWPF.ModelWidoku
{
using Model;

public class EdycjaKoloru


{
private readonly Kolor kolor = Ustawienia.Czytaj();

public byte R
{
get
{
return kolor.R;
}
set
{
kolor.R = value;
}
}

public byte G
{
get
{
return kolor.G;
}
set
Rozdział 3.  Implementacja modelu i model widoku 33

{
kolor.G = value;
}
}

public byte B
{
get
{
return kolor.B;
}
set
{
kolor.B = value;
}
}

public Color Color


{
get
{
return kolor.ToColor();
}
}

public void Zapisz()


{
Ustawienia.Zapisz(kolor);
}
}

static class Rozszerzenia


{
public static Color ToColor(this Kolor kolor)
{
return new Color()
{
A = 255,
R = kolor.R,
G = kolor.G,
B = kolor.B
};
}
}
}

Alternatywne rozwiązania
Rozwiązanie z listingu 3.3 wydaje się dość naturalne. Zastanówmy się jednak także
nad drugą ze wspomnianych wcześniej możliwości, w której nie tworzymy trwałej
instancji klasy modelu, a jego stan jest kopiowany do modelu widoku. Klasa modelu
pełni więc jedynie ograniczoną rolę nośnika. Takie alternatywne rozwiązanie widoczne
34 Część I  Wzorzec MVVM

jest na listingu 3.4 w klasie EdycjaKoloru2. Kod wydaje się znacznie prostszy, bo
możliwe jest, przynajmniej na razie, użycie domyślnie implementowanych własności.
Nie da się jednak tej prostoty utrzymać przy dalszym rozwoju projektu. Zwróćmy
uwagę, że różnica implementacji klas EdycjaKoloru i EdycjaKoloru2 nie wpływa na
ich interfejsy, które są takie same.

Listing 3.4. Nieco odmienne rozwiązanie modelu widoku


public class EdycjaKoloru2
{
public EdycjaKoloru2()
{
Kolor kolor = Ustawienia.Czytaj();
R = kolor.R;
G = kolor.G;
B = kolor.B;
}

public byte R { get; set; }


public byte G { get; set; }
public byte B { get; set; }

public Color Color


{
get
{
return Color.FromRgb(R, G, B);
}
}

public void Zapisz()


{
Kolor kolor = new Kolor(R, G, B);
Ustawienia.Zapisz(kolor);
}
}

Trzecie ze wspomnianych rozwiązań z pozoru może wydawać się jeszcze bardziej


atrakcyjne ze względu na swoją „zwięzłość”. Polega ono na zdefiniowaniu w modelu
widoku publicznie dostępnej instancji modelu (listing 3.5). Jednak z powodów, które
wspominałem w poprzednim rozdziale, a które bardziej szczegółowo omówię w ko-
lejnym, to rozwiązanie stanie się niezbyt wygodne w momencie, kiedy będziemy chcieli
zaimplementować mechanizm powiadamiania widoku o zmianach, jakie zachodzą w mo-
delu widoku, a więc w momencie implementacji interfejsu INotifyPropertyChanged.
Ponieważ w tym scenariuszu widok ma bezpośredni dostęp do instancji modelu i sam
zmienia jego stan, model widoku nie ma prostej możliwości wychwytywania zmian
stanu aplikacji — trzeba to aranżować „na około”. W tej sytuacji interfejs INotify
PropertyChanged powinien być implementowany przez klasę modelu, co pewnie nie
jest rozwiązaniem godnym polecenia, choć czasem stosowanym.
Rozdział 3.  Implementacja modelu i model widoku 35

Listing 3.5. Kolejna alternatywna wersja modelu widoku


public class EdycjaKoloru3
{
private readonly Kolor kolor = Ustawienia.Czytaj();

public Kolor Kolor


{
get
{
return kolor;
}
}

public Color Color


{
get
{
return Kolor.ToColor();
}
}

public void Zapisz()


{
Ustawienia.Zapisz(Kolor);
}
}

Model widoku może zawierać wiele klas — tyle, ile istotnie różnych widoków po-
trzebuje nasza aplikacja. Widokiem może być całe okno, ale także jego część składowa
(np. pasek narzędzi i menu mogą odwzorowywać jeden model widoku, zawartość okna
— inny). W naszej prostej aplikacji mamy tylko jeden widok i w konsekwencji sens ma
tylko jeden model widoku.

Co więcej, żaden ze zdefiniowanych powyżej modeli widoku nie podejmuje się zadania
weryfikacji danych otrzymywanych z widoku. A przynajmniej nie w sposób, w jaki
zwykle przeprowadzana jest walidacja. Za rodzaj kontroli danych uznany może być
użyty w modelu widoku typ danych byte — wymusza to, że składowe koloru są licz-
bami całkowitymi z zakresu od 0 do 255.

Ratujemy widok
Po zmianach, jakie wprowadziliśmy w projekcie, w szczególności po zmianie metod
klasy Ustawienia, kod klasy MainWindows nie będzie chciał się skompilować. Aby to
było możliwe, należy w konstruktorze klasy MainWindow zmienić polecenie odczytu
ustawień:
Color kolor = Ustawienia.Czytaj().ToColor();

a w metodzie Window_Close polecenie zapisu:


Ustawienia.Zapisz(
new Kolor(KolorProstokąta.R, KolorProstokąta.G, KolorProstokąta.B));
Rozdział 4.
Wiązanie danych
(data binding)

Instancja modelu widoku


i kontekst danych
Zwiążmy teraz kod modelu widoku, czyli klasę EdycjaKoloru, z widokiem. Do tego
potrzebujemy instancji modelu widoku, która będzie widoczna w kodzie XAML opi-
sującym interfejs widoku. Jeżeli to wiązanie nam się uda, będziemy mogli zacząć stop-
niowo eliminować code-behind, czyli usuwać kod z klasy MainWindow.cs.

Zacznijmy od utworzenia w widoku instancji modelu widoku. Umieśćmy ją w zaso-


bach okna, dodając do kodu XAML fragment zaznaczony na listingu 4.1. Obecność
klucza (atrybut x:Key) jest konieczna, bo Window.Resources jest słownikiem, a każdy
element słownika musi mieć klucz typu string.

Listing 4.1. Dodanie instancji modelu widoku do zasobów okna


<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:KoloryWPF"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
Title="Kolory WPF" Height="480" Width="640"
KeyDown="Window_KeyDown" Closed="Window_Closed">
<Window.Resources>
<vm:EdycjaKoloru x:Key="edycjaKoloru" />
</Window.Resources>
<Grid>
...
38 Część I  Wzorzec MVVM

Kolejnym krokiem będzie ustanowienie tego obiektu kontekstem danych dla siatki
(kontrolki Grid). Siatka zawiera pozostałe kontrolki, a zatem odziedziczą one jej kontekst
danych, chyba że zostanie on w ich elementach nadpisany. Jeżeli instancję modelu
widoku umieściliśmy w zasobach okna, to nie możemy jej ustanowić kontekstem danych
tego okna. Zasoby dostępne są bowiem tylko w elementach zagnieżdżonych. Warto
podkreślić, że nie jest wcale konieczne, aby wszystkie kontrolki miały wspólny kon-
tekst danych. Możliwe jest ustalanie osobnego kontekstu nawet dla każdej z osobna
(każda kontrolka ma własność DataContext). Wspólny kontekst dla większej grupy
kontrolek jest jednak wygodnym rozwiązaniem i dobrze się sprawdza w większości
przypadków, także w naszej aplikacji.

Aby ustalić kontekst danych siatki, należy użyć atrybutu DataContext w odpowiadającym
mu elemencie XAML, przypisując mu obiekt utworzony w zasobach okna. W przypadku
siatki będzie to:
<Grid DataContext="{StaticResource edycjaKoloru}">

Alternatywne rozwiązanie
Jeżeli zależy nam, żeby i samo okno miało kontekst wiązania, możemy albo umieścić
instancję modelu w zasobach aplikacji (plik App.xaml), albo w ogóle pominąć zasoby
i stworzyć instancję modelu widoku, od razu przypisując ją do własności DataContext.
Pokazuje to listing 4.2. Wybór jednego z tych sposobów nie wpływa na sposób wią-
zania kontrolek.

Listing 4.2. Inne miejsce utworzenia instancji modelu widoku


<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:KoloryWPF"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
Title="Kolory WPF" Height="480" Width="640" Closed="Window_Closed">
<Window.DataContext>
<vm:EdycjaKoloru />
</Window.DataContext>
<Grid>
...
Rozdział 4.  Wiązanie danych (data binding) 39

Wiązanie pozycji suwaków


i koloru prostokąta
Zwiążmy pozycję suwaków z własnościami R, G i B modelu widoku. W tym celu mo-
dyfikujemy w kodzie XAML trzy elementy typu Slider, dodając do nich atrybuty Value:
<Slider x:Name="sliderR" Margin="10,0,10,56" Height="18" VerticalAlignment="Bottom"
Maximum="255" Value="{Binding R, Mode=TwoWay}" />
<Slider x:Name="sliderG" Margin="10,0,10,33" Height="18" VerticalAlignment="Bottom"
Maximum="255" Value="{Binding G, Mode=TwoWay}" />
<Slider x:Name="sliderB" Margin="10,0,10,10" Height="18" VerticalAlignment="Bottom"
Maximum="255" Value="{Binding B, Mode=TwoWay}" />

Wiązanie suwaków z własnościami modelu jest dwustronne, o czym świadczy atrybut


Mode=TwoWay wiązania. Tak musi być, żeby model widoku mógł wyznaczać pozycję
suwaków na przykład po uruchomieniu aplikacji, ale aby jednocześnie „czuł”, gdy
pozycja suwaków zostanie zmieniona przez użytkownika. Taki sposób wiązania jest do-
myślny w WPF, ale już nie w Windows Phone.

W kodzie wiązania może pojawić się Path, na przykład Value="{Binding Path=B, Mode=
TwoWay}". Wskazuje wówczas na własność, z którą związana jest wiązana własność.
Fragment ten można jednak pominąć. Tak właśnie zrobiłem w powyższym listingu.

Proszę zwrócić uwagę, że ze wszystkich trzech elementów Slider usunąłem zdarzenie


Value_Changed. To spowoduje, że kolor prostokąta nie będzie na razie zmieniany po
zmianie pozycji suwaków. W konsekwencji można z klasy MainWindow usunąć metodę
zdarzeniową sliderR_ValueChanged. W efekcie nazwy suwaków nie będą używane,
więc można by je także usunąć z kodu XAML. Nie zrobimy tego jednak ze względu na
zmiany, które planuję zrobić w projekcie później.

Spróbujmy także podłączyć własność Fill (ang. „wypełnienie”) prostokąta do modelu


widoku. To powinno przywrócić możliwość kontrolowania koloru prostokąta za po-
mocą suwaków. To wiązanie jest jednak nieco bardziej skomplikowane, bo własność
Fill nie jest typu Color, lecz typu Brush (ang. „pędzel”). A konkretnie jej typ to dzie-
dzicząca z abstrakcyjnej klasy Brush klasa SolidColorBrush, która reprezentuje „pę-
dzel” zapełniający figurę jednolitym kolorem. Możemy jednak sięgnąć głębiej i związać
własność Color pędzla z własnością Color zdefiniowaną w modelu widoku. Pozwala na
to następujące wiązanie:
<Rectangle x:Name="rectangle" Margin="10,10,10,91" Stroke="Black">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Path=Color, Mode=OneWay}" />
</Rectangle.Fill>
</Rectangle>

Tym razem użyłem wiązania jednostronnego (atrybut Mode=OneWay). W efekcie model


widoku może zmieniać kolor prostokąta, ale nie ma sposobu, aby wystąpił proces
odwrotny. Raz, że kontrolka Rectangle nie daje takich możliwości, a dwa — własność
Color zdefiniowana w modelu widoku jest tylko do odczytu.
40 Część I  Wzorzec MVVM

Zmiany w code-behind
Skompilujmy aplikację i przetestujmy ją. Niestety nie działa! Kolor prostokąta nie zmie-
nia się, gdy zmieniamy pozycje suwaków. Co więcej, aplikacja nawet nie zapisuje po-
zycji suwaków przy zamknięciu i ponownym otwarciu aplikacji. To ostatnie na szczęście
możemy łatwo naprawić, korzystając z tego, że działa już wiązanie suwaków z mo-
delem widoku (listing 4.3). W metodzie Window_Closed odczytujemy referencję do in-
stancji modelu widoku umieszczonej w zasobach okna (ewentualnie z kontekstu wią-
zania), aby wywołać jej metodę Zapisz. Takie rozwiązanie nie „pachnie” zbyt dobrze,
ale to tylko tymczasowe rozwiązanie mające utrzymać działanie aplikacji w trakcie
transformacji projektu ze wzorca AV do MVVM. Niedługo się go pozbędziemy.

Listing 4.3. Łatanie code-behind


using KoloryWPF.ModelWidoku;

namespace KoloryWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void Window_KeyDown(object sender, KeyEventArgs e)


{
if (e.Key == Key.Escape) this.Close();
}

private void Window_Closed(object sender, EventArgs e)


{
EdycjaKoloru edycjaKoloru = this.Resources["edycjaKoloru"] as
EdycjaKoloru;
edycjaKoloru.Zapisz();
}
}
}

Ponieważ referencję do obiektu modelu widoku odczytujemy z zasobów zdefiniowanych


w kodzie XAML widoku, istnieje możliwość, że operacja ta może się nie powieść
i zmienna edycjaKoloru będzie miała wartość null. Wówczas próba wywołania metody
Zapisz spowoduje wystąpienie wyjątku NullReferrenceException. Aby się przed
tym uchronić, można w VS2015 zastąpić operator dostępu . (kropka) operatorem ?.
(w VS2013 trzeba użyć zwykłej instrukcji warunkowej). Jest to jednak broń obo-
sieczna ― ja wolę dowiedzieć się o błędzie już w trakcie projektowania aplikacji niż
zostać zaskoczony brakiem zapisu danych, choć aplikacja nie zgłasza żadnego błędu.
Rozdział 4.  Wiązanie danych (data binding) 41

Widoczna na listingu 4.3 wersja metody Window_Close jest odpowiednia, jeżeli instan-
cję modelu widoku przechowujemy w zasobach okna. Jeśli jej instancja przechowy-
wana jest bezpośrednio we własności DataContext, pierwsze polecenie metody po-
winno zostać zastąpione przez:
EdycjaKoloru edycjaKoloru = this.DataContext as EdycjaKoloru;

Warto zwrócić uwagę, że także w pierwszym przypadku, to znaczy gdy model widoku
zdefiniowany jest jako element zasobów, a własność DataContext siatki jest do niego
tylko „dowiązana”, można w code-behind odczytać referencję do instancji modelu
widoku z własności DataContext. Z poziomu kodu C# nie ma znaczenia, w jaki sposób
ustawiamy wartość własności w kodzie XAML.

Wykorzystajmy to, że musieliśmy zajrzeć do klasy MainWindow, i zróbmy w niej trochę


porządków. Możemy usunąć całą (poza wywołaniem metody InitializeComponent)
zawartość konstruktora, a także metodę zdarzeniową sliderR_ValueChanged i niepo-
trzebną już własność KolorProstokąta. Dzięki temu niepożądany code-behind zosta-
nie znacznie zredukowany.

Implementacja interfejsu
INotifyPropertyChanged
Zdołaliśmy rozwiązać jeden problem — pozycja suwaków powinna być już zapa-
miętywana. Niestety kolor prostokąta nadal zmieniany jest tylko raz, tuż po urucho-
mieniu aplikacji. Później pozostaje niewrażliwy na to, co robimy z suwakami. Po-
wodem jest to, że pomimo ustanowionych wiązań widok wcale nie jest powiadamiany
o zmianach stanu modelu widoku. Co należy zrobić, aby powiadomienia zaczęły być
przesyłane? Mechanizm wiązań XAML wykorzystuje do tego interfejs INotifyProperty
Changed, który powinien być zaimplementowany w klasie modelu widoku.

Aby model widoku powiadamiał widok o zmianach swojego stanu, należy:


1. Do definicji klasy EdycjaKoloru dodajmy deklarację implementacji interfejsu
INotifyPropertyChanged:
public class EdycjaKoloru : INotifyPropertyChanged

2. Przestrzeń nazw, w której zdefiniowany jest ten interfejs, nie jest widoczna.
W Visual Studio 2013 i wcześniejszych wersjach wystarczy ustawić kursor
edycji na wpisanej nazwie interfejsu i z menu kontekstowego edytora wybrać
polecenie Resolve (lub użyć wartego zapamiętania klawisza skrótu Ctrl+.),
aby sekcja poleceń using została uzupełniona o polecenie dołączające przestrzeń
System.ComponentModel. W Visual Studio 2015, w tym samym menu kontekstowym,
zobaczymy pozycję Quick Actions (dostępna także po użyciu skrótu Ctrl+.
lub z rozwijanej listy przy ikonie żarówki widocznej z lewej strony edytora).
Po wybraniu tej pozycji zobaczymy kolejne menu, w którym widoczna jest
pozycja dodająca polecenie using z odpowiednią przestrzenią nazw.
42 Część I  Wzorzec MVVM

3. Ponownie otwórzmy menu kontekstowe edytora na rzecz nazwy interfejsu.


W Visual Studio 2013 wybierzmy z niego polecenie Implement Interface/
Implement Interface. W wersji 2015 wybierzmy Quick Actions, a następnie
Implement Interface. Wówczas do klasy dodany zostanie element wymagany
przez interfejs INotifyPropertyChanged, a więc zdarzenie PropertyChanged:
public event PropertyChangedEventHandler PropertyChanged;

To z tego zdarzenia korzysta mechanizm wiązania XAML. Naszym zadaniem


jest jego zgłoszenie zawsze wtedy, gdy zmieniany jest stan modelu widoku,
wskazując w ten sposób własności, których wartość uległa zmianie.
4. Aby ułatwić sobie zadanie, do klasy dodajmy metodę pomocniczą
OnPropertyChanged (listing 4.4) podobną do tych, jakie zwykle towarzyszą
zdarzeniom. Nasza nie będzie jednak do końca typowa. Jej argumentem będzie
tablica nazw własności, o których zmianie chcemy powiadomić widok. Zdarzenie
będzie wywoływane tyle razy, ile nazw podamy. Dzięki temu, że użyjemy
modyfikatora params, nie będziemy musieli jawnie tworzyć tablicy — wystarczy,
iż będziemy podawać nazwy własności jako kolejne argumenty metody.
To będzie wygodne rozwiązanie, znacznie ułatwiające przygotowywanie kodu
nawet w tak prostej aplikacji jak nasza, w której model widoku ma tylko cztery
publiczne własności. Zwróćmy bowiem uwagę, że każdej zmianie stanu towarzyszy
zmiana przynajmniej dwóch własności modelu widoku: odpowiedniej składowej
koloru i własności Color1.

Listing 4.4. Metoda pomocnicza służąca do uruchamiania metod „subskrybujących” zdarzenie


private void OnPropertyChanged(params string[] nazwyWłasności)
{
if (PropertyChanged != null)
{
foreach(string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
}

5. Metodę OnPropertyChanged należy wywołać w sekcjach set zdarzeń R, G i B.


Dla przykładu w zdarzeniu R powinniśmy wywołać tę metodę z argumentami
"R" i "Color". Kod klasy modelu widoku ze wszystkimi zmianami widoczny
jest na listingu 4.5.

Listing 4.5. Zmieniona klasa modelu widoku


using System.Windows.Media;
using System.ComponentModel;

1
W tym kontekście warto wspomnieć atrybut CallerMemberNameAttribute, który można wstawić przed
deklarację parametru metody obsługującej zmianę własności. Spowoduje on automatyczne przypisanie
temu parametrowi wartości będącej łańcuchem-nazwą własności, z której to wywołanie nastąpiło.
W naszym przypadku, ponieważ zawsze wywołujemy tę funkcję dla większej liczby nazw własności,
takie rozwiązanie nie jest praktyczne.
Rozdział 4.  Wiązanie danych (data binding) 43

namespace KoloryWPF.ModelWidoku
{
public class EdycjaKoloru : INotifyPropertyChanged
{
Kolor kolor = Ustawienia.Czytaj();

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach(string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new
PropertyChangedEventArgs(nazwaWłasności));
}
}

public byte R
{
get
{
return kolor.R;
}
set
{
kolor.R = value;
OnPropertyChanged("R", "Color");
}
}

public byte G
{
get
{
return kolor.G;
}
set
{
kolor.G = value;
OnPropertyChanged("G", "Color");
}
}

public byte B
{
get
{
return kolor.B;
}
set
{
kolor.B = value;
OnPropertyChanged("B", "Color");
}
}

public Color Color


44 Część I  Wzorzec MVVM

{
get
{
return kolor.ToColor();
}
}

public void Zapisz()


{
Ustawienia.Zapisz(kolor);
}
}

...

Teraz, gdy uruchomimy aplikację, wreszcie znowu będzie działać — zmiana pozycji
suwaków znowu będzie powodować zmianę koloru prostokąta. Co więcej, pozycja
suwaków będzie prawidłowo odwzorowana w instancji modelu i ― na razie z użyciem
code-behind ― zapisywana w ustawieniach aplikacji.

Zwróćmy uwagę na drobny szczegół. Otóż bez wcześniejszego usunięcia z konstruktora


klasy MainWindow poleceń
Color kolor = Ustawienia.Czytaj().ToColor();
rectangle.Fill = new SolidColorBrush(kolor);

działanie aplikacji nie byłoby możliwe. Dlaczego? Kod klasy MainWindow wykonywa-
ny jest po interpretacji kodu XAML. To oznacza, że obiekt SolidColorBrush, który
jest domyślnym pędzlem prostokąta, był w konstruktorze klasy MainWindow zastępowany
nowym obiektem tego samego typu. Ale wiązanie zapisane w XAML dotyczy pierwot-
nego pędzla, który nie był faktycznie używany i był usuwany przez kolekcjonera śmieci.
W efekcie ruchy suwaków zmieniałyby stan modelu widoku i modelu, ale nie wpły-
wałyby na kolor prostokąta.

Powiadomienia w alternatywnych
modelach widoku
W poprzednim rozdziale krótko opisałem dwie inne możliwości skonstruowania modelu
widoku. Sprawdźmy teraz, jak sobie one poradzą przy wiązaniu danych. Zacznijmy
od implementacji interfejsu INotifyPropertyChanged w klasie EdycjaKoloru2. Nie-
stety, aby móc wywołać metodę OnPropertyChanged, musimy zrezygnować z domyśl-
nie implementowanych własności. A to oznacza, że klasa straci swój największy atut,
czyli niewielką ilość kodu (listing 4.6). Ponieważ publiczne metody i własności klasy
EdycjaKoloru2 są takie same jak klasy EdycjaKoloru, to aby użyć jej w widoku, wystar-
czy zmienić tylko nazwę klasy w kodzie XAML i w code-behind dodać „2” na końcu.
Rozdział 4.  Wiązanie danych (data binding) 45

Listing 4.6. Implementacja interfejsu INotifyPropertyChanged w klasie EdycjaKoloru2


public class EdycjaKoloru2 : INotifyPropertyChanged
{
public EdycjaKoloru2()
{
Kolor kolor = Ustawienia.Czytaj();
R = kolor.R;
G = kolor.G;
B = kolor.B;
}

private byte r, g, b;

public byte R
{
get
{
return r;
}
set
{
r = value;
OnPropertyChanged("R", "Color");
}
}

public byte G
{
get
{
return g;
}
set
{
g = value;
OnPropertyChanged("G", "Color");
}
}

public byte B
{
get
{
return b;
}
set
{
b = value;
OnPropertyChanged("B", "Color");
}
}

public Color Color


{
get
{
46 Część I  Wzorzec MVVM

return Color.FromRgb(R, G, B);


}
}

public void Zapisz()


{
Kolor kolor = new Kolor(R, G, B);
Ustawienia.Zapisz(kolor);
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach (string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
}
}

Zupełnie inaczej to wygląda w przypadku trzeciego wariantu modelu widoku, czyli


klasy EdycjaKoloru3. W tej klasie nie definiujemy własności pozwalających na dostęp
do poszczególnych własności modelu, lecz po prostu udostępniamy jego instancję
„w całości” (listing 3.5). Dodatkowo definiujemy własność tylko do odczytu Color
konwertującą kolor na typ System.Windows.Media.Color. Niestety takie podejście, w któ-
rym w modelu widoku są tylko dwie własności tylko do odczytu, uniemożliwia nam
użycie zdarzenia PropertyChanged w taki sposób jak w powyższych przykładach. Widok
będzie bowiem wiązany z elementami składowymi samego modelu i w ten sposób
będzie go modyfikował, pomijając pośrednictwo własności modelu widoku. Kod wła-
sności Kolor w modelu widoku nie będzie wywoływany przy każdej zmianie pozycji
suwaków, a jedynie raz w momencie wiązania. A to oznacza, że nie tylko klasa mo-
delu widoku (ze względu na jej własność Color), ale również klasa modelu musi im-
plementować interfejs INotifyPropertyChanged. Listing 4.7 pokazuje modyfikacje mo-
delu, listing 4.8 — modelu widoku, listing 4.9 — kodu XAML widoku, a 4.10 —
metody Window_Closed w code-behind.

Listing 4.7. Model implementujący interfejs INotifyPropertyChanged


using System.ComponentModel;

namespace KoloryWPF.Model
{
public class Kolor : INotifyPropertyChanged
{
private byte r, g, b;

public byte R
{
get
{
return r;
Rozdział 4.  Wiązanie danych (data binding) 47

}
set
{
r = value;
OnPropertyChanged("R");
}
}

public byte G
{
get
{
return g;
}
set
{
g = value;
OnPropertyChanged("G");
}
}

public byte B
{
get
{
return b;
}
set
{
b = value;
OnPropertyChanged("B");
}
}

public Kolor(byte r, byte g, byte b)


{
this.R = r;
this.G = g;
this.B = b;
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach (string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
}
}
}
48 Część I  Wzorzec MVVM

Listing 4.8. Klasa modelu widoku korzystająca ze zdarzenia PropertyChanged modelu


public class EdycjaKoloru3 : INotifyPropertyChanged
{
private Kolor kolor = Ustawienia.Czytaj();

public Kolor Kolor


{
get
{
return kolor;
}
}

public Color Color


{
get
{
return Kolor.ToColor();
}
}

public void Zapisz()


{
Ustawienia.Zapisz(Kolor);
}

public EdycjaKoloru3()
{
Kolor.PropertyChanged +=
(object sender, PropertyChangedEventArgs e) =>
{
OnPropertyChanged("Color");
};
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach (string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
}
}

Listing 4.9. Zmiany w kodzie XAML widoku


<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Rozdział 4.  Wiązanie danych (data binding) 49

xmlns:local="clr-namespace:KoloryWPF"
mc:Ignorable="d"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
Title="Kolory WPF" Height="480" Width="640"
KeyDown="Window_KeyDown" Closed="Window_Closed">
<Window.Resources>
<vm:EdycjaKoloru3 x:Key="edycjaKoloru" />
</Window.Resources>
<Grid DataContext="{StaticResource edycjaKoloru}">
<Rectangle x:Name="rectangle" Margin="10,10,10,91" Stroke="Black">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Color, Mode=OneWay}" />
</Rectangle.Fill>
</Rectangle>
<Slider x:Name="sliderR" Margin="10,0,10,64" Height="22"
VerticalAlignment="Bottom"
Maximum="255" Value="{Binding Kolor.R, Mode=TwoWay}" />
<Slider x:Name="sliderG" Margin="10,0,10,37" Height="22"
VerticalAlignment="Bottom"
Maximum="255" Value="{Binding Kolor.G, Mode=TwoWay}" />
<Slider x:Name="sliderB" Margin="10,0,10,10" Height="22"
VerticalAlignment="Bottom"
Maximum="255" Value="{Binding Kolor.B, Mode=TwoWay}" />
</Grid>
</Window>

Listing 4.10. Metoda zdarzeniowa uruchamiana przed zamknięciem aplikacji


private void Window_Closed(object sender, EventArgs e)
{
EdycjaKoloru3 edycjaKoloru = this.Resources["edycjaKoloru"] as EdycjaKoloru3;
edycjaKoloru.Zapisz();
}

Widok jest związany zarówno z własnościami modelu, jak i z własnością modelu wi-
doku (listing 4.9). Suwaki odnoszą się bezpośrednio do modelu, do jego własności R,
G i B, a prostokąt ― do własności Color zdefiniowanej w modelu widoku. Dlatego obie
klasy Model.Kolor i ModelWidoku.EdycjaKoloru3 muszą implementować interfejs INotify
PropertyChanged. Ponieważ tylko model jest zmieniany z poziomu widoku, model
widoku o zmianach koloru może się dowiedzieć, jedynie subskrybując zdarzenie Property
Changed modelu. To właśnie robi w konstruktorze (listing 4.8). W efekcie, gdy nastąpi
zmiana którejkolwiek składowej koloru w modelu, model widoku zgłasza automatycznie
zmianę własności Color. Jak widać na powyższych listingach, takie podejście powo-
duje, że pomimo początkowej prostoty klas teraz wszystkie musieliśmy rozbudować
o dodatkowy kod związany z mechanizmem powiadamiania o zmianach. Tylko metoda
zdarzeniowa Window_Closed z klasy MainWindow uprościła się nieco dzięki temu, że mamy
bezpośredni dostęp do instancji klasy Kolor i nie musimy tworzyć tymczasowego jej
egzemplarza.
50 Część I  Wzorzec MVVM

Interfejs INotifyDataErrorInfo
Klasa modelu widoku w WPF może implementować interfejsy IDataErrorInfo (od
.NET 3.5) lub INotifyDataErrorInfo (od .NET 4.5), które umożliwiają monitorowanie
danych przesyłanych w ramach wiązania danych. Opis pierwszego interfejsu, wraz
z odnośnikami do dalszych informacji, można znaleźć na stronie http://blogs.msdn.com/
b/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx. Nowszy interfejs, znacznie
usprawniony i działający asynchronicznie, opisany został na stronie https://msdn.
microsoft.com/en-us/library/system.componentmodel.inotifydataerrorinfo(v=vs.110).aspx
w wersji dla .NET 4.5. Bardziej przydatny jest jednak opis dla wersji Silverlight ze
strony https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifydataerro-
-rinfo(v=vs.95).aspx. Ponadto warto przeczytać komentarz ze strony http://stackoverflow.
com/questions/19402840/net-4-5-should-i-use-idataerrorinfo-or-inotifydataerrorinfo.

Klasa ObservedObject
Każda klasa modelu widoku, jeżeli w projekcie jest ich więcej, powinna implementować
interfejs INotifyPropertyChanged. To oznacza konieczność zdefiniowania w każdej
z nich zdarzenia PropertyChanged, a w konsekwencji także metod OnPropertyChanged.
Aby uniknąć powielania tych elementów w każdej klasie modelu widoku, można zdefi-
niować prostą klasę bazową, w której ten interfejs będzie już zaimplementowany (listing
4.11). Wówczas wystarczy, aby klasy modelu widoku dziedziczyły po tej klasie2. Taka
lub podobna klasa obecna jest w większości frameworków wspierających projekto-
wanie aplikacji MVVM.

Listing 4.11. Klasa obserwabli


using System.ComponentModel;

namespace KoloryWPF.ModelWidoku
{
public abstract class ObservedObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach (string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs
(nazwaWłasności));
}
}
}
}

2
Por. inny pomysł rozwiązania problemu w interfejsie IObservable<>.
Rozdział 4.  Wiązanie danych (data binding) 51

Przeciwstawiam wzorzec autonomicznego widoku (AV) opartemu na zdarzeniach


wzorcowi MVVM korzystającemu z wiązań i poleceń. To oczywiście nie są jedyne
rozwiązania, jakie można zastosować w projektach aplikacji WPF. Możliwe i często
stosowane jest także rozwiązanie, w którym widokowi towarzyszy tylko klasa okna,
podobnie jak we wzorcu AV, która jest jednak ustawiana jako kontekst danych
w kodzie XAML. Wówczas kod tej klasy może zawierać własności i polecenia, do któ-
rych definiowane są wiązania w kodzie XAML widoku, co umożliwia wykorzystanie
zalet wiązań i luźnego wiązania z kodem C# bez konieczności definiowania nadmier-
nej liczby klas.
52 Część I  Wzorzec MVVM
Rozdział 5.
Konwersja danych
w wiązaniu

Prosta konwersja typów


W wiązaniu pozycji suwaków z własnościami modelu widoku kryje się pewne zagro-
żenie, które w przypadku WPF się nie ujawnia, ale w Windows Phone 8.1 spowodo-
wałoby, że aplikacja nie chciałaby działać. Własności R, G i B modelu widoku są typu
byte, co ogranicza ich wartości do liczb całkowitych z zakresu od 0 do 255. Własności
Value suwaków są jednak typu double. Odczyt składowych koloru z modelu widoku
i przypisywanie ich do pozycji suwaków, co ma miejsce tylko przy uruchamianiu
aplikacji, jest całkowicie bezpieczny. Konwersja z byte do double jest dopuszczana
implicite, typ double ma bowiem zarówno większy zakres, jak i większą precyzję.
W momencie, w którym poruszymy suwakiem, wiązanie danych wymusza jednak kon-
wersję z double (wartość własności Value suwaka) do byte (własności R, G i B modelu
widoku i pośrednio odpowiadające im własności modelu). W aplikacji Windows Phone
8.1 taka operacja skończyłaby się błędem, choć aplikacja w żaden sposób by go nie
zasygnalizowała. Jedynym śladem byłby zapis w oknie Output środowiska Visual Studio
oraz oczywiście brak zmiany koloru prostokąta.

Aby uniknąć tego typu problemów, użyjemy klasy konwertującej między typami byte
i double, czyli konwertera. Konwerter jest klasą implementującą interfejs Ivalue
Converter z przestrzeni nazw System.Windows.Data (w Windows Phone i WinRT z prze-
strzeni Windows.UI.Xaml.Data), który wymusza obecność dwóch metod: Convert i Convert
Back (listing 5.1). Obie metody mają takie same sygnatury. Wartość źródła wiązania
(własności zdefiniowanej w modelu widoku) przesyłana jest do metody Convert w pa-
rametrze value typu object, a skonwertowana wartość powinna być zwrócona przez
wartość metody1. Pożądany typ, na jaki wartość ma być skonwertowana, jest przekazy-
wany w drugim parametrze. My jednak wiemy, w jakim kontekście będzie używany

1
Niestety nie ma parametrycznej wersji interfejsu IValueConverter, a wydawałoby się to bardzo naturalne.
Rozdział 5.  Konwersja danych w wiązaniu 59

public object[] ConvertBack(object value, Type[] targetTypes, object parameter,


System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

Listing 5.6. Zmieniony kod XAML


<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:KoloryWPF"
mc:Ignorable="d"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
Title="Kolory WPF" Height="480" Width="640"
KeyDown="Window_KeyDown" Closed="Window_Closed">
<Window.Resources>
<vm:EdycjaKoloru x:Key="edycjaKoloru" />
<local:ByteToDoubleConverter x:Key="konwersjaByteDouble" />
<local:ColorToSolidColorBrushConverter x:Key="konwersjaColorBrush" />
<local:SkładoweRGBToSolidColorBrushConverter x:Key="konwersjaRGBBrush" />
<local:SkładoweRGBDoubleToSolidColorBrushConverter
x:Key="konwersjaRGBDoubleBrush" />
</Window.Resources>
<Grid DataContext="{StaticResource edycjaKoloru}">
<Rectangle x:Name="rectangle" Margin="10,10,10,91" Stroke="Black">
<Rectangle.Fill>
<MultiBinding Mode="OneWay"
Converter="{StaticResource konwersjaRGBDoubleBrush}">
<Binding ElementName="sliderR" Path="Value" />
<Binding ElementName="sliderG" Path="Value" />
<Binding ElementName="sliderB" Path="Value" />
</MultiBinding>
</Rectangle.Fill>
</Rectangle>
...
</Grid>
</Window>

Zwróćmy uwagę, że gdyby nie chęć przechowywania koloru między uruchomieniami


aplikacji, a więc gdyby jedynym celem aplikacji było ustawianie koloru prostokąta za
pomocą trzech suwaków, konwerter byłby jedynym kodem, jaki byłby w tym projek-
cie potrzebny. Model i model widoku stają się wówczas zbędne. Warto też podkreślić,
że konwertery, pomimo tego, ią należą do warstwy widoku, mogą być z łatwością te-
stowane — przygotowywanie dla nich testów jednostkowych jest naturalne, bo w ich
kodzie nie ma bezpośrednich odwołań do własności kontrolek; inaczej niż dla metod
zdarzeniowych, które bezpośrednio angażują kontrolki interfejsu.
60 Część I  Wzorzec MVVM

Konwersje „wbudowane”
Nie wszystkie konwersje używane w XAML wymagają przygotowywania konwerterów.
Istnieją gotowe konwertery, których możemy użyć. Jednym z najpopularniejszych jest
BooleanToVisibilityConverter, z którym będziemy mieli jeszcze do czynienia. Oprócz
tego jest jeszcze kilka rzadziej używanych: AlternationConverter, BorderGapMask
Converter, DataGridLengthConverter, MenuScrollingVisibilityConverter, Zoom
PercentageConverter, JournalEntryListConverter, ProgressBarBrushConverter,
ProgressBarHighlightConverter i JournalEntryUnifiedViewConverter.

Poza typowymi konwerterami warto jeszcze w tym kontekście zwrócić uwagę na


atrybut StringFormat wiązania. Pozwala ona na konwersję dat, walut i innych formatów
na łańcuchy wyświetlane w widoku. Tym zagadnieniem też zajmiemy się w dalszych
rozdziałach, a na razie osoby zainteresowane odsyłam do strony http://blogs.msdn.com/b/
matthiasshapiro/archive/2012/12/11/complete-guide-to-windows-phone-stringformat-
-binding.aspx.

Zadania
1. Zaimplementuj i przetestuj wszystkie niezaimplementowane metody
ConvertBack z tego rozdziału.
2. Przygotuj aplikację WPF bez modelu i modelu widoku, w której na oknie należy
umieścić suwak (Slider) i pasek postępu (ProgressBar). Własność Maximum
obu kontrolek ustaw na 100. Za pomocą wiązania między kontrolkami uzgodnij
pozycję paska postępu z pozycją suwaka.
3. Z pozycją suwaka z poprzedniego zadania zwiąż także kolor paska postępu.
Należy wykorzystać do tego konwerter ustalający dla minimalnej pozycji kolor
zielony, dla pośredniej żółty, a dla maksymalnej czerwony.
4. Do aplikacji z poprzednich punktów dodaj model i model widoku, a następnie
przenieś wiązania do własności modelu widoku. Dodaj możliwość przechowania
stanu aplikacji (jedna wartość typu double).
Rozdział 6.
Polecenia (commands)

Interfejs ICommand
W klasie MainWindow tworzącej tak zwany code-behind w projekcie KoloryWPF pozo-
stały już tylko dwie metody: pierwsza obsługuje naciśnięcie klawisza Escape, zamy-
kając okno, druga zaś wymusza zapisanie składowych koloru w ustawieniach aplikacji
w momencie zamykania okna (zdarzenie Window.Closed). W tym rozdziale postaramy
się pozbyć ich obu. Zacznijmy od drugiej. W tym celu zdefiniujemy w modelu wido-
ku tak zwane polecenie (ang. command), które umożliwi nam wymuszenie zapisania
stanu aplikacji. Polecenie to klasa implementująca interfejs ICommand. Interfejs ten
wymusza obecność dwóch metod oraz zdarzenia. Metody to Execute, wykonująca za-
sadnicze działanie polecenia, i CanExecute, sprawdzająca, czy wykonanie polecenia jest
możliwe. Natomiast zdarzenie CanExecuteChanged powiadamia o zmianie możliwości
wykonania polecenia.

Chyba najprostsza klasa polecenia widoczna jest na listingu 6.1. Zapiszmy ją w folderze
ModelWidoku, w nowym pliku o nazwie Polecenia.cs. Nietrywialnie zdefiniowana
jest w niej tylko metoda Execute, która przywraca początkowy stan aplikacji, przesu-
wając suwaki na zerowe pozycje. Do metody tej przekazywany jest parametr, co do
którego zakładamy, że zawiera referencję do instancji modelu widoku. Metoda Can
Execute zawsze zwraca wartość true, a zdarzenie nie jest używane.

Listing 6.1. Klasa implementująca interfejs ICommand


using System;
using System.Windows.Input;

namespace KoloryWPF.ModelWidoku
{
public class ResetujCommand : ICommand
{
public event EventHandler CanExecuteChanged;

public bool CanExecute(object parameter)


62 Część I  Wzorzec MVVM

{
return true;
}

public void Execute(object parameter)


{
EdycjaKoloru modelWidoku = parameter as EdycjaKoloru;
if (modelWidoku != null)
{
modelWidoku.R = 0;
modelWidoku.G = 0;
modelWidoku.B = 0;
}
}
}
}

Aby móc użyć polecenia w kodzie XAML widoku, należy utworzyć jego instancję
w modelu widoku i ją udostępnić. Najlepiej zrobić to za pomocą publicznej własności
tylko do odczytu typu ICommand. Aby udostępnić polecenie ResetujCommand w klasie
EdycjaKoloru, należy do niej dodać kod widoczny na listingu 6.2. W kodzie widoczne
jest zabezpieczenie przed powielaniem egzemplarzy klasy polecenia. Interfejs ICommand
wymaga dodania przestrzeni nazw System.Windows.Input.

Listing 6.2. Własność zdefiniowana w klasie EdycjaKoloru udostępniająca polecenie


private ICommand resetujCommand;

public ICommand Resetuj


{
get
{
if (resetujCommand == null) resetujCommand = new ResetujCommand();
return resetujCommand;
}
}

Przycisk uruchamiający polecenie


Do okna dodajmy przycisk. Przesuńmy suwaki nieco w górę i umieśćmy przycisk pod
nimi (rysunek 6.1). Aby związać polecenie z kliknięciem przycisku w kodzie XAML,
należy użyć atrybutu Command elementu Button. Dodatkowo, jako parametr, powinniśmy
przesłać instancję klasy EdycjaKoloru, czyli model widoku. Jeżeli jest ona zdefiniowana
w zasobach aplikacji, możemy użyć kodu widocznego na listingu 6.3.
64 Część I  Wzorzec MVVM

public ResetujCommand(EdycjaKoloru modelWidoku)


{
if (modelWidoku == null) throw new ArgumentNullException("modelWidoku");
this.modelWidoku = modelWidoku;
}

public event EventHandler CanExecuteChanged;

public bool CanExecute(object parameter)


{
return true;
}

public void Execute(object parameter)


{
EdycjaKoloru modelWidoku = parameter as EdycjaKoloru;
if (modelWidoku != null)
{
modelWidoku.R = 0;
modelWidoku.G = 0;
modelWidoku.B = 0;
}
}
}

W konsekwencji musimy zmienić kod własności Resetuj w klasie EdycjaKoloru (li-


sting 6.5).

Listing 6.5. Kod własności należy uzupełnić o przesyłanie referencji do instancji modelu widoku
private ICommand resetujCommand;

public ICommand Resetuj


{
get
{
if (resetujCommand == null) resetujCommand = new ResetujCommand(this);
return resetujCommand;
}
}

Natomiast kod XAML można uprościć. Przesyłanie parametru do polecenia nie jest
już bowiem potrzebne — i tak parametr zostałby zignorowany:
<Button Content="Resetuj" Height="25" Width="75"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="10,0,0,10"
Command="{Binding Resetuj}"
CommandParameter="{Binding Path=DataContext,
RelativeSource={RelativeSource Self}}" />
Rozdział 6.  Polecenia (commands) 65

Sprawdzanie możliwości
wykonania polecenia
Pójdźmy o krok dalej i wykorzystajmy metodę CanExecute polecenia, aby sprawdzić,
czy wykonanie metody Execute jest możliwe i potrzebne. W naszym przypadku wa-
runkiem niech będzie to, że suwaki są w innym położeniu niż wyjściowe, co przekłada
się na warunek, iż składowe koloru przechowywane w modelu widoku nie są wszyst-
kie równe zeru. Zmieńmy wobec tego metodę CanExecute w taki sposób, aby sprawdzała
ten warunek (listing 6.6). Dodatkowo zmodyfikujmy zdarzenie CanExecuteChanged
polecenia tak, aby przy dodaniu kolejnej metody do zbioru metod subskrybujących to
zdarzenie metoda ta była zapisywana także w menedżerze poleceń zaimplementowa-
nym w klasie System.Windows.Input.CommandManager, który za nas zadba o zgłaszanie
tego zdarzenia w razie zmiany warunku zaimplementowanego w metodzie CanExecute.
Przejmie tym samym odpowiedzialność za powiadamianie wykorzystujących to pole-
cenie kontrolek. Rozbudowana definicja zdarzenia widoczna jest na listingu 6.6. Efekt
tych zmian będzie bardzo ciekawy, choć nie rzucający się w pierwszej chwili w oczy.
Przycisk stanie się nieaktywny, jeżeli suwaki będą w zerowej pozycji. Stanie się znów
aktywny, gdy którykolwiek z nich przesuniemy.

Listing 6.6. Polecenie z nietrywialną metodą CanExecute


public class ResetujCommand : ICommand
{
private readonly EdycjaKoloru modelWidoku;

public ResetujCommand(EdycjaKoloru modelWidoku)


{
this.modelWidoku = modelWidoku;
}

public event EventHandler CanExecuteChanged


{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}

public bool CanExecute(object parameter)


{
return (modelWidoku.R != 0) || (modelWidoku.G != 0) || (modelWidoku.B != 0);
}

public void Execute(object parameter)


{
if (modelWidoku != null)
{
66 Część I  Wzorzec MVVM

modelWidoku.R = 0;
modelWidoku.G = 0;
modelWidoku.B = 0;
}
}
}

Resetowanie stanu suwaków


po naciśnięciu klawisza
Nie tylko przyciski mogą korzystać z poleceń. Polecenie można uruchomić także na
przykład po naciśnięciu jakiegoś klawisza lub kliknięciem myszą. Można to zrobić na
poziomie każdej kontrolki, siatki lub całego okna. To ostatnie rozwiązanie w przy-
padku klawiszy wydaje się najlepsze — naciśnięcie klawisza będzie wówczas wy-
krywane bez względu na to, czy któraś z kontrolek jest aktywna (brzydko mówiąc:
ma „focus”). To jednak oznacza, że element XAML opisujący okno musi mieć przy-
pisany kontekst wiązania (por. listing 4.2 z rozdziału 4.); zmiany wymaga też metoda
Window_Closed z code-behind. Kod pokazujący wiązanie naciśnięcia klawisza R z po-
leceniem Resetuj modelu widoku widoczny jest na listingu 6.7.

Listing 6.7. Wiązanie naciśnięcia klawisza R z poleceniem Resetuj udostępnianym przez model widoku
<Window ... >
<Window.DataContext>
<vm:EdycjaKoloru />
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Key="R" Command="{Binding Resetuj}" />
</Window.InputBindings>

Jeżeli zamiast prostego naciśnięcia klawisza R chcemy, aby resetowanie aplikacji na-
stępowało po naciśnięciu kombinacji Ctrl+R, należy dodać atrybut Modifiers:
<KeyBinding Key="R" Modifiers="Control" Command="{Binding Resetuj}" />

W podobny sposób możemy związać polecenie z czynnościami wykonywanymi myszą.


Wystarczy, że do elementu Window.InputBindings dodamy element:
<MouseBinding Gesture="MiddleClick" Command="{Binding Resetuj}" />

Spowoduje on, że polecenie będzie wykonywane, gdy kliknięty zostanie środkowy


klawisz myszy. Jeżeli chcemy, żeby dodatkowym warunkiem było jednoczesne przy-
trzymywanie klawisza Alt, należy element MouseBinding następująco zmodyfikować:
<MouseBinding Gesture="Alt+MiddleClick" Command="{Binding Resetuj}" />
Rozdział 6.  Polecenia (commands) 67

Klasa RelayCommand
Przedstawiona wyżej klasa polecenia może być uogólniona tak, żeby mogła przecho-
wywać dowolną czynność i dowolny warunek weryfikujący potrzebę lub możliwość
wykonania tej czynności. Zamiast korzystać z metod zdefiniowanych w klasie polece-
nia, wystarczy przecież, aby klasa ta przechowała referencje do tych dwóch metod lub
wyrażeń lambda. Referencje te będą do niej przekazane jako argumenty konstruktora.
Wówczas metody te mogą odwoływać się do pól modelu widoku bez przekazywania
jego referencji do obiektu polecenia. Często używaną implementacją takiej ogólnej
klasy polecenia jest klasa RelayCommand (co można chyba trafnie, ale brzydko prze-
tłumaczyć jako „polecenie przekaźnikowe”), opisana na przykład w dostępnym on-line
artykule Josha Smitha pod tytułem WPF Apps With The Model-View-ViewModel De-
sign Pattern z „MSDN Magazine” (http://msdn.microsoft.com/en-us/magazine/dd419663.
aspx#id0090030) (por. też klasę MvvmCommand z książki Budowanie aplikacji bizneso-
wych za pomocą Windows Presentation Foundation i wzorca Model View ViewModel,
której autorem jest Raffaele Garofalo, i DelegateCommand z bibliotek Prism, zob.
https://msdn.microsoft.com/en-us/library/ff648465.aspx). Klasa RelayCommand widoczna
jest na listingu 6.8. Jedyna modyfikacja względem oryginału polega na usunięciu jedno-
argumentowego konstruktora i zastąpieniu go wartością domyślną w pozostawionym
konstruktorze.

Listing 6.8. Uogólniona klasa polecenia


using System;
using System.Diagnostics;
using System.Windows.Input;

public class RelayCommand : ICommand


{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields

#region Constructor
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
if (execute == null) throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructor

#region ICommand Members


[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}

public event EventHandler CanExecuteChanged


{
add
68 Część I  Wzorzec MVVM

{
if (_canExecute != null) CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null) CommandManager.RequerySuggested -= value;
}
}

public void Execute(object parameter)


{
_execute(parameter);
}
#endregion // ICommand Members
}

Przyjrzyjmy się konstruktorowi klasy RelayCommand. Jeżeli jego argument execute


równy jest null, zgłaszamy wyjątek, wskazując nazwę argumentu, który ma niepo-
prawną wartość. Robimy to, podając po prostu jego nazwę jako łańcuch. Jeśli zmienimy
nazwę argumentu, na przykład korzystając z narzędzi refactoringu, łańcuch ten
przestanie być właściwy. W C# 6.0 i VS2015 zamiast łańcucha możemy użyć opera-
tora nameof:
if (execute == null) throw new ArgumentNullException(nameof(execute));

Użycie tej klasy w modelu widoku (listing 6.9) przenosi miejsce, w którym zdefinio-
wany jest kod polecenia i towarzyszący mu warunek jego wykonania z osobnej klasy
polecenia do klasy modelu widoku. To bardzo wygodne rozwiązanie, szczególnie że
z punktu widzenia widoku i kodu XAML użycie ogólnej wersji klasy polecenia niczego
nie zmienia.

Listing 6.9. Prywatne pole i publiczna własność udostępniająca polecenie w klasie modelu widoku
private ICommand resetujCommand;

public ICommand Resetuj


{
get
{
if (resetujCommand == null)
{
resetujCommand = new RelayCommand(
argument =>
{
R = 0;
G = 0;
B = 0;
},
argument => (R != 0) || (G != 0) || (B != 0)
);
}
return resetujCommand;
}
}
Rozdział 6.  Polecenia (commands) 69

Należy wspomnieć o predefiniowanych poleceniach, które są gotowe do użycia


w czterech statycznych klasach: ApplicationCommands (polecenia związane z druko-
waniem, wyszukiwaniem itp.), NavigationCommands (nawigacja między oknami), Media
Commands (obsługa dźwięków i filmów) i EditingCommands (edycja). Nie będziemy
z nich korzystać w tej książce, ale zdecydowanie warto się nimi zainteresować.
Więcej informacji znajduje się na stronie https://msdn.microsoft.com/pl-pl/library/
ms752308(v=vs.110).aspx.

Zdarzenia a polecenia
Przycisk, jak również elementy menu, czy pole opcji (checkbox) ma możliwość wią-
zania z poleceniem. Każda kontrolka ma też możliwość związania poleceń z klawi-
szami lub ruchami wykonywanymi myszą. Służy do tego omówiony wyżej podelement
InputBindings, który może występować w każdej kontrolce — nie tylko w oknie.
Mimo to sytuacji, w których mamy naturalną możliwość wykonania polecenia, jest
nadal znacznie mniej niż sytuacji, w których zgłaszane są zdarzenia kontrolek. Bardzo
łatwo jest dla przykładu związać metodę zdarzeniową ze zdarzeniem zamknięcia
ekranu. Jak jednak zrobić to, używając polecenia? To pozwoliłoby nam pozbyć się
metody zdarzeniowej w code-behind i tym samym pozostać w zgodzie ze wzorcem
MVVM. Okazuje się, że jest pewien trik, który to umożliwia, a który został przygo-
towany na potrzeby współpracy Visual Studio z Expression Blend. Można go użyć,
aby zdarzenie „przekształcić” w polecenie. Kluczowa jest tu klasa EventTrigger,
zwykle używana podczas definiowania stylów (zob. rozdział 11.), która wykonuje
wskazane polecenie w momencie wystąpienia wybranego zdarzenia.
1. Zacznijmy od wprowadzenia zmian w modelu widoku. Zastąpmy metodę
Zapisz poleceniem o tej samej nazwie (listing 6.10).

Listing 6.10. Zmiany w klasie EdycjaKoloru


public void Zapisz()
{
Ustawienia.Zapisz(kolor);
}

private ICommand zapiszCommand;

public ICommand Zapisz


{
get
{
if (zapiszCommand == null)
zapiszCommand = new RelayCommand(argument => {
Ustawienia.Zapisz(kolor); });
return zapiszCommand;
}
}
70 Część I  Wzorzec MVVM

2. Kolejnym krokiem będzie dodanie do projektu dwóch bibliotek: System.Windows.


Interactivity.dll i Microsoft.Expression.Interaction.dll (obie w najwyższych
wersjach). W tym celu z menu Project wybieramy Add Reference... Wówczas
pojawi się okno Reference Manager, z którego lewej strony wybieramy
Assemblies, Framework i w okienku edycyjnym przeszukiwania (prawy górny
róg okna) wpisujemy Interact.
3. Gdy pojawią się wyniki wyszukiwania, zaznaczamy obie biblioteki (należy
zaznaczyć każdą z nich osobno, uważając na wersje) i klikamy przycisk OK.
4. Z pliku MainWindow.xaml.cs usuwamy metodę zdarzeniową Window_Closed.
W tej chwili została w nim już tylko metoda zdarzeniowa Windows_KeyDown,
zamykająca okno po naciśnięciu klawisza Esc.
5. Następnie przechodzimy do edycji kodu XAML w pliku MainWindows.xaml.
Zakładam, że instancja modelu widoku jest kontekstem wiązania okna, a więc
w kodzie XAML jest obecne przypisanie podobne do tego:
<Window.DataContext>
<vm:EdycjaKoloru />
</Window.DataContext>

6. Do znacznika Window dodajemy przestrzeń nazw http://schemas.microsoft.com/


expression/2010/interactivity, której nadajemy alias i, co oznacza, że umieszczamy
w nim atrybut:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

7. Z tego samego znacznika należy usunąć atrybut Closed, który wskazywał


na metodę zdarzeniową usuniętą przed chwilą z code-behind.
8. Następnie do elementu Window wstawiamy element Interaction.Triggers,
gdzie Interaction to przestrzeń nazw, a Triggers to kolekcja, do której
dodajemy instancję klasy EventTrigger. Atrybut EventName tego elementu
wskazuje nazwę zdarzenia macierzystego elementu (czyli Window). W naszym
przypadku będzie to Closed. Zawartością elementu EventTrigger powinien być
natomiast element InvokeCommandAction, który wskazuje polecenie wykonywane
w razie wystąpienia zdarzenia. Cały kod XAML, wraz z opisanymi wyżej
zmianami, widoczny jest na listingu 6.11.

Listing 6.11. Zamknięcie okna spowoduje wykonanie polecenia Zapisz zdefiniowanego w modelu widoku
<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:KoloryWPF"
mc:Ignorable="d"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="Kolory WPF" Height="480" Width="640"
KeyDown="Window_KeyDown" Closed="Window_Closed"[BP1]>
<Window.Resources>
...
Rozdział 6.  Polecenia (commands) 71

<vm:SkładoweRGBDoubleToSolidColorBrushConverter
x:Key="konwersjaRGBDoubleBrush" />
</Window.Resources>
<Window.DataContext>
<vm:EdycjaKoloru />
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Key="R" Modifiers="Control" Command="{Binding Resetuj}" />
<MouseBinding Gesture="MiddleClick" Command="{Binding Resetuj}" />
</Window.InputBindings>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closed">
<i:InvokeCommandAction Command="{Binding Zapisz}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<Rectangle x:Name="rectangle" Margin="10,10,10,109" Stroke="Black">
<Rectangle.Fill>
<MultiBinding Mode="TwoWay" Converter="{StaticResource
konwersjaRGBDoubleBrush}">
<Binding ElementName="sliderR" Path="Value" />
<Binding ElementName="sliderG" Path="Value" />
<Binding ElementName="sliderB" Path="Value" />
</MultiBinding>
</Rectangle.Fill>
</Rectangle>
...
<Button Content="Resetuj" Height="25" Width="75"
VerticalAlignment="Bottom" HorizontalAlignment="Left"
Margin="10,0,0,10"
Command="{Binding Resetuj}" />
</Grid>
</Window>

9. Warto oczywiście uruchomić aplikację i sprawdzić, czy zapisywanie


zrealizowane w nowy sposób działa prawidłowo.

Zamykanie okna
W code-behind została już tylko jedna metoda, która związana jest ze zdarzeniem
KeyDown okna. Możemy się jej pozbyć, definiując w modelu widoku następujące polecenie:
public ICommand ZamknijOkno
{
get
{
return new RelayCommand(argument => { App.Current.MainWindow.Close(); });
}
}

i podłączając je do naciśniętego klawisza Esc w sposób, którego już używaliśmy:


<Window.InputBindings>
<KeyBinding Key="R" Modifiers="Control" Command="{Binding Resetuj}" />
72 Część I  Wzorzec MVVM

<MouseBinding Gesture="MiddleClick" Command="{Binding Resetuj}" />


<KeyBinding Key="Escape" Command="{Binding ZamknijOkno}" />
</Window.InputBindings>

Nie jest to jednak dobre rozwiązanie. Model widoku absolutnie nie powinien znać
szczegółów widoku. Zwróćmy uwagę, że konsekwencją złamania tej zasady będą
trudności, z jakimi przyjdzie nam się zmierzyć, jeżeli zechcemy przygotować testy
jednostkowe dla polecenia ZamknijOkno. Dlatego na pewno nie jest to rozwiązanie do-
celowe. Tak czy inaczej, dzięki temu poleceniu możemy usunąć atrybut KeyDown ze
znacznika Window i metodę zdarzeniową Window_KeyDown z pliku MainWindow.xaml.cs.
Po uruchomieniu aplikacji przekonamy się, że to rozwiązanie działa, pomimo brzydkiego
zapachu, jaki wokół siebie roztacza.

Powyższe polecenie i korzystające z niego wiązanie można nieco poprawić, przesy-


łając referencję okna przez parametr:
public ICommand ZamknijOkno
{
get
{
return new RelayCommand(
argument => { (argument as System.Windows.Window).Close(); });
}
}

W VS2015 warto użyć nowego operatora dostępu ?. sprawdzającego, czy obiekt, na


którego rzecz wywoływana jest metoda Close, nie jest równy null. W tym celu należy
następująco zmodyfikować wyrażenie lambda:
argument => { (argument as System.Windows.Window)?.Close(); }

Wówczas wiązanie powinno wyglądać następująco:


<KeyBinding Key="Escape" Command="{Binding ZamknijOkno}" CommandParameter="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

To jednak nie zmienia zasadniczego mankamentu tego rozwiązania. Już lepsze jest
moim zdaniem pozostawienie kodu odpowiedzialnego za zamknięcie okna w meto-
dzie zdarzeniowej z code-behind. Innym, bardziej eleganckim obejściem problemu
jest kolejny trik, który nazywa się zachowaniem dołączonym do kontrolki okna.
Omówię go w następnym rozdziale.

Zadanie
Napisz samodzielnie komendę kopiującą do schowka kod koloru (np. #FFFF0000 dla
czerwieni). Użyj do tego metody System.Windows.Clipboard.SetText. Stwórz korzy-
stający z tego polecenia przycisk. Zwiąż je także z naciśnięciem kombinacji klawiszy
Ctrl+C.
Rozdział 7.
Zachowania,
własności zależności
i własności doczepione

Zachowania (behaviors)
Zachowania, podobnie jak mechanizm przekształcania zdarzeń w polecenia, zostały
wprowadzone do XAML na potrzeby współpracy Visual Studio z Expression Blend.
Dzięki nim projektant korzystający z Blend może dodawać do kontrolek dodatkowe
„umiejętności” bez potrzeby pisania kodu — wystarczy, że będzie dysponował wcze-
śniej uzgodnionymi z programistami zachowaniami (ang. behaviors) rozszerzającymi
możliwości kontrolek. Aby móc definiować zachowania, musimy do projektu dodać
biblioteki System.Windows.Interactivity.dll i Microsoft.Expression.Interaction.dll. To
są te same biblioteki, których wymaga mechanizm przekształcania zdarzeń w polecenia
omówiony w poprzednim rozdziale. Z punktu widzenia programisty zachowania to nic
innego jak klasa, która rozszerza wskazany typ kontrolki o pewne nowe możliwości.
W poniższych przykładach rozszerzać będziemy klasę okna Window, ale mechanizm ten
działa dla wszystkich kontrolek (por. zadanie 2.).

Bardzo prosty przykład, pokazujący, w jaki sposób można zdefiniować zachowanie


dotyczące okna, widoczny jest na listingu 7.1. Zachowanie to pozwala wskazać klawisz,
którego naciśnięcie spowoduje zamknięcie okna. Będzie do tego służyła publiczna
własność Klawisz typu Key. Oprócz tego w klasie zachowania zdefiniowana jest metoda
zdarzeniowa Window_PreviewKeyDown, która zakłada, że nadawcą zdarzenia (parametr
sender) jest obiekt reprezentujący okno. Dzięki temu może wykorzystać przesłaną przez
ten parametr referencję obiektu okna, aby je zamknąć. Metoda ta jest wiązana ze zdarze-
niem PreviewKeyDown okna w metodzie OnAttached uruchamianej w momencie podłącza-
nia zachowania do rozszerzanego obiektu. Ten sposób definiowania zachowań nadaje
się do przypadków, w których zachowanie dotyczy wyłącznie obiektu, do którego
74 Część I  Wzorzec MVVM

zostanie dodane — w naszym przypadku okna. Referencja do tego obiektu, tak zwanego
obiektu powiązanego, dostępna jest poprzez własność AssociatedObject zachowania,
a jej typ określa parametr klasy bazowej Behavior.

Listing 7.1. Definiowanie prostego zachowania


using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace KoloryWPF
{
public class ZamknięcieOknaPoNaciśnięciuKlawisza : Behavior<Window>
{
public Key Klawisz { get; set; }

protected override void OnAttached()


{
Window window = this.AssociatedObject;
if (window != null) window.PreviewKeyDown += Window_PreviewKeyDown;
}

private void Window_PreviewKeyDown(object sender, KeyEventArgs e)


{
Window window = (Window)sender;
if (e.Key == Klawisz) window.Close();
}
}
}

Aby użyć tego zachowania, przejdźmy do kodu XAML widoku. Należy w nim zade-
klarować przestrzeń nazw http://schemas.microsoft.com/expression/2010/interactivity,
w której znajdują się klasy potrzebne do dodania zachowania. W naszym projekcie prze-
strzeń ta jest już obecna pod aliasem i ze względu na użyty wcześniej mechanizm
przekształcania zdarzeń na polecenia. Pozostaje wobec tego stworzyć kolekcję zachowań
i:Interaction.Behaviors i dodać do niej zachowanie ZamknięcieOknaPoNaciśnieciu
Klawisza, określając jednocześnie, jakiego klawisza chcemy używać do zamykania
aplikacji (listing 7.2).

Listing 7.2. Dodawanie zachowania do kodu XAML


<Window x:Class="KoloryWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:KoloryWPF"
mc:Ignorable="d"
xmlns:vm="clr-namespace:KoloryWPF.ModelWidoku"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="Kolory WPF" Height="480" Width="640">
...
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closed">
<i:InvokeCommandAction Command="{Binding Zapisz}" />
Rozdział 7.  Zachowania, własności zależności i własności doczepione 75

</i:EventTrigger>
</i:Interaction.Triggers>
<i:Interaction.Behaviors>
<local:ZamknięcieOknaPoNaciśnieciuKlawisza Klawisz="Escape" />
</i:Interaction.Behaviors>
...

Oczywiście aby użycie powyższego zachowania rozszerzającego klasę okna miało


sens, należy najpierw odłączyć polecenie Zamknij związane z klawiszem Esc poprzez
element KeyBinding w kolekcji Window.InputBindings (zob. ostatni podrozdział po-
przedniego rozdziału). Definiując to polecenie, uprzedzałem, że nie jest ono dobrym
rozwiązaniem — powyższe zachowanie jest znacznie lepszym. W konsekwencji można
usunąć polecenie Zamknij z modelu widoku.

Własność zależności
(dependency property)
Wydaje mi się, że powyższy przykład jest dość łatwy do zrozumienia. Drugi będzie
bardziej skomplikowany, bo wykorzystamy w nim dodatkowo nowy mechanizm za-
projektowany dla WPF, mianowicie własność zależności (dependency property). Wła-
sności tego typu są powszechnie stosowane w WPF, w szczególności w klasach kon-
trolek dostępnych w XAML jako elementy. Atrybuty kontrolek są właśnie tego rodzaju
własnościami. Używa się ich tak samo jak zwykłych własności zdefiniowanych w kla-
sach kontrolek. Różnią się jednak sposobem przechowywania wartości. Podczas gdy
zwykłe własności z reguły przechowują swoją wartość w prywatnych polach, własności
zależności robią to w specjalnym słowniku zdefiniowanym w klasie DependencyObject.
Właśnie po tej klasie dziedziczy klasa Behavior — klasa bazowa definiowanych przez
nas zachowań. To jednak nie jest cała prawda, bo w rzeczywistości w tym słowniku
przechowywane są tylko te wartości własności zależności, których wartość została
zmieniona. W ten sposób zmniejsza się ilość miejsca używanego przez własności
kontrolek, co ma duże znaczenie w aplikacjach WPF, zważywszy na to, że zwykle
używamy tylko niewielkiej części spośród wszystkich własności kontrolek (czyli atry-
butów elementów dodanych do kodu XAML) — większość pozostaje przy swoich
domyślnych wartościach. Ten mechanizm pozwala również na „dziedziczenie” wartości
własności. Kontrolki WPF (elementy w kodzie XAML) „dziedziczą” wartości wła-
sności po elementach, w których zostały umieszczone, czyli po swoich pojemnikach.
Dotyczy to na przykład ich formatowania (kolor tła, cechy czcionki itp.), ale również
wszystkich innych, choćby kontekstu wiązania danych. Mechanizm własności zależ-
ności „widzi” relacje zawierania elementów XAML. Jeżeli w danym elemencie został
użyty atrybut, to wykorzystana będzie oczywiście wskazana w nim wartość. Jeśli jed-
nak w elemencie nie ma przypisania wartości atrybutu, mechanizm własności zależ-
ności potrafi odnaleźć ją w nadrzędnych elementach XAML, a jeżeli takiej nie znaj-
dzie — skorzystać z wartości domyślnej. Ponadto własności zależności używają
mechanizmu powiadamiania o zmianach wartości, co jest kluczowe w kontekście wią-
zania danych.
76 Część I  Wzorzec MVVM

Sprawdźmy, jak wygląda definiowanie tego typu własności na przykładzie nowego


zachowania (listing 7.3). Zachowanie to zakłada, że w oknie znajduje się przycisk1.
Należy go wskazać we własności Przycisk tego zachowania. Ta własność będzie właśnie
własnością zależności. Wartość domyślna tej własności będzie równa null, co ozna-
cza, że zachowanie będzie w istocie nieaktywne. Jeżeli jednak przypiszemy własności
Przycisk jakąś wartość, a konkretnie referencję istniejącego przycisku, to wykonana
zostanie metoda PrzyciskZmieniony, która zwiąże ze zdarzeniem Click tego przycisku
metodę zamykającą okno, do którego dodamy projektowane zachowanie. Jednocze-
śnie usuwane jest wiązanie zdarzenia Click z przycisku będącego poprzednią wartością
własności Przycisk. To może się zdarzyć, gdy własność tę zmienimy na przykład
w code-behind.

Listing 7.3. Definiowanie zachowania opartego na własności zależności


public class PrzyciskZamykającyOkno : Behavior<Window>
{
public static readonly DependencyProperty PrzyciskProperty =
DependencyProperty.Register(
"Przycisk",
typeof(Button),
typeof(PrzyciskZamykającyOkno),
new PropertyMetadata(null, PrzyciskZmieniony)
);

public Button Przycisk


{
get { return (Button)GetValue(PrzyciskProperty); }
set { SetValue(PrzyciskProperty, value); }
}

private static void PrzyciskZmieniony(DependencyObject d,


DependencyPropertyChangedEventArgs e)
{
Window window = (d as PrzyciskZamykającyOkno).AssociatedObject;
RoutedEventHandler button_Click =
(object sender, RoutedEventArgs _e) => { window.Close(); };
if (e.OldValue != null) ((Button)e.OldValue).Click -= button_Click;
if (e.NewValue != null) ((Button)e.NewValue).Click += button_Click;
}
}

Na listingu 7.3 widoczna jest „zwykła” własność Przycisk, która będzie widoczna ja-
ko atrybut zachowania. Jej wartość jest odczytywana i zmieniana za pomocą metod
SetValue i GetValue zdefiniowanych w klasie DependencyObject. Tej samej nazwy,
„Przycisk”, używamy, rejestrując własność zależności metodą DependencyProperty.
Register. Wartość, jaką w ten sposób uzyskamy, zapisujemy w statycznym polu
PrzyciskProperty. To pole musi być statyczne, bo odnosi się do statycznych elementów
klasy DependencyObject, między innymi do zdefiniowanego w nim słownika przecho-
wującego wartość własności zależności. Argumentami metody Register są: nazwa

1
To zachowanie jest wzorowane na przykładzie, który znalazłem na stronie
http://stackoverflow.com/questions/4376475/wpf-mvvm-how-to-close-a-window.
Rozdział 7.  Zachowania, własności zależności i własności doczepione 77

własności, jej typ, typ właściciela (w naszym przypadku zachowania) oraz dodatkowe
dane — obiekt typu PropertyMetadata. Ten ostatni daje nam możliwość określenia
wartości domyślnej własności (w naszym przypadku jest ona równa null) oraz metody,
która będzie wykonywana, gdy wartość własności zostanie zmieniona. My użyliśmy
metody PrzyciskZmieniony, której działanie opisałem wyżej.

Wróćmy do kodu XAML. Aby użyć nowego zachowania, należy do siatki, w której
umieszczone są wszystkie kontrolki, dodać jeszcze jeden przycisk:
<Button x:Name="przyciskZamknij" Content="Zamknij" Height="25" Width="75"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="100,0,0,10"
/>

Ważne jest, żeby nowy przycisk został nazwany — ja użyłem nazwy przyciskZamknij.
Następnie do zbioru zachowań, który mamy już zdefiniowany, dodajemy nowe zacho-
wanie typu PrzyciskZamykającyOkno:
<i:Interaction.Behaviors>
<local:ZamknięcieOknaPoNaciśnięciuKlawisza Klawisz="Escape" />
<local:PrzyciskZamykającyOkno x:Name="przyciskZamykającyOkno"
Przycisk="{Binding ElementName=przyciskZamknij}" />
</i:Interaction.Behaviors>

W zdarzeniu tym wiążemy z atrybutem Przycisk dodany wcześniej przycisk o nazwie


przyciskZamknij. To powoduje, że wykonana zostanie metoda PrzyciskZamykającyOkno.
PrzyciskZmieniony, która ze zdarzeniem Click przycisku wiąże metodę zamykającą
bieżące okno. Możemy się o tym łatwo przekonać, uruchamiając aplikację i klikając ten
przycisk.

Idąc za ciosem, zdefiniujmy w zachowaniu PrzyciskZamykającyOkno jeszcze dwie


własności: Polecenie i ParametrPolecenia. Pierwsza umożliwi ustalenie polecenia
wykonywanego przed zamknięciem okna (ale tylko w przypadku, gdy okno zamykamy
za pomocą przycisku) oraz argumentu przesyłanego do tego polecenia. Klasa zacho-
wania z dodanymi elementami widoczna jest na listingu 7.4. Teraz oprócz własności
Przycisk w kodzie XAML możemy przypisać także atrybuty Polecenie i Parametr
Polecenia. Do przetestowania nowych możliwości możemy użyć polecenia Resetuj
zdefiniowanego w poprzednim rozdziale, co powinno spowodować, że suwaki będą
ustawione na zerach, jeżeli otworzymy aplikację ponownie po tym, jak zamknęliśmy
ją, używając przycisku.

Listing 7.4. Dodatkowe własności zależności zdefiniowane w zachowaniu


public class PrzyciskZamykającyOkno : Behavior<Window>
{
public static readonly DependencyProperty PrzyciskProperty =
DependencyProperty.Register(
"Przycisk",
typeof(Button),
typeof(PrzyciskZamykającyOkno),
new PropertyMetadata(null, PrzyciskZmieniony)
);

public Button Przycisk


78 Część I  Wzorzec MVVM

{
get { return (Button)GetValue(PrzyciskProperty); }
set { SetValue(PrzyciskProperty, value); }
}

public static readonly DependencyProperty PolecenieProperty =


DependencyProperty.Register(
"Polecenie",
typeof(ICommand),
typeof(PrzyciskZamykającyOkno));

public ICommand Polecenie


{
get { return (ICommand)GetValue(PolecenieProperty); }
set { SetValue(PolecenieProperty, value); }
}

public static readonly DependencyProperty ParametrPoleceniaProperty =


DependencyProperty.Register(
"ParametrPolecenia",
typeof(object),
typeof(PrzyciskZamykającyOkno));

public object ParametrPolecenia


{
get { return GetValue(ParametrPoleceniaProperty); }
set { SetValue(ParametrPoleceniaProperty, value); }
}

private static void PrzyciskZmieniony(DependencyObject d,


DependencyPropertyChangedEventArgs e)
{
Window window = (d as PrzyciskZamykającyOkno).AssociatedObject;
RoutedEventHandler button_Click =
(object sender, RoutedEventArgs _e) =>
{
ICommand polecenie = (d as PrzyciskZamykającyOkno).Polecenie;
object parametrPolecenia =
(d as PrzyciskZamykającyOkno).ParametrPolecenia;
if (polecenie != null) polecenie.Execute(parametrPolecenia);
window.Close();
};
if (e.OldValue != null) ((Button)e.OldValue).Click -= button_Click;
if (e.NewValue != null) ((Button)e.NewValue).Click += button_Click;
}
}
Rozdział 7.  Zachowania, własności zależności i własności doczepione 79

Własność doczepiona
(attached property) i zachowanie
doczepione (attached behavior)
Znając własności zależności, możemy przyjrzeć się także kolejnej koncepcji XAML
i WPF, a mianowicie własnościom doczepianym (ang. attached property). Pozwalają
one „przekazywać” własności zdefiniowane w elemencie nadrzędnym do elementów-
dzieci. Dobrym przykładem jest na przykład własność Dock z pojemnika DockPanel2 lub
Grid.Row i Grid.Column z pojemnika Grid. Siatka (element Grid) sprawdza, czy umiesz-
czone w niej elementy mają ustawione własności Grid.Row lub Grid.Column, i umieszcza
je w komórkach o podanych w tych własnościach indeksach. Jeżeli ich nie ma, używa
domyślnych wartości, czyli w tym przypadku zer. Tworzenie własności doczepianych
zazwyczaj związane jest z ułożeniem kontrolek i realizowane jest przede wszystkim
w kontekście kontrolek-pojemników.

Aby zdefiniować własną własność doczepianą, należy zdefiniować statyczne pole typu
DependencyProperty, które przechowuje wartość zwróconą przez metodę Dependency
Property.RegisterAttached. Metoda ta rejestruje własność zależności (analogicznie
jak własność Przycisk z listingu 7.3). Oprócz tego należy zdefiniować dwie statyczne
metody: SetNazwaWłasności i GetNazwaWłasności. Jeżeli te elementy zamkniemy w osob-
nej klasie statycznej, uzyskamy zachowanie doczepiane (ang. attached behavior).
Przykład takiego „zestawu” widoczny jest na listingu 7.5. W przykładzie tym dodajemy
do kontrolek własność Klawisz. Przypisując ją w kodzie XAML, wiążemy ze zdarzeniem
PreviewKeyDown tej kontrolki metodę zdarzeniową zdefiniowaną w wyrażeniu lambda.
Jeżeli tą kontrolką jest całe okno, to przypisujemy mu metodę zamykającą to okno.
W pozostałych przypadkach przełączamy własność IsEnabled kontrolek na false.

Listing 7.5. Definiowanie zachowania doczepionego


public static class KlawiszWyłączBehavior
{
public static Key GetKlawisz(DependencyObject d)
{
return (Key)d.GetValue(KlawiszProperty);
}

public static void SetKlawisz(DependencyObject d, Key value)


{
d.SetValue(KlawiszProperty, value);
}

public static readonly DependencyProperty KlawiszProperty =


DependencyProperty.RegisterAttached(
"Klawisz",
typeof(Key),
typeof(KlawiszWyłączBehavior),
new PropertyMetadata(Key.None, KlawiszZmieniony));

2
Por. omówienie na stronie https://msdn.microsoft.com/en-us/library/ms749011(v=vs.110).aspx.
80 Część I  Wzorzec MVVM

private static void KlawiszZmieniony(DependencyObject d,


DependencyPropertyChangedEventArgs e)
{
Key klawisz = (Key)e.NewValue;
if(d is Window)
{
(d as Window).PreviewKeyDown +=
(object sender, KeyEventArgs _e) =>
{
if (_e.Key == klawisz)
(sender as Window).Close();
};
}
else
{
(d as UIElement).PreviewKeyDown +=
(object sender, KeyEventArgs _e) =>
{
if (_e.Key == klawisz)
(sender as UIElement).IsEnabled = false;
};
}
}
}

Tego typu zachowania nie trzeba dołączać do kolekcji zachowań, której używaliśmy
do tej pory. W zamian zachowania takie dodają do kontrolek dodatkowy atrybut.
Przykłady ich użycia pokazuje listing 7.6. Proszę zwrócić uwagę, że aby po urucho-
mieniu aplikacji móc nacisnąć przycisk na rzecz suwaka, trzeba go wpierw kliknąć,
aby uzyskał „focus”. Podobnie jest w przypadku siatki.

Listing 7.6. Kod XAML z zaznaczonymi przykładami użycia zachowań doczepionych


<Window x:Class="KoloryWPF.MainWindow"
...
xmlns:local="clr-namespace:KoloryWPF"
...
local:KlawiszWyłączBehavior.Klawisz="Q" >
...
<Grid local:KlawiszWyłączBehavior.Klawisz="W">
...
<Slider x:Name="sliderR" Margin="10,0,40,86"
Height="18" VerticalAlignment="Bottom" Maximum="255"
Value="{Binding R, Mode=TwoWay,
Converter={StaticResource konwersjaByteDouble}}"
local:KlawiszWyłączBehavior.Klawisz="E" />
...
</Grid>
</Window>
Rozdział 7.  Zachowania, własności zależności i własności doczepione 81

Zadania
1. Zmodyfikuj zachowanie ZamknięcieOknaPoNaciśnięciuKlawisza w taki sposób,
żeby własność Klawisz była własnością zależności i żeby metoda OnAttached
przestała być używana.
2. W aplikacji z suwakiem i paskiem postępu z zadań 2. – 4. z rozdziału 5. zdefiniuj
zachowanie rozszerzające kontrolkę Slider o możliwość ustalenia jednej z pozycji
w zależności od naciśniętego klawisza: dla klawisza 0 zmienia pozycję na 0,
dla 1 na 10%, dla 2 na 20% itd.
82 Część I  Wzorzec MVVM
Rozdział 8.
Testy jednostkowe
Wielką zaletą wzorca architektonicznego MVVM jest to, że rozszerza zakres kodu
projektów, który może być testowany, w szczególności testami jednostkowymi. W przy-
padku tego wzorca obowiązek testowania dotyczy nie tylko modelu, ale również mo-
delu widoku. Możliwe jest także testowanie niektórych fragmentów widoku, na
przykład konwerterów.

Testowanie oprogramowania, niezależnie od tego, czy traktowane jest jako osobny


etap projektu, czy jako integralna część procesu wytwarzania kodu, jest tematem bardzo
obszernym. Na pierwszej linii powinny jednak zawsze stać testy jednostkowe, które
warto tworzyć, bez względu na charakter i rozmiar projektu, a które mają za zadanie
pilnować, aby kod w trakcie wielu zmian, jakie wprowadza się w projekcie w trakcie
jego rozwoju, nie przestał robić tego, czego od niego oczekujemy. To ten rodzaj testów,
z którym powinien być „zaprzyjaźniony” nie tylko wyspecjalizowany tester oprogra-
mowania, ale również „zwykły” koder, programista i projektant. Są one, przynajmniej
po części, gwarancją poprawności kodu, ale też fundamentem poczucia bezpieczeń-
stwa w zespole zajmującym się rozwojem projektu.

Testy jednostkowe powinny powstawać równocześnie z zasadniczym kodem i powinny


dotyczyć wszystkich metod i własności publicznych, a w niektórych przypadkach także
prywatnej części klas. W poprzednich rozdziałach, pokazując konwersję projektu z ar-
chitektury AV do MVVM, nie zastosowałem się do tej zasady. Teraz częściowo nad-
robimy tę zaległość. Nie będę jednak przedstawiał wszystkich możliwych testów, jakie
powinny być napisane dla aplikacji KoloryWPF. Tych dla nawet stosunkowo prostego
projektu powinno być wiele. Przedstawię natomiast wybrane testy, które będą ilustrować
kolejne zagadnienia związane z przygotowywaniem testów jednostkowych. Głównym
celem tego rozdziału jest bowiem pomoc w rozpoczęciu testowania projektu — poka-
żę, jak utworzyć przeznaczony dla nich projekt i jak napisać pierwsze testy. Z pew-
nością nie jest to przewodnik po dobrych praktykach ani zbiór mądrościowych porad
dotyczących testów.
84 Część I  Wzorzec MVVM

Testy jednostkowe w Visual Studio 2013


W Visual Studio 2010, w menu kontekstowym edytora, dostępne było bardzo wygod-
ne polecenie Create Unit Test..., umożliwiające tworzenie testów jednostkowych dla
wskazanej kursorem metody. W Visual Studio 2012, w którym zmodyfikowany został
moduł odpowiedzialny za testy jednostkowe, to wygodne polecenie zniknęło. W zamian
w wersjach 2012 i 2013 należało ręcznie przygotowywać metody testów. W tych wer-
sjach zmuszeni jesteśmy także do samodzielnego dodania do projektu testów referencji
do projektu testowanego. W Visual Studio 2015 na szczęście wróciło „wspomaganie”
tworzenia testów – omówię je w dalszej części rozdziału, a na razie opiszę, jak radzić
sobie w Visual Studio 2013. Ten „ręczny” sposób można wykorzystać również w Visual
Studio 2015, ale to się zwyczajnie nie opłaca.

Projekt testów jednostkowych


Wczytajmy do Visual Studio 2013 lub 2015 projekt aplikacji KoloryWPF z poprzed-
niego rozdziału. Do tego rozwiązania dodamy kolejny projekt, zawierający zbiór klas
z testami jednostkowymi. W projekcie testów jednostkowych warto odwzorować struktu-
rę testowanego projektu. To oznacza grupowanie w jednej klasie testów wybranej klasy
projektu. Analogicznie klasy testujące jedną warstwę należy umieszczać we wspólnym
folderze o takich samych nazwach jak w testowanym projekcie.
1. W podoknie Solution Explorer rozwiń menu kontekstowe dla całego
rozwiązania i wybierz Add/New Project...
2. Pojawi się okno Add New Project, w którego lewym panelu wybierzmy
kategorię projektów Visual C#/Test.
3. W środkowym panelu zaznaczmy pozycję Unit Test Project.
4. Ustalmy nazwę projektu na TestyJednostkowe i kliknijmy OK.
Powstanie nowy projekt, a do edytora zostanie wczytany plik UnitTest1.cs,
automatycznie dodany do tego projektu. W pliku tym zdefiniowana jest
przykładowa klasa UnitTest1 z pustą metodą TestMethod1. Klasa ozdobiona
jest atrybutem TestClass, a metoda — atrybutem TestMethod.
5. Do projektu dodajmy foldery Model i ModelWidoku.
6. Przenieśmy plik UnitTest1.cs do folderu Model i zmieńmy jego nazwę na
KolorTesty.cs. W Visual Studio 2013 pojawi się pytanie, czy zmienić także
nazwę klasy. Pozwólmy na to, klikając Tak.

Warto zaznaczać w nazwach plików, że zawierają testy — w zakładkach edytora VS


wyświetlane są tylko nazwy plików, więc oznaczenie ich w ten sposób znacznie ułatwia
nawigację między zakładkami.
Rozdział 8.  Testy jednostkowe 85

Przygotowania do tworzenia testów


W projekcie testowym należy dodać referencję do projektu testowanego:
1. Aby umożliwić testowanie klasy KoloryWPF.Model.Kolor, dodaj do projektu
TestyJednostkowe referencję do projektu KoloryWPF. W tym celu z menu
kontekstowego projektu TestyJednostkowe wybierz Add/Reference..., z lewej
strony otwartego okna wybierz Solution/Projects. Następnie zaznacz pozycję
projektu KoloryWPF i kliknij przycisk OK.
2. Na początku pliku KolorTesty.cs dodaj polecenie using KoloryWPF.Model;,
dzięki czemu w metodach testowych łatwo będzie odwoływać się do testowanej
klasy Kolor.

Pierwszy test jednostkowy


Przygotujemy teraz pierwszy test jednostkowy. Będzie on sprawdzał działanie kon-
struktora klasy Kolor i jednocześnie jej trzech własności R, G i B. Teoretycznie rzecz
ujmując, w metodzie przeprowadzającej test można wyróżnić trzy etapy: przygotowa-
nie (ang. arrange), działanie (ang. act) i weryfikacja (ang. assert). Etapy te zaznaczone
zostały w komentarzach widocznych na listingu 8.1. W praktyce granica między tymi
etapami dość często się zaciera.

Listing 8.1. Klasa testująca klasę Kolor z jedną metodą testującą


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using KoloryWPF.Model;

namespace TestyJednostkowe.Model
{
[TestClass]
public class KolorTesty
{
[TestMethod]
public void TestKonstruktoraIWłasności()
{
//przygotowanie (arrange)
byte r = 0;
byte g = 128;
byte b = 255;

//działanie (act)
Kolor kolor = new Kolor(r, g, b);

//weryfikacja (assert)
Assert.AreEqual(r, kolor.R, "Niezgodność dotycząca własności R");
Assert.AreEqual(g, kolor.G, "Niezgodność dotycząca własności G");
Assert.AreEqual(b, kolor.B, "Niezgodność dotycząca własności B");
}
}
}
86 Część I  Wzorzec MVVM

Zmieńmy nazwę metody TestMethod1 na TestKonstruktoraIWłasności i umieśćmy w niej


kod widoczny na listingu 8.1. Pamiętajmy, że metoda testująca nie może zwracać warto-
ści ani pobierać parametrów, a dodatkowo musi być ozdobiona atrybutem TestMethod.

Powyższy test należy do najczęściej używanego rodzaju testów, w którym weryfika-


cja polega na porównaniu jakiejś wartości otrzymanej w wyniku działania (drugi etap
testu) z wartością oczekiwaną. W tego typu testach należy użyć wywołania statycznej
metody Assert.AreEqual, która decyduje o powodzeniu testu. Może ona być wywo-
ływana wielokrotnie ― wówczas do zaliczenia całego testu konieczne jest zaliczenie
wszystkich wywołań tej metody.

W przypadku, gdy w metodzie testującej jest kilka poleceń weryfikujących, warto


użyć możliwości podania komunikatu, który wyświetlany jest w oknie Test Explorer
(o nim za chwilę) w razie niepowodzenia testu. Możliwość ta jest jednak opcjonalna
i metoda Assert.AreEqual może być wywoływana tylko z dwoma argumentami, czyli
porównywanymi wartościami.

Testy jednostkowe
w Visual Studio 2015
W Visual Studio 2015 do menu kontekstowego edytora kodu wróciły polecenia uła-
twiające tworzenie testów jednostkowych. Przede wszystkim do dyspozycji mamy
polecenie Create Unit Tests, które umożliwia utworzenie testu jednostkowego dla wy-
branej metody lub własności, a jeżeli to konieczne, także projektu dla testów. Poza
tym w wersji Enterprise jest dostępne także polecenie Create IntelliTest, które umożliwia
utworzenie zbioru testów dla całej klasy i przygotowuje ich standardowe fragmenty
(zob. komentarz na ten temat poniżej).
1. Przejdźmy do pliku Model\Kolor.cs, ustawmy kursor edytora w konstruktorze
klasy Kolor i z menu kontekstowego wybierzmy Create Unit Tests. Pojawi się
okno widoczne na rysunku 8.1.
2. W rozwijanej liście Test Framework możemy wybrać platformę odpowiedzialną
za zarządzanie i przeprowadzanie testów. Domyślnie jest to dostarczona razem
z Visual Studio 2015 platforma MSTest, ale możliwe jest użycie innych – choćby
popularnego NUnit.
3. Kolejna rozwijana lista pozwala na wybór istniejącego lub utworzenie nowego
projektu testów jednostkowych. Załóżmy, że w rozwiązaniu nie ma jeszcze
takiego projektu, wówczas jedyną opcją będzie <New Test Project>.
4. W polu edycyjnym poniżej wpisujemy nazwę projektu, na przykład
„TestyJednostkowe”. Można wykorzystać istniejący szablon „[Project]Tests”,
który spowoduje utworzenie projektu o nazwie identycznej jak nazwa bieżącego
projektu, ale z dodanym przyrostkiem ..Tests, czyli KoloryWPFTests.
5. Przestrzeń nazw ustaliłem jako „[Namespace].TestyJednostkowe”, co spowoduje,
że będzie ona miała postać KoloryWPF.Model.TestyJednostkowe.
88 Część I  Wzorzec MVVM

}
}
}

Alternatywnym rozwiązaniem jest skorzystanie z polecenia Create IntelliTests, które


jest w stanie generować testy jednostkowe nie tylko dla pojedynczej metody, ale od
razu dla całej klasy, a konkretnie dla jej konstruktora oraz publicznych metod i wła-
sności. Testy, które powstają w ten sposób, zawierają kod tworzący instancję zwra-
caną przez metodę lub własność obiektu, należy jednak je uzupełnić o polecenia
weryfikujące ich poprawność. Testy IntelliTest wymagają specjalnego projektu, nie
można ich więc dołączać do utworzonego przed chwilą projektu testów jednostko-
wych. Można je uruchamiać poleceniem Run IntelliTests z menu kontekstowego
edytora ― inaczej niż zwykłe testy jednostkowe.

Uruchamianie testów
Sprawdźmy, czy kod naszego testu jest poprawny, kompilując całe rozwiązanie razem
z projektem testów (Ctrl+Shift+B lub F6). Aby uruchomić test (nie dotyczy to testów
IntelliTest), wybierzmy z menu Test polecenie Run/All Tests. Pojawi się wówczas
wspomniane przed chwilą podokno o nazwie Test Explorer (z lewej strony na rysunku
8.2). W podoknie tym widoczne są wszystkie uruchomione testy i ich wyniki. W Visual
Studio 2013 i 2015 ikona pokazująca efekt weryfikacji widoczna jest również w edytorze
kodu nad sygnaturą metody testującej, obok liczby wywołań (jednak nie we wszystkich
edycjach VS).

Rysunek 8.2.
Podokno Test
Explorer
Rozdział 8.  Testy jednostkowe 89

Testy wielokrotne
Choć testowanie działania metod lub operatorów dla wybranych wartości jest po-
trzebne i użyteczne, to konieczne jest również przeprowadzenie testów dla większego
zakresu wartości parametrów, szczególnie w końcowym etapie prac nad klasą. Oczy-
wiście trudno się spodziewać, że zawsze będziemy w stanie przygotować pętlę iteru-
jącą po wszystkich możliwych wartościach pól testowanej klasy. W przypadku typów
int lub double już dla jednego pola zajęłoby to o wiele za dużo czasu. Nawet w przypad-
ku klasy Kolor, w której wszystkie trzy pola są typu byte, a więc przyjmują wartości od
0 do 255, wszystkich możliwych stanów jest aż 2563 = 16 777 216. To oznacza, że nawet
w przypadku tak prostej klasy testowanie wszystkich możliwości (listing 8.3), choć
daje pewność, że klasa poprawnie działa we wszystkich stanach, jest niepraktyczne,
bo tak długiego testu nie można często powtarzać. Lepszym rozwiązaniem jest w tej
sytuacji testowanie klasy dla wielu losowo wybranych składowych koloru. Musi być ich
na tyle dużo, aby pokryły cały zakres możliwych wartości wszystkich pól (listing 8.4).

Listing 8.3. Testy zawierające elementy losowe mogą być powtarzane w jednej metodzie
[TestMethod]
public void TestKonstruktoraIWłasności_WszystkieWartości()
{
for(byte r = 0; r <= 255; r++)
for(byte g = 0; g <= 255; g++)
for (byte b = 0; b <= 255; b++)
{
Kolor kolor = new Kolor(r, g, b);

Assert.AreEqual(r, kolor.R, "Niezgodność dotycząca własności R");


Assert.AreEqual(g, kolor.G, "Niezgodność dotycząca własności G");
Assert.AreEqual(b, kolor.B, "Niezgodność dotycząca własności B");
}
}

Listing 8.4. Testy losowe


private const int liczbaPowtórzeń = 100000;
private Random rnd = new Random();

[TestMethod]
public void TestKonstruktoraIWłasności_LosoweWartości()
{
byte[] losoweWartościSkładowychKoloru = new byte[3 * liczbaPowtórzeń];
rnd.NextBytes(losoweWartościSkładowychKoloru);

for (int i = 0; i < liczbaPowtórzeń; i++)


{
byte r = losoweWartościSkładowychKoloru[3 * i];
byte g = losoweWartościSkładowychKoloru[3 * i + 1];
byte b = losoweWartościSkładowychKoloru[3 * i + 2];

Kolor kolor = new Kolor(r, g, b);

Assert.AreEqual(r, kolor.R, "Niezgodność dotycząca własności R");


90 Część I  Wzorzec MVVM

Assert.AreEqual(g, kolor.G, "Niezgodność dotycząca własności G");


Assert.AreEqual(b, kolor.B, "Niezgodność dotycząca własności B");
}
}

Wielokrotne powtarzanie testów i, co za tym idzie, wielokrotne wywoływanie metod


Assert.AreEqual lub Assert.IsTrue nie naraża nas na zafałszowanie wyniku całego
testu. Jak pamiętamy, do zaliczenia testu niezbędne jest, żeby wszystkie wywołania tych
metod potwierdziły poprawność kodu. W konsekwencji niezgodność choćby w jednym
podteście powoduje negatywny wynik całego testu.

Dostęp do prywatnych
pól testowanej klasy
Test konstruktora z listingu 8.1 ma zasadniczą wadę: testuje jednocześnie działanie
konstruktora i własności. W razie niepowodzenia nie wiemy, który z tych elementów
jest wadliwy. Możliwa jest też sytuacja, w której błędy kryją się zarówno w konstrukto-
rze, jak i we własnościach i wzajemnie się kompensują. Oczywiście trudno to sobie
wyobrazić w przypadku tak prostej klasy, jaką jest Kolor. W rozbudowanych klasach
jest to jednak bardziej prawdopodobne. Warto byłoby wobec tego oprócz powyższego
testu przygotować także test, w którym konstruktor sprawdzany jest przez bezpośrednią
weryfikację zainicjowanych w nim wartości pól oraz test własności sprawdzanych bez
udziału konstruktora.

Warto byłoby się nauczyć, jak takie testy pisać. Problem w tym, że w klasie Kolor nie
ma prywatnych pól — własności zdefiniowane są jako domyślnie zaimplementowane.
Załóżmy jednak na chwilę, że zamiast korzystać z domyślnie implementowanych
własności użyliśmy klasycznego rozwiązania z prywatnymi polami, które przechowują
wartości własności (listing 8.5). Wówczas moglibyśmy sprawdzić, czy konstruktor
prawidłowo je inicjuje. Ale jak to zrobić, skoro są one prywatne? Pomocą służy klasa
PrivateObject z przestrzeni nazw Microsoft.VisualStudio.TestTools.UnitTesting, tej
samej, w której zdefiniowana jest klasa Assert. Przykład jej użycia pokazuje listing 8.6.
W nim do odczytu wartości prywatnego pola używam metody PrivateObject.GetField.

Listing 8.5. Klasa modelu z jawnie zdefiniowanymi prywatnymi polami przechowującymi wartości
składowych koloru
namespace KoloryWPF.Model
{
public class Kolor
{
private byte r, g, b;

public byte R
{
get { return r; }
set { r = value; }
}
Rozdział 8.  Testy jednostkowe 91

public byte G
{
get { return g; }
set { g = value; }
}
public byte B
{
get { return b; }
set { b = value; }
}

public Kolor(byte r, byte g, byte b)


{
this.r = r;
this.g = g;
this.b = b;
}
}
}

Listing 8.6. Weryfikowanie wartości prywatnych pól


[TestMethod]
public void TestKonstruktora()
{
byte r = 0;
byte g = 128;
byte b = 255;

Kolor kolor = new Kolor(r, g, b);

PrivateObject po = new PrivateObject(kolor);


byte kolor_r = (byte)po.GetField("r");
byte kolor_g = (byte)po.GetField("g");
byte kolor_b = (byte)po.GetField("b");
Assert.AreEqual(r, kolor_r, "Niezgodność dotycząca pola r");
Assert.AreEqual(g, kolor_g, "Niezgodność dotycząca pola g");
Assert.AreEqual(b, kolor_b, "Niezgodność dotycząca pola b");
}

Warto również zwrócić uwagę na metodę PrivateObject.SetField, umożliwiającą


zmianę wartości prywatnego pola testowanej klasy (listing 8.7). Dzięki niej można
ustawić wartość prywatnych pól i sprawdzić, czy własności poprawnie udostępniają
ich wartości. Klasa PrivateObject ma również metody GetProperty i SetProperty,
służące do testowania prywatnych własności, oraz metodę Invoke, pozwalającą testować
prywatne metody.

Listing 8.7. Inicjacja prywatnych pól i testowanie własności udostępniających ich wartości
[TestMethod]
public void TestWłasności()
{
byte r = 0;
byte g = 128;
byte b = 255;
Rozdział 8.  Testy jednostkowe 93

public static byte g = 128;


public static byte b = 255;

public static Kolor Czytaj()


{
return new Kolor(r, g, b);
}

public static void Zapisz(Kolor kolor)


{
r = kolor.R;
g = kolor.G;
b = kolor.B;
}
}

Najbardziej elegancka byłaby możliwość „wstrzykiwania” obiektu odpowiedzialnego


za przechowywanie ustawień (oryginalnej klasy Ustawienia we właściwym kodzie
lub atrapy w testach) do modelu widoku na przykład poprzez argument konstruktora.
Ale wówczas nie mogłyby to być statyczne klasy, jak jest w tej chwili, i oczywiście
należałoby zdefiniować dla nich wspólny interfejs, który obie by implementowały. Inną
możliwością jest uczynienie z nich singletonów, których klasy przekazywane byłyby
przez parametr klasy modelu widoku. Niestety oba rozwiązania niezbyt współgrają
z tym, że klasa EdycjaKoloru jest modelem widoku. Klasa modelu widoku tworzona
jest w XAML za pomocą konstruktora domyślnego, co wyklucza pierwsze rozwiązanie.
Utrudnione jest także użycie typu parametrycznego (ang. generic type) jako modelu
widoku, co wyklucza drugą z powyższych propozycji. Innym prostym rozwiązaniem
jest kompilacja warunkowa. Jeżeli zdefiniujemy stałą makro TESTY, kompilowana mo-
głaby być klasa-atrapa, w przeciwnym razie — „normalna” klasa ustawień. Jeszcze in-
nym rozwiązaniem, najmniej eleganckim, ale też najprostszym do realizacji, jest pod-
mienienie jednej klasy drugą na czas testu. I tak klasę Ustawienia z listingu 3.2
zwyczajnie „zakomentowałem”, a do pliku Model/Ustawienia.cs wstawiłem na czas
testów klasę Ustawienia z listingu 8.8. Ta druga, w odróżnieniu od oryginalnej, jest
publiczna, będziemy się bowiem do niej odnosić w kodzie metod testujących. Na li-
stingu 8.89 widoczne są dwa testy korzystające z powyższej atrapy: test konstruktora
klasy EdycjaKoloru i test zdefiniowanego w niej polecenia Zapisz. Na dokładkę test
polecenia Resetuj.

Listing 8.9. Kilka testów modelu widoku


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using KoloryWPF.Model;
using KoloryWPF.ModelWidoku;

namespace TestyJednostkowe.ModelWidoku
{
[TestClass]
public class EdycjaKoloruTesty
{
[TestMethod]
public void TestKonstruktora()
94 Część I  Wzorzec MVVM

{
EdycjaKoloru ek = new EdycjaKoloru();

Assert.AreEqual(0, ek.R, "Niezgodność dotyczy własności R");


Assert.AreEqual(128, ek.G, "Niezgodność dotyczy własności G");
Assert.AreEqual(255, ek.B, "Niezgodność dotyczy własności B");
}

private const int liczbaPowtórzeń = 100000;


private Random rnd = new Random();

public void TestPoleceniaZapisz()


{
for (int i = 0; i < liczbaPowtórzeń; i++)
{
byte[] składoweKoloru = new byte[3];
rnd.NextBytes(składoweKoloru);

EdycjaKoloru ek = new EdycjaKoloru();


ek.R = składoweKoloru[0];
ek.G = składoweKoloru[1];
ek.B = składoweKoloru[2];

Assert.IsTrue(ek.Zapisz.CanExecute(null));
ek.Zapisz.Execute(null);
Assert.AreEqual(ek.R, Ustawienia.r);
Assert.AreEqual(ek.G, Ustawienia.g);
Assert.AreEqual(ek.B, Ustawienia.b);
}
}

[TestMethod]
public void TestPoleceniaResetuj()
{
EdycjaKoloru ek = new EdycjaKoloru();
//ek.R = 0; ek.G = 128; ek.B = 255;

Assert.IsTrue(ek.Resetuj.CanExecute(null), "Niezgodność dotyczy metody


CanExecute wywołanej przed zresetowaniem");
ek.Resetuj.Execute(null);
Assert.AreEqual(0, ek.R, "Niezgodność dotyczy własności R");
Assert.AreEqual(0, ek.G, "Niezgodność dotyczy własności G");
Assert.AreEqual(0, ek.B, "Niezgodność dotyczy własności B");
Assert.IsFalse(ek.Resetuj.CanExecute(null), "Niezgodność dotyczy metody
CanExecute wywołanej po zresetowaniu");
}
}
}

Z atrap obiektów można korzystać w każdej sytuacji, w której testowana klasa zależy
od jakichś zewnętrznych obiektów lub danych (np. odczytywanych z baz danych lub
urządzeń fizycznych). Te obiekty lub dane mogą być trudne do kontroli, choćby dla-
tego, że odczytywane z nich wartości zależą od jeszcze innych czynników, nie są de-
terministyczne, zależą od decyzji użytkownika programu lub po prostu nie są jeszcze
przetestowane i tym samym godne zaufania albo zwyczajnie nie są gotowe. Wielką
Rozdział 8.  Testy jednostkowe 95

zaletą atrap jest to, że możemy w pełni kontrolować ich zachowanie. Szczególnie wy-
godne jest to w sytuacji, w której chcemy odtworzyć błąd występujący dla pewnego
trudnego do odtworzenia w rzeczywistym obiekcie zewnętrznym stanu. Wówczas
obiekty zastępcze są wręcz nieocenione. Pozwalają one również uniknąć sytuacji, w któ-
rych nie jest jasne, co jest testowane i co jest źródłem ewentualnego błędu — testowana
klasa czy obiekt zewnętrzny.

Testowanie konwersji
Testy jednostkowe modelu i modelu widoku są obowiązkowymi elementami poważ-
nych projektów opartych na wzorcu MVVM. Ale również niektóre klasy widoku mogą
być testowane. Dobrym przykładem są konwertery. To wdzięczny przedmiot testów
— należy przygotować dane wejściowe, przeprowadzić konwersję i sprawdzić, czy uzy-
skaliśmy prawidłowy obiekt na wyjściu. Jako przykładu użyjmy konwertera ColorTo
SolidColorBrushConverter, przekształcającego wskazany kolor w pędzel typu Solid
ColorBrush. Jego przykładowy test widoczny jest na listingu 8.10. Warto rozważyć,
czy dla testów klas należących do warstwy widoku nie utworzyć dodatkowego pro-
jektu. Testy te wymagają bowiem dodania do projektu wielu referencji do bibliotek
charakterystycznych właśnie dla widoku. W naszym przypadku konieczne będą refe-
rencje do PresentationCore (klasy Color i Brush), PresentationFramework (interfejs
IValueConverter) i WindowsBase. Klasę testów konwertera umieściłem w osobnym
folderze Widok.

Listing 8.10. Testy konwertera ColorToSolidColorBrushConverter


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using KoloryWPF;
using System.Windows.Media;

namespace TestyJednostkowe.Widok
{
[TestClass]
public class KonwerteryTesty
{
[TestMethod]
public void ColorToSolidColorBrushTest_Convert()
{
ColorToSolidColorBrushConverter konwerter =
new ColorToSolidColorBrushConverter();
Color kolor = Colors.Violet;

object obiekt = konwerter.Convert(kolor, typeof(SolidColorBrush), null,


System.Globalization.CultureInfo.CurrentCulture);
Assert.IsInstanceOfType(obiekt, typeof(SolidColorBrush));

SolidColorBrush pędzel = (SolidColorBrush)obiekt;


//Assert.AreEqual(Brushes.Violet, pędzel);
Assert.AreEqual(kolor, pędzel.Color);
Rozdział 8.  Testy jednostkowe 97

możemy sprawdzić, jeżeli metodę testującą poprzedzimy atrybutem ExpectedException


(listing 8.11). Jego parametrem jest oczekiwany typ wyjątku. Atrybut sprawia, że test
się powiedzie, jeżeli w metodzie testującej zgłoszony zostanie wyjątek wskazany w pa-
rametrze lub po nim dziedziczący.

Listing 8.11. Prowokowanie wyjątku w metodzie testującej


[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void ColorToSolidColorBrushTest_ConvertBack()
{
ColorToSolidColorBrushConverter konwerter = new
ColorToSolidColorBrushConverter();
konwerter.ConvertBack(null, null, null, null);
}

Ponownie uruchommy testy, aby sprawdzić, czy nowy test działa prawidłowo. Możemy
to zrobić z okna Test Explorer, klikając Run All. Testy uruchamiane są równolegle, co
jest zarówno wygodne, jak i kłopotliwe. Wygodne, bo to w oczywisty sposób przy-
spiesza ich wykonanie, a dodatkowo pozwala sprawdzić ukryte zależności przy jed-
noczesnym dostępie do zasobów. Kłopotliwe, bo utrudnia separację testów w takich
przypadkach. Weźmy chociażby często używaną w testach klasę Random — klasa ta
nie jest bezpieczna ze względu na użycie w wielu wątkach. Przy dużej liczbie rów-
noległych wywołań metoda Random.Next może zwracać niepoprawne wyniki. W takiej
sytuacji lepiej korzystać z generatorów liczb pseudolosowych tworzonych lokalnie
w metodach testujących, a inicjowanych ziarnem pobranym z generatora zdefiniowa-
nego jako pole klasy testującej.

***

Celem testów jednostkowych jest szukanie ukrytych w kodzie błędów logicznych


i pilnowanie poprawności kodu podczas zmian wprowadzanych do projektu. Oba te
cele wymagają przygotowania jak największej liczby różnorodnych testów, nawet je-
żeli wydaje się nam, że wszystkie błędy już znaleźliśmy, a poważnych zmian już nie
będzie. Takie przekonanie jest naturalne u programisty, który tworzy kod. Stąd częsty
brak wystarczającego zaangażowania w proces testowania i pokusa, żeby ten etap
projektu „odbębnić” jak najszybciej. Na pośpiech często naciskają też kierownicy
projektów i ich zwierzchnicy. I dlatego dobrze jest, jeżeli testowaniem nie zajmuje się
programista, który przygotowuje kod, a ktoś inny, najlepiej osoba wyspecjalizowana
w przeprowadzaniu testów lub w ogóle osobny dział testowania. Z drugiej strony często
można usłyszeć opinię zupełnie odwrotną, której też trudno odmówić słuszności, a mia-
nowicie, że każdy programista jest odpowiedzialny za własny kod, dlatego powinien
sam przeprowadzać jego testy, w szczególności pisać testy jednostkowe, które pozwolą
mu pilnować jego poprawności. Oba poglądy mogą być jednocześnie słuszne, bo do-
tyczą innych płaszczyzn. Pierwszy mówi o tym, jak jest (psychologia pracy), a drugi
o tym, jak być powinno (etyka programisty). W praktyce trzeba oba w jakimś zakresie
połączyć, w czym pomagają nowoczesne metodyki wytwarzania oprogramowania i pracy
w zespołach.
98 Część I  Wzorzec MVVM

Pisanie testów jest trudnym, a jednocześnie często niedocenianym elementem pro-


jektów informatycznych. Wymaga wyobraźni, ogromnej skrupulatności i odpowie-
dzialności. I z reguły jest bardziej pracochłonne niż samo pisanie kodu, a kod testów,
pomimo jego stosunkowo niewielkiej złożoności, niejednokrotnie jest największą
częścią projektu.
Rozdział 9.
Powtórzenie
W ramach powtórzenia podstawowych wiadomości o MVVM przygotujemy aplika-
cję, której zadaniem będzie sumowanie cen towarów wrzucanych do koszyka w su-
permarkecie. Na razie przygotujemy ją w WPF, co oczywiście nie ma większego sensu,
ale w trzeciej części książki przeniesiemy ją na Windows Phone i Windows RT. Widok
aplikacji będzie się składał z kontrolki TextBlock, która będzie wyświetlała bieżącą
wartość sumy, kontrolki TextBox, która będzie umożliwiała wpisanie ceny dokładanego
produktu, i przycisku, który będzie powodował, że wpisana cena będzie dodawana do
sumy. W przedstawionej poniżej uproszczonej wersji aplikacja będzie miała stały li-
mit wydatków równy 1000 złotych, którego nie będzie można przekroczyć. Aby zadanie
uprościć, aplikacja w wersji WPF nie będzie zapisywać swojego stanu.

Model
Stwórzmy projekt aplikacji WPF o nazwie AsystentZakupówWPF. Przygotowywanie
kodu zacznijmy od modelu. Przypominam, że powinien on być łatwy do testowania,
co oznacza między innymi brak zależności (oczywiście jeżeli to jest możliwe). Musimy
także zdecydować, czy ma jedynie opisywać elementy odwzorowywanego świata, jak
było w aplikacji KoloryWPF, czy też zawierać jakąś logikę. To oznacza między in-
nymi decyzję, czy model ponosi odpowiedzialność za poprawność i zapisywanie danych,
czy też odpowiedzialność za to będzie ponosił model widoku. Tym razem proponuję
wyposażyć klasę modelu w mechanizmy walidacji danych.

Model będzie znowu wyjątkowo prosty, ponieważ i tym razem składa się tylko z jednej
klasy ― SumowanieKwot (listing 9.1). Klasa ta udostępnia dwie publiczne własności
typu decimal o nazwach Suma i Limit, ale możliwość ich modyfikacji jest ograniczona
do zakresu prywatnego. Ich wartości inicjowane są w konstruktorze klasy. Poza tym są
w niej zdefiniowane dwie publiczne metody: Dodaj i CzyKwotaJestPoprawna. Argu-
mentem obu jest kwota typu decimal. Pierwsza zwiększa wartość sumy, a druga po-
zwala sprawdzić, czy dodanie wskazanej kwoty jest jeszcze możliwe, to znaczy czy
po dodaniu suma nie przekroczy ustalonego w konstruktorze limitu. Do przechowywania
sumy, limitu i przekazywania kwot używamy typu decimal, który właśnie do operacji
100 Część I  Wzorzec MVVM

na walutach został zaprojektowany. Jedyną przestrzenią nazw, jakiej wymaga kod tej
klasy, jest System. W niej także zdefiniowany jest wyjątek ArgumentOutOfRangeException,
który zgłaszamy w wypadku, gdy dodanie nowej kwoty spowodowałoby przekroczenie
limitu.

Listing 9.1. Klasa modelu


using System;

namespace AsystentZakupówWPF.Model
{
public class SumowanieKwot
{
public decimal Limit { get; private set; }
public decimal Suma { get; private set; }

public SumowanieKwot(decimal limit, decimal suma = 0)


{
this.Limit = limit;
this.Suma = suma;
}

public void Dodaj(decimal kwota)


{
if (!CzyKwotaJestPoprawna(kwota))
throw new ArgumentOutOfRangeException("Kwota zbyt duża lub ujemna");
Suma += kwota;
}

public bool CzyKwotaJestPoprawna(decimal kwota)


{
bool czyDodatnia = kwota > 0;
bool czyPrzekroczyLimit = Suma + kwota > Limit;
return czyDodatnia && !czyPrzekroczyLimit;
}
}
}

Zgodnie z wytycznymi DDD (zob. rozdział 2.) klasa modelu powinna mieć nazwę
w formie rzeczownika, która dobrze oddaje opisywany przez model fragment rzeczy-
wistości. Nazwa, której użyłem w powyższym modelu, a mianowicie SumowanieKwot,
opisuje czynność. To jednak wiąże się z drugim odstępstwem od DDD — powyższy
model zawiera logikę w postaci metody Dodaj, która umożliwia tytułowe sumowanie
kwot, oraz metody, która sprawdza, czy sumowanie jest możliwe.

Prototyp widoku
Następnie zdefiniujmy widok. To, czy pierwszy projektowany jest model czy widok,
zależy w zasadzie od preferencji programistów i ścieżki, jaką podąży rozmowa z osobą
zamawiającą projekt. Ja lubię zaczynać od widoku, bo wówczas łatwo jest omawiać
przypadki użycia, które ułatwiają ustalenie, czego klient tak naprawdę potrzebuje.
Rozdział 9.  Powtórzenie 101

Trzy kontrolki widoku będą ułożone w trzech wierszach siatki (rysunek 9.1). W pierwszym
będzie TextBlock, w którym chcemy wyświetlać sumę obliczoną w modelu. Druga
kontrolka to pole edycyjne TextBox, pozwalająca wpisać kwotę. Natomiast trzecia to
przycisk, którym będziemy uruchamiali dodawanie wpisanej kwoty do sumy. Cały kod
XAML widoczny jest na listingu 9.2. Proszę zwrócić uwagę na to, że ograniczyłem
kod opisujący interfejs do minimum. W szczególności pole tekstowe nie ma nawet
etykiety podpowiadającej jego przeznaczenie.

Rysunek 9.1.
Interfejs aplikacji

Listing 9.2. Pierwsza wersja widoku


<Window x:Class="AsystentZakupówWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AsystentZakupówWPF"
mc:Ignorable="d"
ResizeMode = "NoResize"
Title="Asystent zakupów" Height="200" Width="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left" Grid.Row="0"
FontSize="25" Foreground="Navy" Margin="10">
Suma:
<Run Foreground="Black" FontFamily="Courier New"
Text="0" />
</TextBlock>
<TextBox x:Name="tbKwota" FontSize="30" FontFamily="Courier New"
TextAlignment="Right" Margin="10" Grid.Row="1" Text="0" />
<Button x:Name="btnDodaj" Content="Dodaj" FontSize="20" Margin="10"
Grid.Row="2" />
</Grid>
</Window>
102 Część I  Wzorzec MVVM

Model widoku
Model widoku również nie będzie zbyt skomplikowany. Będzie udostępniał jedną
własność Suma i jedno polecenie DodajKwotę, a także metodę sprawdzającą, czy łańcuch
zawiera poprawną kwotę. Poza tym oczywiście implementuje interfejs INotifyProperty
Changed. Nietypowy ― i nie do końca kanoniczny ― jest ukłon modelu widoku
w stronę widoku polegający na tym, że zamiast typu decimal użytego w modelu uży-
wamy łańcuchów. Własność Suma jest typu string, a polecenie DodajKwotę akceptuje
argument będący łańcuchem. Dzięki temu unikamy definiowania konwerterów, ale co
ważniejsze, możemy przeprowadzić weryfikację kwoty w metodzie CanExecute pole-
cenia. Argument polecenia, zarówno w przypadku metody Execute, jak i CanExecute,
będzie używany, co oznacza, że będziemy musieli go uwzględnić w wiązaniu przycisku.
Pełen kod modelu widoku widoczny jest na listingu 9.3. Do projektu należy dodać
plik z klasą RelayCommand (por. listing 6.8), której używamy przy definiowaniu polecenia.
Ta klasa powinna być widoczna w przestrzeni nazw AsystentZakupówWPF.ModelWidoku.

Listing 9.3. Klasa modelu widoku


using System;
using System.Windows.Input;

namespace AsystentZakupówWPF.ModelWidoku
{
using Model;
using System.ComponentModel;
using System.Windows;

public class ModelWidoku : INotifyPropertyChanged


{
private SumowanieKwot model = new SumowanieKwot(1000);

public string Suma


{
get
{
return model.Suma.ToString();
}
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(string nazwaWłasnosci)


{
PropertyChanged?.Invoke(this, new
PropertyChangedEventArgs(nazwaWłasnosci));
}

public bool CzyŁańcuchKwotyJestPoprawny(string s)


{
if (string.IsNullOrWhiteSpace(s)) return false;
decimal kwota;
if (!decimal.TryParse(s, out kwota)) return false;
else return model.CzyKwotaJestPoprawna(kwota);
104 Część I  Wzorzec MVVM

Listing 9.4. Wiązania dodane w widoku


<Window x:Class="AsystentZakupówWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AsystentZakupówWPF"
xmlns:mw="clr-namespace:AsystentZakupówWPF.ModelWidoku"
mc:Ignorable="d"
Title="Asystent zakupów" Height="200" Width="200">
<Window.DataContext>
<mw:ModelWidoku />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="25"
Foreground="Navy" Grid.Row="0" Margin="10,10,10,10">
Suma:
<Run Foreground="Black" FontFamily="Courier New"
Text="{Binding Path=Suma, Mode=OneWay}" />
</TextBlock>
<TextBox x:Name="tbKwota" FontSize="30" FontFamily="Courier New"
TextAlignment="Right" Margin="10,10,10,10" Grid.Row="1" Text="0" />
<Button x:Name="btnDodaj" Content="Dodaj" FontSize="20" Margin="10,10,10,10"
Grid.Row="2" Command="{Binding DodajKwotę}"
CommandParameter="{Binding ElementName=tbKwota, Path=Text}" />
</Grid>
</Window>

Konwerter
Dzięki użyciu własności CanExecute i temu, że w klasie RelayCommand wykorzystuje-
my menedżer poleceń, przycisk Dodaj staje się nieaktywny, gdy wpisany w polu edy-
cyjnym łańcuch nie jest poprawną liczbą, względnie jest kwotą ujemną lub taką, która
spowodowałaby przekroczenie limitu. Nieaktywność przycisku uniemożliwia podję-
cie próby przekroczenia limitu, nie wskazuje jednak na źródło błędu — użytkownik
może poczuć się zdezorientowany. Chciałbym wobec tego dodatkowo podkreślić nie-
poprawność wpisanego łańcucha zmianą jego koloru. W tym celu nie musimy wcale
dodawać kodu do modelu widoku. Wystarczy, jeżeli ten kolor zwiążemy z kontrolo-
waną przez menedżer poleceń własnością IsEnabled przycisku. Do tego potrzebujemy
jednak konwertera wielkości typu bool do Brush (listing 9.5).
Rozdział 9.  Powtórzenie 105

Listing 9.5. Klasa konwertera


using System;
using System.Windows.Data;
using System.Windows.Media;

namespace AsystentZakupówWPF
{
class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
bool b = (bool)value;
return b ? Brushes.Black : Brushes.Red;
}

public object ConvertBack(object value, Type targetType, object parameter,


System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

W kodzie XAML stwórzmy instancję konwertera i użyjmy jej, aby do elementu TextBox
dodać wiązanie z przyciskiem (listing 9.6).

Listing 9.6. Użycie konwertera


Window x:Class="AsystentZakupówWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AsystentZakupówWPF"
xmlns:mw="clr-namespace:AsystentZakupówWPF.ModelWidoku"
mc:Ignorable="d"
Title="Asystent zakupów" Height="200" Width="200">
<Window.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush" />
</Window.Resources>
...
<TextBox x:Name="tbKwota" FontSize="30" FontFamily="Courier New"
TextAlignment="Right" Margin="10,10,10,10" Grid.Row="1" Text="0"
Foreground="{Binding ElementName=btnDodaj,
Path=IsEnabled, Mode=OneWay,
Converter={StaticResource boolToBrush}}" />
...

To spowoduje, że aplikacja swoim wyglądem wyraźnie zasygnalizuje niepoprawną


kwotę wpisaną w polu edycyjnym (rysunek 9.2).
108 Część I  Wzorzec MVVM
Część II
Zaawansowane
zagadnienia budowania
interfejsu w XAML
Rozdział 10.
Budowanie złożonych
kontrolek
Ten rozdział poświęcony będzie tylko jednej kontrolce — przyciskowi. Ale akurat
wybór konkretnej kontrolki nie jest wcale istotny. Jako przykład mogłyby nam posłużyć
także inne kontrolki dostępne w XAML. Istotne jest natomiast to, co zrobimy z tym
przyciskiem. A zrobimy z nim rzeczy, które dla osoby przyzwyczajonej do standar-
dowego interfejsu projektowanego na przykład za pomocą kontrolek Windows Forms
są trudne do pomyślenia.

Konfiguracja przycisku
w podoknie Properties
Zaczniemy od umieszczenia w oknie jednego przycisku i skonfigurowania go za pomocą
podokna Properties.
1. Stwórzmy nowy projekt aplikacji WPF o nazwie XamlWPF.
2. Umieśćmy na formie przycisk, klikając dwukrotne jego ikonę w podoknie
Toolbox.
3. Za pomocą okna Properties zmieniamy jego etykietę, wpisując dowolny tekst
w polu znajdującym się przy własności Content. Ja, lojalnie względem swego
pracodawcy, użyłem łańcucha „Uniwersytet Mikołaja Kopernika” (rysunek 10.1).
4. Zmieńmy także położenie i wielkość przycisku:
a) w podoknie Properties, w sekcji Layout, przy własnościach Horizontal
Alignment i VerticalAlignment, klikamy ikony odpowiadające własności
Center (rysunek 10.1);
b) szerokość i wysokość przycisku (własności Width i Height) ustalamy na równą
200100 (rysunek 10.1).
Rozdział 10.  Budowanie złożonych kontrolek 113

Rysunek 10.2.
Konfigurowanie
pędzla za pomocą
okna własności

Wszystkie zmiany wprowadzone za pomocą podokna Properties zostaną naniesione


na kod XAML. Po wykonaniu powyższych czynności element przycisku powinien
być podobny do tego, który jest widoczny na listingu 10.1, a po uruchomieniu aplika-
cji zobaczymy przycisk zaprezentowany na rysunku 10.3. Od tego momentu pozosta-
niemy już przy edycji kodu XAML — w praktyce okazuje się to znacznie wygodniejsze
niż korzystanie z podokna Properties.

Listing 10.1. Pierwotny kod XAML projektowanego przycisku


<Window x:Class="XamlWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Uniwersytet Mikołaja Kopernika"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100" Foreground="Navy" />
</Grid>
</Window>
Rozdział 10.  Budowanie złożonych kontrolek 115

Pędzle
Własności kontrolki, których wartości ustalane w podoknie Properties zostały zapisane
jako atrybuty elementu Button, mogą być także umieszczone w kodzie jako elementy
zagnieżdżone. Weźmy na przykład własność Foreground. Usuńmy ze znacznika Button
reprezentujący ją atrybut i przypisaną do niego wartość Navy, a zamiast tego wstawmy
podelement wyróżniony na listingu 10.2. Zauważmy, że nowy element ma nazwę
Button.Foreground. Oznacza to, że nie reprezentuje osobnej kontrolki WPF, a tylko
własność kontrolki nadrzędnej Button.

Listing 10.2. Odmienny sposób ustalania wartości własności


<Button x:Name="button"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100" Foreground="Navy">
Uniwersytet Mikołaja Kopernika
<Button.Foreground>Navy</Button.Foreground>
</Button>

Przeniesienie miejsca ustalenia wartości własności Foreground z atrybutu do podele-


mentu nie ma uzasadnienia, jeżeli jej wartość można określić jednym słowem, na
przykład White lub Navy. Jeżeli jednak przypisujemy jej bardziej złożony obiekt lub
chcemy jej elementy związać z własnościami modelu, może być to niezbędne. Skupmy
się teraz na własności Background (podobnie jak Foreground i własność Fill prosto-
kąta jest ona typu Brush — można przypisać im pędzel). W oknie Properties możemy
zobaczyć, że domyślna wartość własności Background jest typu LinearGradientBrush.
Jest to więc pędzel wypełniający obszar za pomocą stopniowo zmieniającego się ko-
loru (dla ułatwienia nazwijmy go gradientem, co jest powszechnie, ale nieprawidłowo
używanym określeniem). Domyślnie kolor zmieniany jest w pionie. W ramach testów
możemy sprawdzić, jak w roli wypełnienia przycisku sprawi się gradient radialny (li-
sting 10.3).

Listing 10.3. Przycisk z tłem przygotowanym za pomocą radialnego gradientu


<Button x:Name="button"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100">
Uniwersytet Mikołaja Kopernika
<Button.Foreground>White</Button.Foreground>
<Button.Background>
<RadialGradientBrush GradientOrigin="0.75,0.25">
<GradientStop Color="Yellow" Offset="0.0" />
<GradientStop Color="Orange" Offset="0.5" />
<GradientStop Color="Red" Offset="1.0" />
</RadialGradientBrush>
</Button.Background>
</Button>
Rozdział 10.  Budowanie złożonych kontrolek 117

<GradientStop Offset="0.833" Color="Indigo" />


<GradientStop Offset="1" Color="Purple" />
</LinearGradientBrush>
</Button.Background>

Rysunek 10.6.
Gradient liniowy
z wieloma punktami
pośrednimi

Innym rozwiązaniem na ciekawe tło jest użycie gotowego obrazu i pędzla ImageBrush,
na przykład:
<ImageBrush ImageSource="ObrazTła.png" Stretch="Uniform"
AligmentX="Left" AlignmentY="Top" Opacity="0.5" />

Możemy jednego obrazu użyć na całym przycisku lub utworzyć z niego powtarzającą
się mozaikę.

Przyciski sformatowane „na bogato” z pewnością wyglądają ciekawie i rzucają się


w oczy, w praktyce jednak dla klasycznych prostokątnych przycisków najlepiej sprawdza
się bardziej dyskretny gradient liniowy pomiędzy dwoma bliskimi odcieniami tego
samego koloru — prostota nie szkodzi czytelności, a elegancja nigdy się nie nudzi. Zde-
finiujmy zatem skierowany poziomo liniowy gradient w tonacji niebiesko-granatowej.
W tym celu jeszcze raz modyfikujemy kod XAML elementu Button zgodnie z wyróż-
nieniem na listingu 10.5. Efekt widoczny jest na rysunku 10.7.

Listing 10.5. Liniowy gradient


<Button x:Name="button"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100">
Uniwersytet Mikołaja Kopernika
<Button.Foreground>White</Button.Foreground>
<Button.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>
120 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

a) z menu Project wybieramy Add Existing Item...;


b) zmieniamy filtr na Image Files;
c) w standardowym oknie dialogowym wybieramy plik logo.gif, który
musimy wcześniej przygotować, i klikamy przycisk Add;
d) zaznaczamy dodany plik w podoknie Solution Explorer i upewniamy się
w oknie Properties, czy opcja Build Action ustawiona jest na Resource,
a Copy to Output Directory na Do not copy ― w ten sposób obraz zostanie
dodany do skompilowanego pliku .exe i nie trzeba będzie go rozpowszechniać
osobno.
2. Następnie modyfikujemy kod XAML odpowiadający za wygląd przycisku
Button. Zmiany te pokazane są na listingu 10.7, a ich efekt jest widoczny
na rysunku 10.9.

Listing 10.7. StackPanel pozwala na „wstawienie” do przycisku wielu różnych elementów


<Button x:Name="button"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
<Button.Foreground>White</Button.Foreground>
<Button.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>

Jak zapowiedziałem wyżej, ze względu na to, że na przycisku może być tylko jeden
obiekt, musieliśmy obiekt TextBlock, obraz z logo i dodatkowy prostokąt, który służy
za margines między nimi, umieścić na panelu (klasa StackPanel), a dopiero ten panel
umieściliśmy na przycisku. StackPanel, obok Grid, Canvas czy DockPanel, to pojem-
niki, w których możemy umieszczać wiele kontrolek, a one przejmują odpowiedzial-
ność za ich właściwe ułożenie1. W StackPanel kontrolki są ułożone w poziomie lub
pionie w jednej linii. W siatce (Grid) można zdefiniować siatkę komórek, w których
umieszczamy kontrolki. To dwa najczęściej wykorzystywane pojemniki.

1
Zob. https://msdn.microsoft.com/en-us/library/bb675291(v=vs.90).aspx.
122 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="200">
<Button x:Name="button" Margin="0,0,0,0">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
<Button.Foreground>White</Button.Foreground>
<Button.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>
</UserControl>

4. Zbudujmy cały projekt. Dzięki temu po powrocie do kodu XAML okna


(zakładka MainWindow.xaml edytora kodu) w podoknie Toolbox powinniśmy
zobaczyć nową sekcję o nazwie XamlWPF Controls, a w niej kontrolkę
oznaczoną jako PrzyciskWPF (XamlWPF).
5. Usuńmy z okna przycisk i zamiast niego wstawmy utworzoną na jego wzór
kontrolkę. Ustawmy jej szerokość i wysokość:
<local:PrzyciskUMK HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100"/>

Nawet po usunięciu siatki przycisk nie jest całą kontrolką, lecz tylko jest w niej za-
warty. To oznacza, że własności i zdarzenia kontrolki nie są własnościami i zdarze-
niami przycisku. Nie będziemy więc mogli łatwo zmienić „z zewnątrz” na przykład
pędzli użytych do rysowania przycisku. Dalszy rozwój kontrolki, który by to umożliwił,
oznacza konieczność edycji kodu C#. Możemy użyć do tego zdarzeń i code-behind
kontrolki (plik PrzyciskUMK.xaml.cs został dołączony do projektu podczas definiowania
kontrolki) lub wzorca MVVM omówionego w pierwszej części książki.
Rozdział 11.
Style
Kod XAML, podobnie jak kod C#, powinien być pisany zgodnie z ogólnie przyjętymi
dobrymi praktykami. Jedną z takich powszechnie uznanych praktyk jest unikanie po-
wielania kodu, czyli reguła DRY (od ang. don’t repeat yourself). Załóżmy dla przy-
kładu, że w oknie znajduje się nie jeden przycisk, ale zestaw kilku kontrolek. Zmiana
tła każdego z nich w sposób opisany w poprzednim rozdziale ― a więc tworzenie za-
gnieżdżonego elementu pędzla, który w każdym przypadku jest taki sam ― oznaczałaby
wielokrotne powielenie sporej ilości kodu. Aby tego uniknąć, możemy zdefiniować je-
den pędzel, umieścić go w zasobach i wykorzystać w poszczególnych kontrolkach.

Dla osób mających doświadczenie w projektowaniu stron WWW pożyteczne może


być porównanie stylów dostępnych w języku XAML do kaskadowych arkuszy stylów,
których można używać w HTML. Oczywiście można określić wygląd każdego elementu
strony w obrębie opisującego go znacznika HTML. Znacznie wygodniej jest jednak
zrobić to, „centralnie” korzystając z CSS. Ułatwia to kontrolowanie wyglądu jednej
lub wielu stron oraz wprowadzanie w nim zmian. Te same zalety mają style w XAML,
ale ich rola nie ogranicza się tylko do wyglądu.

Siatka i wiele kontrolek


Zacznijmy od umieszczenia w oknie kilku kontrolek, kontrolując ich położenie za
pomocą siatki. Siatkę podzielimy na dwanaście komórek w trzech wierszach. W tym
celu należy wypełnić kolekcje ColumnDefinitions i RowDefinitions — własności
elementu Grid. Kolumny podzielmy tak, żeby pierwsza z nich miała stałą szerokość
250 pikseli, a pozostałe dzieliły się w stosunku 2:1:1. Wiersze natomiast niech będą
równej wysokości. Realizujący to kod widoczny jest na listingu 11.1 (por. też rysunek
11.1). Kontrolki mogą wybierać komórki siatki, w której zostają umieszczone za pomo-
cą własności Grid.Column i Grid.Row, które są poznanymi w rozdziale 7. własnościami
doczepianymi. Domyślnie wartości obu tych własności są równe 0, co oznacza, że
kontrolki domyślnie umieszczane są w komórce znajdującej się w lewym górnym rogu.
126 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

1. Do kodu XAML dodajemy element Window.Resources, a w nim dwa elementy


LinearGradientBrush wyposażone w atrybuty x:Key (jak na listingu 11.3).

Listing 11.3. Definiowanie gradientów w zasobach okna


<Window x:Class="XamlWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<LinearGradientBrush x:Key="NiebieskiGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FioletowyGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>
</Window.Resources>
<Grid>
...
<Button x:Name="button" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100"
Foreground="White"
Background="{StaticResource NiebieskiGradient}">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
<Button.Foreground>White</Button.Foreground>
<Button.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
</Button.Background>
</Button>
<TextBox Grid.Column="0" Grid.Row="1" Text="Pole edycyjne" Margin="10,10,10,10"
Foreground="White"
Background="{StaticResource NiebieskiGradient}" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="Etykieta"
Margin="10,10,10,10"
Foreground="White"
Background="{StaticResource NiebieskiGradient}" />
Rozdział 11.  Style 127

<Button Grid.Column="1" Grid.Row="0" Content="Przycisk"


Margin="10,10,10,10"
Foreground="White"
Background="{StaticResource NiebieskiGradient}" />
<Button Grid.Column="1" Grid.Row="1" Content="Przycisk"
Margin="10,10,10,10"
Foreground="White"
Background="{StaticResource NiebieskiGradient}" />
<Rectangle Grid.Column="1" Grid.Row="2" Margin="10,10,10,10"
Fill="{StaticResource NiebieskiGradient}" />
</Grid>
</Window>

2. Obiekty te można wykorzystać do określenia wyglądu kontrolek. Można ich


użyć do ustalenia wartości wszystkich atrybutów typu Brush. Przykłady są
widoczne na listingu 11.3.

Przycisk, którym bawiliśmy się do tej pory, zachował swój wygląd. Dodatkowo upodob-
niliśmy do niego nowe kontrolki. Wyobraźmy sobie, o ile dłuższy, pełen powtórzeń
i trudniejszy do konserwacji byłby kod, w którym w każdym z elementów opisujących
kontrolki definiowalibyśmy pędzle umieszczane w zagnieżdżonych podelementach.

Style
Zróbmy następny krok. Zamiast przechowywać pojedyncze obiekty, których wielo-
krotnie używamy do określenia wyglądu kontrolek, możemy zebrać je razem w stylu
i ten styl przypisywać kontrolkom. W ten sposób możemy określić cały ich wygląd.
Zresztą nie tylko wygląd, style mogą przypisywać wartości dowolnym atrybutom
kontrolek.

Zacznijmy od prostego przykładu stylu (zob. listing 11.4), który określa jedynie kolory
czcionki i tła kontrolki, a więc atrybuty Foreground i Background. Nazwijmy go Styl
Niebieski. Styl przeznaczony jest dla wszystkich kontrolek dziedziczących z klasy
Control. Do nich należą przyciski i pola edycyjne, ale nie Rectangle i TextBlock.
Wspólną klasą dla wszystkich czterech byłby UIElement, lecz on z kolei nie zawiera
definicji własności Foreground i Background, więc jest dla naszych celów bezużyteczny.

Grupę kontrolek, dla których przeznaczony jest styl, wskazujemy za pomocą atrybutu
TargetType. Jego wartością powinna być wspólna klasa bazowa interesujących nas
kontrolek. Sam styl składa się z elementów Setter, w których wskazujemy własność
i wartość, jaką chcemy jej przypisać. Przypisanie stylu kontrolce jest równoważne
z ustawieniem jej atrybutów/własności zgodnie z wartościami wskazanymi w tym stylu.

Definicję stylu należy umieścić w zasobach, w naszym przypadku najlepiej w zasobach


okna, obok wcześniej zdefiniowanych gradientów. Obiekt jednego z gradientów z zaso-
bów jest zresztą w dodawanym stylu wykorzystywany, choć moglibyśmy określić tło
także umieszczając w stylu zagnieżdżony element pędzla. Styl przypisujemy kontrolkom,
używając atrybutu Style. Przykład takiego przypisania również widoczny jest na li-
128 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

stingu 11.4. Ustalając styl, usuwałem z kontrolek atrybuty Foreground i Background,


ponieważ nadpisywałyby wartości wskazane w stylu.

Listing 11.4. Definiowanie stylu określającego kolory kontrolek


<Window x:Class="XamlWPF.MainWindow"
...
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<LinearGradientBrush x:Key="NiebieskiGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FioletowyGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>
<Style x:Key="StylNiebieski" TargetType="Control" >
<Setter Property="Foreground" Value="White" />
<Setter Property="Background"
Value="{StaticResource NiebieskiGradient}" />
</Style>
</Window.Resources>
<Grid>
...
<Button x:Name="button" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100"
Foreground="White" Background="{StaticResource NiebieskiGradient}"
Style="{StaticResource StylNiebieski}">
...
</Button>
<TextBox Grid.Column="0" Grid.Row="1" Text="Pole edycyjne" Margin="10,10,10,10"
Foreground="White" Background="{StaticResource NiebieskiGradient}"
Style="{StaticResource StylNiebieski}" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="Etykieta"
Margin="10,10,10,10"
Foreground="White" Background="{StaticResource NiebieskiGradient}" />
<Button Grid.Column="1" Grid.Row="0" Content="Przycisk"
Margin="10,10,10,10"
Foreground="White" Background="{StaticResource NiebieskiGradient}"
Style="{StaticResource StylNiebieski}" />
<Button Grid.Column="1" Grid.Row="1" Content="Przycisk"
Margin="10,10,10,10"
Foreground="White" Background="{StaticResource NiebieskiGradient}"
Style="{StaticResource StylNiebieski}" />
<Rectangle Grid.Column="1" Grid.Row="2" Margin="10,10,10,10"
Fill="{StaticResource NiebieskiGradient}" />
</Grid>
</Window>

Alternatywnie możemy nie wskazywać typu, dla którego przeznaczony jest styl, a w za-
mian określać osobno, dla jakiego typu przeznaczone są poszczególne elementy Setter.
Wówczas można w jednym stylu uzgodnić wygląd kontrolek różnych typów, jak w po-
niższym przykładzie:
Rozdział 11.  Style 129

<Style x:Key="StylNiebieski">
<Setter Property="Control.Foreground" Value="White" />
<Setter Property="Control.Background" Value="{StaticResource
NiebieskiGradient}" />
<Setter Property="TextBlock.Foreground" Value="White" />
<Setter Property="TextBlock.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="Shape.Fill" Value="{StaticResource NiebieskiGradient}" />
</Style>

Tak zdefiniowany styl możemy przypisać nie tylko przyciskom i innym kontrolkom
dziedziczącym z klasy Control, ale także do kontrolek TextBlock i Rectangle, które
z tej klasy nie dziedziczą.

Wyzwalacze
W stylu oprócz elementów Setter ustalających wartości własności kontrolek („usta-
wiacze”) możemy także zdefiniować wyzwalacze. Określają one styl kontrolki w spe-
cjalnych sytuacjach, które rozpoznajemy za pomocą własności tej kontrolki. Na przy-
kład możemy uaktywnić część stylu dopiero w momencie, gdy własność IsMouseOver
równa jest True, a więc gdy nad kontrolką znajduje się kursor myszy. Inny wyzwalacz
może działać, kiedy kontrolka ma określony rozmiar (własności Width i Height) lub
kiedy jest wyłączona (własność IsEnabled). My dodamy do stylu StylNiebieski wy-
zwalacz zmieniający kolor wypełnienia w momencie najechania kursorem myszy. Poka-
zuje to listing 11.5. Widać na nim, że wyzwalacze umieszczane są w kolekcji Triggers,
w której każdy wyzwalacz odpowiada jednej własności z określoną wartością. W wy-
zwalaczu znajdują się natomiast elementy Setter — takie same jak w stylu, ale
oczywiście przypisujące inne wartości.

Listing 11.5. Dzięki wyzwalaczom styl nabiera dynamicznego charakteru


<Style x:Key="StylNiebieski">
<Setter Property="Control.Foreground" Value="White" />
<Setter Property="Control.Background" Value="{StaticResource
NiebieskiGradient}" />
<Setter Property="TextBlock.Foreground" Value="White" />
<Setter Property="TextBlock.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="Shape.Fill" Value="{StaticResource NiebieskiGradient}" />
<Style.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Control.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="TextBlock.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Shape.Fill"
Value="{StaticResource FioletowyGradient}" />
</Trigger>
</Style.Triggers>
</Style>
Rozdział 11.  Style 131

<GradientStop Color="Blue" Offset="0.0" />


<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>
<Style x:Key="StylNiebieski">
<Setter Property="Control.Foreground" Value="White" />
<Setter Property="Control.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="TextBlock.Foreground" Value="White" />
<Setter Property="TextBlock.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="Shape.Fill" Value="{StaticResource NiebieskiGradient}" />
<Style.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Control.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="TextBlock.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Shape.Fill"
Value="{StaticResource FioletowyGradient}" />
</Trigger>
</Style.Triggers>
</Style>
</Application.Resources>
</Application>

Wygodne jest zdefiniowane w zasobach globalnych stylów, które opisałem przed


chwilą, a więc takich, które nie mają nazwy, a za to mają określony typ kontrolek, dla
których są przeznaczone (atrybut TargetType). Dzięki temu style te są automatycznie
stosowane do wszystkich kontrolek wskazanego typu w całej aplikacji bez konieczności
zaznaczania tego w ich elementach XAML! Przekonajmy się o tym, definiując w zaso-
bach aplikacji styl przeznaczony dla przycisków (listing 11.7, plik App.xaml). Jedno-
cześnie usuńmy wszystkie atrybuty Style z kodu XAML z pliku MainWindow.xaml
(listing 11.8).

Listing 11.7. Styl w zasobach globalnych, który będzie automatycznie stosowany we wszystkich
przyciskach aplikacji
<Application x:Class="XamlWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XamlWPF"
StartupUri="MainWindow.xaml">
<Application.Resources>
<LinearGradientBrush x:Key="NiebieskiGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FioletowyGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>
<Style TargetType="Button">
<Setter Property="Foreground" Value="White" />
132 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

<Setter Property="Background" Value="{StaticResource NiebieskiGradient}" />


<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background"
Value="{StaticResource FioletowyGradient}" />
</Trigger>
</Style.Triggers>
</Style>
</Application.Resources>
</Application>

Listing 11.8. Uproszczony kod XAML okna aplikacji bez jawnego odwoływania do stylów
<Window x:Class="XamlWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Button x:Name="button" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
</Button>
<TextBox Grid.Column="0" Grid.Row="1" Text="Pole edycyjne"
Margin="10,10,10,10" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="Etykieta" Margin="10,10,10,10" />
<Button Grid.Column="1" Grid.Row="0" Content="Przycisk" Margin="10,10,10,10" />
<Button Grid.Column="1" Grid.Row="1" Content="Przycisk" Margin="10,10,10,10" />
<Rectangle Grid.Column="1" Grid.Row="2" Margin="10,10,10,10" />
</Grid>
</Window>
Rozdział 12.
Transformacje i animacje

Transformacje kompozycji
i renderowania
Przycisk, który projektowaliśmy w poprzednim rozdziale, w tym będzie nadal mody-
fikowany. Modyfikacje nie będą jednak dotyczyły wyglądu przycisku, lecz jego poło-
żenia. Rozpocznijmy od czegoś prostego, a jednocześnie efektownego. Przyjrzymy się
własnościom LayoutTransform i RenderTransform, które umożliwiają między innymi
przesunięcie i obrócenie kontrolki. Pierwsza zdefiniowana została w klasie bazowej
FrameworkElement, a druga w UIElement. Pierwsza wpływa na położenia innych kon-
trolek, jeżeli ich pozycja zależy od wielkości obracanej kontrolki, podczas gdy druga
potrafi obracać się nad lub pod innymi kontrolkami. Transformacja obrotów w Layout
Transform potrafi kręcić pociskiem tylko wokół jego środka, natomiast ta przypisana
do RenderTransform może obracać go wokół dowolnego punktu. Z analogicznych powo-
dów translacja (przesunięcie) będzie działać tylko wówczas, gdy przypiszemy ją do Render
Transform. Niestety, trzeba za to zapłacić nieco większym obciążeniem procesora.

Zacznijmy od prostego przykładu: obróćmy o 45° przyciski znajdujące się w drugiej ko-
lumnie siatki (w projekcie omawianym w poprzednim rozdziale). W przypadku pierw-
szego przycisku użyjmy transformacji kompozycji (LayoutTransform), a w przypadku
drugiego — transformacji renderowania (RenderTransform). Realizujący to kod widocz-
ny jest na listingu 12.1, a efekt, jaki powoduje — na rysunku 12.1.

Listing 12.1. Przyciski z transformacjami


<Button Grid.Column="1" Grid.Row="0" Content="Przycisk" Margin="10,10,10,10"
Style="{StaticResource StylNiebieski}">
<Button.LayoutTransform>
<RotateTransform CenterX="0" CenterY="0" Angle="45" />
</Button.LayoutTransform>
</Button>
<Button Grid.Column="1" Grid.Row="1" Content="Przycisk" Margin="10,10,10,10"
Style="{StaticResource StylNiebieski}">
Rozdział 12.  Transformacje i animacje 137

Jak widać na rysunku 12.4, co już zapowiedziałem wyżej, przesunięcie jest niesku-
teczne w przypadku transformacji kompozycji. Działa tylko jako transformacja rende-
rowania. Transformacja kompozycji realizowana jest przed ułożeniem kontrolek, któ-
re całkowicie niweluje jej skutki. W przypadku transformacji skalowania, w której
możemy określić powiększenie osobno w każdym wymiarze (rysunek 12.5)
<ScaleTransform ScaleX="1.5" ScaleY="0.5" />

i pochylenia, w którym możemy ustalić, jak pochylone są pary równoległych krawę-


dzi kontrolki (rysunek 12.6)
<SkewTransform AngleX="45" AngleY="0" />

typ transformacji wpływa na to, czy inne kontrolki są przesuwane tak, żeby odległość
między nimi pozostawała niezmieniona, jak również czy przesunięta zostaje prze-
kształcana komórka.

Rysunek 12.4.
Przykład translacji
138 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Rysunek 12.5.
Przykład
transformacji
skalowania

Rysunek 12.6.
Przykład
transformacji
pochylenia
Rozdział 12.  Transformacje i animacje 139

Rysunek 12.6.
— ciąg dalszy

Możliwe jest grupowanie transformacji:


<Button Width="100" Height="50" Margin="0,0,0,10"
Style="{StaticResource StylNiebieski}">
<Button.RenderTransform>
<TransformGroup>
<RotateTransform Angle="45" />
<TranslateTransform X="50" Y="-25" />
</TransformGroup>
</Button.RenderTransform>
</Button>

Należy jednak pamiętać, że kolejność transformacji ma znaczenie. Łatwo to sobie


uzmysłowić, kładąc swój smartphone na stole, a następnie obracając go i przesuwając
w obróconym kierunku. Następnie połóżmy go tak jak leżał na początku i wykonajmy
te dwa przekształcenia w odwróconej kolejności: wpierw przesuńmy w pierwotnym
kierunku, a dopiero potem obróćmy. Okaże się, że smartphone, choć zorientowany tak
samo, znajduje się w innym miejscu (por. rysunek 12.7).

Rysunek 12.7.
Przycisk po
wykonaniu dwóch
transformacji
w różnej kolejności
Rozdział 12.  Transformacje i animacje 141

Wprowadźmy podobne rozwiązanie w aplikacji WPF, korzystając z transformacji


ustalanych w wyzwalaczach stylów.
1. Usuńmy polecenia transformacji (element Button.LayoutTransform
lub Button.RenderTransform) z elementów umieszczonych w oknie.
2. Do elementu definiującego styl, a raczej do wyzwalacza związanego
z najechaniem kursora myszy na przycisk, dodajmy element Setter
zmieniający własność LayoutTransform (listing 12.3).

Listing 12.3. Dodajemy wyzwalacz uruchamiający transformację przycisku


<Window x:Class="XamlWPF.MainWindow"
...
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
...
<Style x:Key="StylNiebieski">
<Setter Property="Control.Foreground" Value="White" />
<Setter Property="Control.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="TextBlock.Foreground" Value="White" />
<Setter Property="TextBlock.Background"
Value="{StaticResource NiebieskiGradient}" />
<Setter Property="Shape.Fill"
Value="{StaticResource NiebieskiGradient}" />
<Style.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Control.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="TextBlock.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Shape.Fill"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Control.LayoutTransform">
<Setter.Value>
<ScaleTransform ScaleX="1.1" ScaleY="1.1" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
<Button x:Name="button" Margin="10,10,10,10"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100"
Style="{StaticResource StylNiebieski}">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
Rozdział 12.  Transformacje i animacje 145

<Setter Property="Control.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="TextBlock.Background"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Shape.Fill"
Value="{StaticResource FioletowyGradient}" />
<Setter Property="Control.LayoutTransform">
<Setter.Value>
<ScaleTransform ScaleX="1.0" ScaleY="1.0" />
</Setter.Value>
</Setter>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty =
"LayoutTransform.(ScaleTransform.ScaleX)"
Duration="0:0:2"
From="1" To="1.2"
AutoReverse="True" RepeatBehavior="Forever" />
<DoubleAnimation
Storyboard.TargetProperty =
"LayoutTransform.(ScaleTransform.ScaleY)"
Duration="0:0:2"
From="1" To="1.2"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
<Button x:Name="button" Margin="10,10,10,10"
...
</StackPanel>
</Window>

Funkcje w animacji
Dodajmy do jednego z przycisków transformację translacji, w której zastosujemy inną
niż liniową funkcję przejścia od wartości początkowej do końcowej. Można wybrać
jedną z wielu funkcji, my użyjemy funkcji sinus dla współrzędnej X, przez co przycisk
będzie oscylował w poziomie. Oprócz sinusa dostępne są także funkcje będące róż-
nymi potęgami czasu, funkcja imitująca oscylacje tłumione itp. (zob. tabela 12.1). Dla
spójności przykładu cały zaangażowany w animację kod XAML zamkniemy w elemen-
cie jednego przycisku (listing 12.5).
146 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Listing 12.5. Przycisk z animacją korzystającą z funkcji wygładzania


<Button Width="100" Height="50" Margin="0,0,0,10"
Style="{StaticResource StylNiebieski}">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="translacjaPrzycisku"
Storyboard.TargetProperty="X"
From="-20" To="20"
BeginTime="0:0:0" Duration="0:0:2"
AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimation.EasingFunction>
<SineEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
<Button.RenderTransform>
<TranslateTransform x:Name="translacjaPrzycisku" X="0" Y="0" />
</Button.RenderTransform>
</Button>

Tabela 12.1. Dostępne funkcje wygładzania


Nazwa funkcji wygładzania Opis
BackEase Zanim animacja się rozpocznie, wartość jest zmieniana w przeciwną
stronę (zamach zgodny z tradycyjnymi regułami animacji Disneya).
BounceEase Nadaje efekt sprężystości.
CircleEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją trygonometryczną
czasu.
CubicEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją czasu podniesionego
do trzeciej potęgi.
ElasticEase Tworzy animację, która przypomina oscylującą sprężynę tłumioną
aż do zatrzymania.
ExponentialEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją wykładniczą czasu.
PowerEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją czasu podniesionego
do potęgi wyznaczonej przez własność Property.
QuadraticEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją czasu podniesionego
do drugiej potęgi.
QuarticEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją czasu podniesionego
do czwartej potęgi.
Rozdział 12.  Transformacje i animacje 147

Tabela 12.1. Dostępne funkcje wygładzania — ciąg dalszy


Nazwa funkcji wygładzania Opis
QuinticEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją czasu podniesionego
do piątej potęgi.
SineEase Zmienia wartość ze zmienną prędkością. Prędkość rośnie na początku
i zmniejsza się na końcu ruchu zgodnie z funkcją sinus czasu.

Po uruchomieniu aplikacji przycisk oscyluje w poziomie (składowa pozioma położenia


zadana jest funkcją sinus). Dodatkowo jeżeli do elementu Storyboard dodamy animację
własności Y transformacji, w której użyjemy funkcji ElasticEase, to uzyskamy oscylacje
podobne jak dla rozkołysanego oscylatora:
<DoubleAnimation Storyboard.TargetName="translacjaPrzycisku"
Storyboard.TargetProperty="Y"
From="0" To="10"
BeginTime="0:0:0" Duration="0:0:15"
AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimation.EasingFunction>
<ElasticEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>

Animacja koloru
Ponieważ do tej pory używałem jedynie animacji typu DoubleAnimation, warto poka-
zać choćby jeden przykład animacji innego typu, na przykład ColorAnimation. Przy-
cisk opisany poniższym elementem XAML będzie zmieniał kolor od niebieskiego do
czerwonego, a ściślej niebiesko-granatowy gradient będzie przechodził w fioletowo-
czerwony (listing 12.6). Aby skomplikować nieco sprawę, lewy kolor gradientu będzie
zmieniał się z inną częstością niż prawy.

Listing 12.6. Animacja kolorów tła przycisku


<Button Width="100" Height="50" Margin="0,0,0,10"
Style="{StaticResource StylNiebieski}" >
<Button.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Offset="0" Color="Blue" x:Name="kolorPoczątku" />
<GradientStop Offset="1" Color="Navy" x:Name="kolorKońca" />
</LinearGradientBrush>
</Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName="kolorPoczątku"
Storyboard.TargetProperty="Color"
From="Blue" To="BlueViolet"
148 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

BeginTime="0:0:0" Duration="0:0:1"
AutoReverse="True" RepeatBehavior="Forever">
</ColorAnimation>
<ColorAnimation
Storyboard.TargetName="kolorKońca"
Storyboard.TargetProperty="Color"
From="Navy" To="DarkRed"
BeginTime="0:0:0" Duration="0:0:2"
AutoReverse="True" RepeatBehavior="Forever">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>

Warto choć wspomnieć, że oprócz animacji przedstawionej w tym rozdziale XAML


umożliwia także animację z użyciem ramek kluczowych (keyframes).

W kontekście animacji warto też wspomnieć o Blendzie, wygodnym środowisku


służącym do tworzenia warstwy widoku, które zawiera narzędzia wizualne ułatwia-
jące tworzenie animacji, zarówno oparte na scenorysach, jak i na ramkach kluczo-
wych. Środowisko Blend jest dostarczane razem z Visual Studio.
Rozdział 13.
Szablony kontrolek
Usuńmy z kodu XAML naszej przykładowej aplikacji wszystkie omówione w po-
przednich rozdziałach formatowania, pozostawiając w oknie cztery proste przyciski,
a w zasobach (nieużywane) pędzle z liniową zmianą koloru (listing 13.1).

Listing 13.1. Kod XAML okna oczyszczony z animacji


<Window x:Class="XamlWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<LinearGradientBrush x:Key="NiebieskiGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FioletowyGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>
</Window.Resources>
<StackPanel Orientation="Vertical">
<Button x:Name="button" Margin="10,10,10,10"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100" >
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16" Foreground="White">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
</Button>
150 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

<Button Content="Przycisk" Foreground="White" Width="100" Height="50"


Margin="0,0,0,10" />
<Button Content="Przycisk" Foreground="White" Width="100" Height="50"
Margin="0,0,0,10" />
<Button Content="Przycisk" Foreground="White" Width="100" Height="50"
Margin="0,0,0,10" />
</StackPanel>
</Window>

Następnie w zasobach okna zdefiniujmy szablon, który niejako podsumowuje wiele


z poznanych w drugiej części książki wiadomości dotyczących XAML. Podczas projek-
towania szablonu programista może przejąć całkowitą kontrolę nad wyglądem i za-
chowaniem kontrolki, włączając w to reakcje na działania użytkownika i animacje wy-
zwalane zdarzeniami lub zmianą stanu kontrolki. Listing 13.2 pokazuje zatem pełen
kod pliku MainWindow.xaml, w którym zdefiniowany został szablon o nazwie Szablon
Niebieski przeznaczony dla przycisków. Definiuje on zaokrąglony prostokąt, który
pełnić będzie rolę tła przycisku. Do określenia jego kolorów korzystamy z gradientów
NiebieskiGradient i FioletowyGradient zdefiniowanych w poprzednich rozdziałach.
Ponadto z najechaniem kursorem myszy na kontrolkę związana została animowana
zmiana koloru tła, zmiana stopnia jasności oraz obrót przycisku.

Listing 13.2. Wyróżniony został kod szablonu i sposób jego przypisania do przycisku
<Window x:Class="XamlWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XamlWPF"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<LinearGradientBrush x:Key="NiebieskiGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="Navy" Offset="1.0" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FioletowyGradient"
StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="Blue" Offset="0.0" />
<GradientStop Color="BlueViolet" Offset="1.0" />
</LinearGradientBrush>

<ControlTemplate x:Key="SzablonNiebieski" TargetType="{x:Type Button}">


<Grid>
<Rectangle x:Name="tło" RadiusX="15" RadiusY="15"
Fill="{StaticResource NiebieskiGradient}">
<Rectangle.BitmapEffect>
<OuterGlowBitmapEffect GlowColor="black" GlowSize="5"/>
</Rectangle.BitmapEffect>
<Rectangle.LayoutTransform>
<RotateTransform Angle="0" x:Name="obrót" />
</Rectangle.LayoutTransform>
</Rectangle>
<Viewbox>
Rozdział 13.  Szablony kontrolek 151

<ContentPresenter Margin="10,10,10,10" />


</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="tło" Property="Fill"
Value="{StaticResource FioletowyGradient}" />
<Setter TargetName="tło" Property="BitmapEffect">
<Setter.Value>
<OuterGlowBitmapEffect GlowColor="Blue" GlowSize="5"/>
</Setter.Value>
</Setter>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="obrót"
Storyboard.TargetProperty = "Angle"
Duration="0:0:2"
From="0" To="90" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<StackPanel Orientation="Vertical">
<Button x:Name="button" Margin="10,10,10,10"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Height="100" Template="{StaticResource SzablonNiebieski}">
<StackPanel Orientation="Horizontal">
<Image Width ="70" Height="70" Source="logo.gif" />
<Rectangle Width="10" />
<TextBlock FontSize="16" Foreground="White">
<Run Foreground="Yellow">Uniwersystet</Run><LineBreak/>
Mikołaja<LineBreak/>
Kopernika
</TextBlock>
</StackPanel>
</Button>
<Button Content="Przycisk" Foreground="White" Width="100" Height="50"
Margin="0,0,0,10" Template="{StaticResource SzablonNiebieski}" />
<Button Content="Przycisk" Foreground="White" Width="100" Height="50"
Margin="0,0,0,10" Template="{StaticResource SzablonNiebieski}" />
<Button Content="Przycisk" Foreground="White" Width="100" Height="50"
Margin="0,0,0,10" Template="{StaticResource SzablonNiebieski}" />
</StackPanel>
</Window>

Warto zaznaczyć różnice między szablonem kontrolki a stylem. Styl służy do ustale-
nia własności kontrolek i ich zmiany w pewnych sytuacjach (służą do tego wyzwala-
cze). Szablon umożliwia natomiast zbudowanie kontrolki określonego typu całkowite
od nowa, określa nawet sposób, w jaki jest rysowana. Zwróćmy uwagę, że szablon
może być jedną z własności ustalanych w stylu.
152 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Czym natomiast różni się szablon kontrolki od kontrolki użytkownika, której przykład
stworzyliśmy w rozdziale 10.? Jak wspomniałem wyżej, szablon pozwala określić, jak
ma kontrolka wyglądać, to znaczy jak ma być narysowana. Z kolei tworzenie własnej
kontrolki polega raczej na budowaniu nowej złożonej kontrolki z gotowych klocków
(istniejących kontrolek), aby ułatwić jej wielokrotne używanie. Zatem jeżeli chcemy,
żeby nasza kontrolka wyglądała nietypowo, to myślimy o szablonie kontrolki, a jeżeli
chcemy, żeby kontrolka spełniała nietypowe zadania, budujemy własną kontrolkę (user
control).

Zastosowany w powyższym szablonie obrót samego prostokąta pełniącego rolę tła


(bez obracania zawartości przycisku, rysunek 13.1) jest dość efektowną, ale może
niezbyt typową reakcją przycisku. Jeżeli chcielibyśmy obracać cały przycisk, należa-
łoby przypisać transformację obrotu nie do prostokąta, lecz do elementu Grid, który
obejmuje wszystkie elementy szablonu, i być może użyć transformacji renderowania
zamiast transformacji kompozycji. Odpowiednie zmiany w kodzie szablonu pokazuje
listing 13.3.

Listing 13.3. Obrót całego przycisku


<ControlTemplate x:Key="SzablonNiebieski" TargetType="{x:Type Button}">
<Grid>
<Grid.RenderTransform>
<RotateTransform Angle="0" x:Name="obrót" />
</Grid.RenderTransform>
...

Rysunek 13.1.
Obracające się
tło przycisków
Rozdział 14.
Zdarzenia trasowane
(routed events)
Zdarzenia, czy to w Windows Forms, czy w WPF, opisują niemal wszystkie możliwe
czynności i zmiany stanu kontrolek. Gdy klikniemy przycisk, uruchamiana jest metoda
przechowywana w kolekcji Button.Click zawierającej referencje do metod. W Windows
Forms, jeżeli przycisk znajduje się w jakimś pojemniku, na przykład panelu, kliknięcie
tego przycisku aktywuje zdarzenie tylko tego przycisku. Nie ma w ten sposób prostej
możliwości, aby panel zareagował, chyba że sami zaprogramujemy to w metodzie zda-
rzeniowej przycisku. W WPF, w którym zagnieżdżanie kontrolek jest na porządku
dziennym, potrzebny był mechanizm uruchamiania metod zdarzeniowych nie tylko
kontrolki, która bezpośrednio była zaangażowana w zdarzenie, ale również całej „dra-
binki” jej rodziców. Tę rolę pełnią zdarzenia trasowane (ang. routed events).

Pojedyncza kontrolka
Zacznijmy od pojedynczego przycisku. Stwórzmy nowy projekt aplikacji WPF i umie-
śćmy na nim przycisk. Z przyciskiem zwiążmy metodę zdarzeniową (klikając go w wi-
doku projektowania albo dodając atrybut Click do kodu XAML). Kod XAML okna
z wielkim przyciskiem pokazuje listing 14.1. W listingu 14.2 widoczny jest natomiast
kod klasy MainWindow wskazanej w elemencie okna, czyli tak zwany code-behind, w któ-
rej zdefiniowana jest metoda zdarzeniowa button_Click. Jej jedyne polecenie zmienia
etykietę nadawcy zdarzenia (czyli samego przycisku), wyświetlając napis „Zdarzenie
trasowane”. Uruchommy aplikację, żeby się przekonać, czy kliknięcie przycisku zmienia
jego etykietę.

Listing 14.1. Kod XAML okna


<Window x:Class="ZdarzeniaTrasowane.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
154 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

xmlns:local="clr-namespace:ZdarzeniaTrasowane"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Przycisk"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
Width="300" Height="300"
Click="button_Click" />
</Grid>
</Window>

Listing 14.2. Metoda zdarzeniowa w code-behind zmieniająca etykietę przycisku


using System.Windows;
using System.Windows.Controls;

namespace ZdarzeniaTrasowane
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void button_Click(object sender, RoutedEventArgs e)


{
(sender as Button).Content = "Zdarzenia trasowane";
}
}
}

Zwróćmy uwagę na drugi argument metody zdarzeniowej. Zamiast „pustego” System.


EventArgs jest to System.Windows.RoutedEventArgs, w którym dostępne jest kilka
ciekawych własności. Obiekt ten przekazuje między innymi informacje o źródle, czyli
o kontrolce, która wywołała zdarzenie, a w ogólności nie musi być bezpośrednim
„nadawcą” zdarzenia.

Zagnieżdżenie w elemencie przycisku kolejnego przycisku lub etykiety (TextBlock),


które nie korzystają ze zdarzenia Click, nie zmienia sposobu działania zdarzenia. Warto
to przetestować, modyfikując kod przycisku zgodnie ze wzorem z listingu 14.3.

Listing 14.3. Zagnieżdżanie elementów


<Button x:Name="button" Content="Przycisk"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
Width="300" Height="300"
Click="button_Click" />
<TextBlock Text="Przycisk" />
</Button>
Rozdział 14.  Zdarzenia trasowane (routed events) 155

Zagnieżdżanie przycisków
Następnie zastąpmy etykietę przyciskiem, z którego zdarzeniem Click także zwiążemy
istniejącą już metodę zdarzeniową (listing 14.4).

Listing 14.4. Zagnieżdżone przyciski


<Button x:Name="button"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
Width="300" Height="300"
Click="button_Click">
<Button x:Name="button1"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="0,0,0,0"
Width="250" Height="250"
Click="button_Click" />
</Button>

Ponownie uruchommy aplikację i sprawdźmy, co się stanie, gdy teraz klikniemy przy-
cisk. Bez względu na to, który przycisk klikniemy, przycisk wewnętrzny zostanie za-
mknięty, a w zamian pojawi się napis „Zdarzenie trasowane”.

Aby to bliżej zbadać, dodajmy jeszcze jeden przycisk (listing 14.5). Okaże się, że tak-
że teraz bez względu na to, który przycisk klikniemy, efekt będzie taki sam. Pozostanie
tylko jeden przycisk z etykietą „Zdarzenie trasowane”. Będzie tak również, gdy z dwóch
zagnieżdżonych przycisków usuniemy wiązanie metody button_Click ze zdarzeniem.

Listing 14.5. Trzy zagnieżdżone przyciski


<Button x:Name="button"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
Width="300" Height="300"
Click="button_Click">
<Button x:Name="button1"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="0,0,0,0"
Width="250" Height="250"
Click="button_Click">
<Button x:Name="button2"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="0,0,0,0"
Width="200" Height="200"
Click="button_Click" />
</Button>
</Button>

Zmieńmy teraz metodę zdarzeniową tak, żeby jej działanie ograniczało się do zmiany
koloru przycisku (listing 14.6). Okaże się, że jeżeli klikniemy przycisk wewnętrzny,
to kolor zmienią wszystkie trzy przyciski (zobaczymy to dopiero, kiedy myszka wyjdzie
poza ich obręb). Gdy klikniemy pośredni przycisk, kolor zmienią tylko dwa, a gdy
156 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Listing 14.6. Kliknięcie będzie zmieniało nie etykietę, lecz kolor tła przycisku
private void button_Click(object sender, RoutedEventArgs e)
{
(sender as Button).Content = "Zdarzenia trasowane";
(sender as Button).Background = new SolidColorBrush(Colors.Yellow);
}

zewnętrzny — tylko on jeden. To ewidentnie oznacza, że zdarzenia dotyczą nie tylko


przycisku, który bezpośrednio klikamy, ale również jego rodziców. To oczywiście
pod warunkiem, że zdarzenie Click przycisków-rodziców wskazuje na metodę zda-
rzeniową. Jeżeli z elementu XAML opisującego pośredni przycisk usuniemy atrybut
Click — ten przycisk nie zmieni koloru po kliknięciu wewnętrznego przycisku. Ale
jeśli klikniemy ów pośredni przycisk, choć nie ma związanego zdarzenia, zmieni się
kolor przycisku zewnętrznego, który takie zdarzenie ma wśród swoich atrybutów.
Zatem zdarzenie jest przekazywane nawet wówczas, gdy kontrolka, dla której wystą-
piło ono po raz pierwszy, wcale nie ma w znaczniku atrybutu Click.

A czemu wcześniej przyciski znikały? Metoda zdarzeniowa zmieniała po prostu za-


wartość (ang. content) kolejnych przycisków w hierarchii, zastępując je etykietami
TextBlock. Na ekranie widzieliśmy jednak tylko efekt ostatniej zmiany w sekwencji.

Kontrola przepływu
zdarzeń trasowanych
Dodajmy teraz do okna pole opcji (CheckBox) i listę (ListBox), które pozwolą nam do-
kładniej obserwować kolejne wywołania metody zdarzeniowej (listing 14.7). Zacznijmy
od listy, w której będziemy umieszczać informacje o kolejnych wywołaniach metody
button_Click (listing 14.8) i informacjach przekazywanych w zdarzeniu trasowanym
(rysunek 14.1). Zdefiniujmy również licznik — pole typu int ― który pomoże sprawdzić,
ile razy metoda zdarzeniowa została wywołana.

Listing 14.7. Dodajemy listę i pole opcji


<Window x:Class="ZdarzeniaTrasowane.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ZdarzeniaTrasowane"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="10,10,0,0"
Width="300" Height="300"
Click="button_Click">
Rozdział 14.  Zdarzenia trasowane (routed events) 157

<!--<TextBlock Text="Przycisk" />-->


<Button x:Name="button1"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="0,0,0,0"
Width="250" Height="250"
Click="button_Click">
<Button x:Name="button2"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="0,0,0,0"
Width="200" Height="200"
Click="button_Click" />
</Button>
</Button>
<ListBox x:Name="listBox"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,10,10,0"
Height="200" Width="420" />
<CheckBox x:Name="checkBox" Content="Przerwij"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,220,10,0" />
</Grid>
</Window>

Listing 14.8. Rejestrowanie uruchomień metody zdarzeniowej w liście


private int licznik = 0;

private void button_Click(object sender, RoutedEventArgs e)


{
(sender as Button).Background = new SolidColorBrush(Colors.Yellow);
listBox.Items.Add(String.Format(
"C: licznik=" + licznik.ToString() +
", nadawca: " + (sender as Control).Name +
", źródło: " + (e.Source as Control).Name +
", oryginalne źródło: " + (e.OriginalSource as Control).Name));
licznik++;
}

Rysunek 14.1.
Kolejność
wywoływania metody
zdarzeniowej
po naciśnięciu
najbardziej
zagnieżdżonego
przycisku
158 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Z przykładu widocznego na rysunku 14.1 wynika, że wpierw wywoływana jest meto-


da zdarzeniowa dla najbardziej zagnieżdżonego przycisku (nadawca i źródło to button2),
następnie dla pośredniego i wreszcie dla zewnętrznego. Przy poszczególnych wy-
wołaniach zmienia się nadawca zdarzenia — jest nim zawsze „bieżący” przycisk, ale
informacja o oryginalnym źródle cały czas jest również dostępna.

Przerwanie kolejki
Mechanizm zdarzeń trasowanych daje możliwość przerwania sekwencji wywoływań
metody zdarzeniowej. Wystarczy, że zmienimy własność Handled obiektu Routed
EventArgs na True. Wykorzystajmy do tego pole opcji. Jeżeli będzie zaznaczone,
zmienimy wartość własności Handled, ale dopiero, gdy licznik równy będzie 1 (listing
14.9). W efekcie, jeśli zaznaczymy pole opcji i klikniemy najbardziej zagnieżdżony
przycisk, metoda zdarzeniowa wywołana będzie tylko dwa razy.
Listing 14.9. Podniesienie flagi Handled sygnalizuje, że zdarzenie zostało obsłużone
private void button_Click(object sender, RoutedEventArgs e)
{
(sender as Button).Background = new SolidColorBrush(Colors.Yellow);
listBox.Items.Add(String.Format(
"C: licznik=" + licznik.ToString() +
", nadawca: " + (sender as Control).Name +
", źródło: " + (e.Source as Control).Name +
", oryginalne źródło: " + (e.OriginalSource as Control).Name));
if (checkBox.IsChecked.Value && licznik == 1)
{
e.Handled = true;
licznik = 0;
return;
}
licznik++;
}

Bulgotanie (bubbling)
i tunelowanie (tunneling)
Opisany powyżej proces przekazywania zdarzeń z kontrolki, dla której wystąpiło zda-
rzenie, do pojemników, w których jest umieszczona, nazywa się bąblowaniem (ang.
bubbling) lub, bardziej po polsku, bulgotaniem (przez analogię do pęcherzy powietrza,
które powstają przy dnie naczynia z gotującą się wodą i unoszą się ku powierzchni).
Rozdział 14.  Zdarzenia trasowane (routed events) 159

Bulgotanie nie dotyczy jednak tych zdarzeń, obecnych także w Windows Forms, które
są wywoływane dla pojemników przed wywołaniem właściwego zdarzenia dla umiesz-
czonej w tym pojemniku kontrolki. Typowe przykłady takich zdarzeń to Preview
MouseDown i PreviewKeyDown. W przypadku tych zdarzeń następuje odwrócenie kolejki
— metoda zdarzeniowa najpierw wywoływana jest dla najbardziej „zewnętrznego”
pojemnika, który jest jego nadawcą, a kończy się na kontrolce, która jest zasadniczym
źródeł zdarzenia. To nazywa się tunelowaniem (ang. tunneling), co ma się kojarzyć
z kopaniem tuneli w głąb ziemi (nie z tunelowaniem w mechanice kwantowej).

Skopiujmy zawartość metody button_Click, choć z pewnymi zmianami, do nowej meto-


dy o nazwie button_PreviewMouseDown (listing 14.10). Następnie metodę tę zwiążmy
ze zdarzeniami PreviewMouseDown wszystkich trzech przycisków. W efekcie, po klik-
nięciu najbardziej zagnieżdżonego przycisku, najpierw trzykrotnie wykonana zostanie
metoda button_PreviewMouseDown, przy czym nadawcami będą kolejno button, button1
i button2 (tunelowanie), a potem metoda button_Click z odwróconą kolejnością
nadawców (bulgotanie). Dowodem jest rysunek 14.2 góra. Jeżeli zaznaczone będzie
pole opcji, wykonana zostanie tylko dwa razy metoda button_PreviewMouseDown, co
widać na rysunku 14.2 dół.

Listing 14.10. Tunelowanie


private void button_PreviewMouseDown(object sender,
System.Windows.Input.MouseButtonEventArgs e)
{
(sender as Button).Background = new SolidColorBrush(Colors.Orange);
listBox.Items.Add(String.Format(
"PMD: licznik=" + licznik.ToString() +
", nadawca: " + (sender as Control).Name +
", źródło: " + (e.Source as Control).Name +
", oryginalne źródło: " + (e.OriginalSource as UIElement).ToString()));
if (checkBox.IsChecked.Value && licznik == 1)
{
e.Handled = true;
licznik = 0;
return;
}
licznik++;
}

Na tym przykładzie widać też różnicę między własnościami Source i OriginalSource


w zdarzeniach typu RoutedEventArgs. Jeżeli klikniemy wewnętrzny przycisk, czyli
button2, źródłem w bulgotaniu i tunelowaniu będzie ten właśnie przycisk. Jednak
w przypadku tunelowania, które rozpoczyna się od rodzica i kończy na źródle, oryginal-
nym źródłem będzie najbardziej „zewnętrzny” pojemnik kontrolek, czyli element bez na-
zwy typu ButtonChrome (przycisk z tematu „Chrome” obecny w WPF).
Rozdział 14.  Zdarzenia trasowane (routed events) 161

Listing 14.11. Tworzenie nowych zagnieżdżonych przycisków z poziomu kodu


private void button_Click(object sender, RoutedEventArgs e)
{
Button przycisk = (sender as Button);

Button b = new Button();


b.Content = "Nowy przycisk";
b.HorizontalAlignment = HorizontalAlignment.Left;
b.VerticalAlignment = VerticalAlignment.Top;
double w = przycisk.Width - 20;
double h = przycisk.Height - 20;
if (w > 0 && h > 0)
{
b.Width = w;
b.Height = h;
b.Click += button_Click;
przycisk.Content = b;
}
else przycisk.IsEnabled = false;
b.Click += button_Click;
przycisk.Content = b;

e.Handled =
true;
}
162 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML
Rozdział 15.
Kolekcje w MVVM i XAML
Do tej pory konsekwentnie unikałem tematu kolekcji, kontrolek służących do ich pre-
zentacji w widoku i wiązania kolekcji udostępnianych przez model. Teraz zmierzymy
się z tym zagadnieniem. Zbudujemy w tym celu prostą aplikację, w której model będzie
przechowywał listę zadań. Zadanie będzie zawierało łańcuch opisu, priorytet i zakładaną
datę realizacji. Widok będzie umożliwiał prezentację zadań, oznaczanie tych, które
zostały zrealizowane, oraz ich usuwanie. Zadania niezrealizowane w założonym terminie
będą wyróżniane. Ponadto możliwe będzie dodawanie nowych zadań.

Model
Zacznijmy od utworzenia projektu typu WPF Application, który nazwiemy ZadaniaWPF.
Przygotowywanie kodu zacznijmy od utworzenia modelu. Korzystając z podokna
Solution Explorer w nowo utworzonym projekcie, stwórzmy folder Model, do którego
dodajmy plik z klasą Zadanie. W klasie zdefiniowanych powinno być pięć domyślnie
implementowanych własności publicznych, których wartość może być jednak zmie-
niana tylko z wnętrza tej klasy (listing 15.1). W efekcie do inicjacji obiektu można
użyć jedynie konstruktora. Oprócz tego nadpiszmy metodę ToString w taki sposób, żeby
zwracała łańcuch z informacją o zadaniu. Wykorzystamy ją później, implementując
eksport zadań do pliku tekstowego.

Listing 15.1. Klasa encji opisująca zbiór własności zadania


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ZadaniaWPF.Model
{
public enum PriorytetZadania : byte { MniejWażne, Ważne, Krytyczne };

public class Zadanie


164 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

{
public string Opis { get; private set; }
public DateTime DataUtworzenia { get; private set; }
public DateTime PlanowanyTerminRealizacji { get; private set; }
public PriorytetZadania Priorytet { get; private set; }
public bool CzyZrealizowane { get; set; };

public Zadanie(string opis, DateTime dataUtworzenia,


DateTime planowanyTerminRealizacji, PriorytetZadania priorytetZadania ,
bool czyZrealizowane)
{
this.Opis = opis;
this.DataUtworzenia = dataUtworzenia;
this.PlanowanyTerminRealizacji = planowanyTerminRealizacji;
this.Priorytet = priorytetZadania;
this.CzyZrealizowane = czyZrealizowane;
}

public override string ToString()


{
return Opis + ", priorytet: " + OpisPriorytetu(Priorytet) +
", data utworzenia: " + DataUtworzenia +
", planowany termin realizacji: " +PlanowanyTerminRealizacji.ToString() +
", " + (CzyZrealizowane ? "zrealizowane" : "niezrealizowane");
}
}
}

Wśród własności klasy Zadanie jest własność Priorytet, która może przyjmować
jedną z trzech wartości określonych w typie wyliczeniowym PriorytetZadania (także
widocznym na listingu 15.1). Aby ułatwić obsługę priorytetów (liczb typu byte)
w wyższych warstwach aplikacji, przygotowane są metody służące do jego konwersji na
przyjazny dla człowieka łańcuch i z powrotem. Oczywiście najlepiej byłoby zapisać
służące do tego metody w definicji typu PriorytetZadania. Ale ponieważ typ wyli-
czeniowy nie daje takiej możliwości1, umieścimy te metody w klasie Zadanie. Na li-
stingu 15.2 widoczna jest statyczna metoda OpisPriorytetu przygotowująca łańcuch na
podstawie jednej z trzech wartości priorytetu oraz „odwrotna”, również statyczna metoda
ParsujOpisPriorytetu.

Listing 15.2. Konwersja priorytetu na łańcuch i z powrotem


public static string OpisPriorytetu(PriorytetZadania priorytet)
{
switch (priorytet)
{
case PriorytetZadania.MniejWażne:
return "mniej ważne";
case PriorytetZadania.Ważne:
return "ważne";

1
Metoda generująca opis projektu mogłaby wprawdzie być rozszerzeniem dla typu PriorytetZadania
— wówczas wywoływalibyśmy ją na rzecz konkretnej instancji priorytetu. Jednak w przypadku metody
„odwrotnej” rozszerzana musiałaby być klasa String, co nie wydaje mi się dobrym pomysłem, skoro
jedynie dla trzech wartości zwracałaby sensowne wyniki.
Rozdział 15.  Kolekcje w MVVM i XAML 165

case PriorytetZadania.Krytyczne:
return "krytyczne";
default:
throw new Exception("Nierozpoznany priorytet zadania");
}
}

public static PriorytetZadania ParsujOpisPriorytetu(string opisPriorytetu)


{
switch (opisPriorytetu)
{
case "mniej ważne":
return PriorytetZadania.MniejWażne;
case "ważne":
return PriorytetZadania.Ważne;
case "krytyczne":
return PriorytetZadania.Krytyczne;
default:
throw new Exception("Nierozpoznany opis priorytetu zadania");
}
}

Drugą klasę modelu nazwiemy Zadania. Będzie przechowywała listę zadań w prywat-
nym polu o nazwie listaZadań. Klasa Zadania będzie wyposażona w metody, które po-
zwalają dodawać zadania do tej listy i je z niej usuwać. Możliwe będzie również po-
bieranie referencji do zadania na podstawie jego indeksu w liście. Później dodamy
także możliwość sortowania zadań według terminów realizacji lub priorytetów. Aby
możliwe było wykorzystywanie klasy Zadania w zapytaniach LINQ i w pętlach foreach,
uczynimy ją „prawdziwą” kolekcją. Oznacza to, że będzie implementować interfejs
IEnumerable<Zadanie>.

Zacznijmy od zdefiniowania klasy i podstawowych metod umożliwiających manipu-


lację zbiorem zadań (listing 15.3). Umieśćmy ją w osobnym pliku Zadania.cs, zapisanym
w folderze Model.

Listing 15.3. Pierwsza wersja klasy przechowującej listę zadań


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ZadaniaWPF.Model
{
public class Zadania
{
private List<Zadanie> listaZadań = new List<Zadanie>();

public void DodajZadanie(Zadanie zadanie)


{
listaZadań.Add(zadanie);
}

public bool UsuńZadanie(Zadanie zadanie)


166 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

{
return listaZadań.Remove(zadanie);
}

public int LiczbaZadań


{
get
{
return listaZadań.Count;
}
}

public Zadanie this[int indeks]


{
get
{
return listaZadań[indeks];
}
}
}
}

Jak widać na listingu 15.3, w klasie Zadania zdefiniowane są metody DodajZadanie


i UsuńZadanie, które tak naprawdę udostępniają tylko metody Add i Remove listy zadań
przechowywanej w polu listaZadań (typu List<Zadanie>). Podobnie indeksator klasy
Zadania odwołuje się do indeksatora tej listy. Jak już wspomniałem, aby instancje klasy
Zadania mogły działać jak kolekcje, powinniśmy zaimplementować interfejs IEnumerable
<Zadanie>, który wymusi na nas dodanie do klasy dwóch przeciążonych metod o nazwie
GetEnumerator. Pierwsza powinna zwrócić obiekt implementujący interfejs IEnumerator
<Zadanie>, a druga jego nieparametryzowaną wersję, czyli IEnumerator. Obie metody
widoczne są na listingu 15.4. I tym razem pośredniczą one tylko w dostępie do kolekcji
z pola listaZadań, udostępniając zdefiniowany w niej obiekt.

Listing 15.4. Awans do kolekcji


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Collections;

namespace ZadaniaWPF.Model
{
public class Zadania : IEnumerable<Zadanie>
{
private List<Zadanie> listaZadań = new List<Zadanie>();

public void DodajZadanie(Zadanie zadanie)


{
listaZadań.Add(zadanie);
}

public bool UsuńZadanie(Zadanie zadanie)


Rozdział 15.  Kolekcje w MVVM i XAML 167

{
return listaZadań.Remove(zadanie);
}

public int LiczbaZadań


{
get
{
return listaZadań.Count;
}
}

public Zadanie this[int indeks]


{
get
{
return listaZadań[indeks];
}
}

public IEnumerator<Zadanie> GetEnumerator()


{
return listaZadań.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)this.GetEnumerator();
}
}
}

Taki sposób implementacji klasy Zadania, który sprawia, że jest ona jedynie cienką
warstwą otaczającą kolekcję List<Zadanie>, czyni wątpliwym sens trudu, jaki włoży-
liśmy w jej stworzenie. Chciałbym ją jednak zachować jako przykład modelu-kolekcji,
który łatwo rozszerzyć i zmodyfikować we własnych projektach. Dobrym przykładem
takiego rozszerzenia będzie możliwość sortowania kolekcji zadań, którą opiszę w dalszej
części tego rozdziału.

Przechowywanie danych w pliku XML


Do folderu Model dodajmy plik PlikXML.cs ze statyczną klasą PlikXML. W tej klasie
definiujemy dwie statyczne metody: zapisującą i odczytującą kolekcję zadań z pliku
XML (listing 15.5). Tworzenie statycznej klasy służącej do zapisywania i odczytywania
całych instancji modelu to moje ulubione rozwiązanie umożliwiające łatwą zmianę
sposobu przechowywania danych. Sam model nie musi wówczas wiedzieć, w jaki
sposób jest przechowywany. Kod obu metod klasy PlikXML jest na tyle standardowy, że
nie warto go szerzej omawiać. Zwróćmy tylko uwagę na wykorzystanie zapytania LINQ
w metodzie Zapisz. Źródłem LINQ jest w tym przypadku instancja naszej kolekcji
Zadania, co jest możliwe dzięki temu, że implementuje interfejs IEnumerable<Zadanie>
Rozdział 15.  Kolekcje w MVVM i XAML 169

{
XDocument xml = XDocument.Load(ścieżkaPliku);
IEnumerable<Zadanie> dane =
from zadanie in xml.Root.Descendants("Zadanie")
select new Zadanie(
zadanie.Element("Opis").Value,
DateTime.Parse(zadanie.Element("DataUtworzenia").Value),
DateTime.Parse(
zadanie.Element("PlanowanaDataRealizacji").Value),
(PriorytetZadania)byte.Parse(
zadanie.Element("Priorytet").Value),
bool.Parse(zadanie.Element("CzyZrealizowane").Value));
Zadania zadania = new Zadania();
foreach (Zadanie zadanie in dane) zadania.DodajZadanie(zadanie);
return zadania;
}
catch (Exception exc)
{
throw new Exception("Błąd przy odczycie danych z pliku XML", exc);
}
}
}
}

Model widoku zadania


Kolejną warstwą aplikacji jest model widoku. Jak często bywa w aplikacjach MVVM,
jest to najciekawsza część kodu. W tej warstwie, podobnie jak w warstwie modelu,
również zdefiniujemy dwie klasy: pierwsza będzie modelem widoku pojedynczego
zadania, druga — modelem widoku całej kolekcji zadań. Zacznijmy od stworzenia
folderu ModelWidoku, w którym będziemy przechowywać pliki z kodem należącym
do tej warstwy. Następnie w folderze tym umieśćmy plik z klasą Zadanie2. Klasa ta
przechowuje instancję klasy Zadanie z warstwy modelu i ma własności Opis, Priorytet,
DataUtworzenia, PlanowanyTerminRealizacji i CzyZrealizowane, które umożliwiają od-
czytanie odpowiadających im własności modelu. Dodatkowo zdefiniowana jest w niej
własność CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie, ułatwiająca wy-
krycie sytuacji, w której zadanie nie zostało zrealizowane pomimo tego, że planowany
termin jego realizacji już minął. Klasa Zadanie z warstwy modelu widoku wyposażona
została w dwa konstruktory: w pierwszym podajemy wartości poszczególnych wła-
sności zadania, w drugim — instancję modelu. Istnieje także możliwość „wyłuskania”
modelu za pomocą metody GetModel (komentarz poniżej). Klasa Zadanie implemen-
tuje interfejs INotifyPropertyChanged. Ma wobec tego zdefiniowane zdarzenie Property
Changed i metodę pomocniczą OnPropertyChanged, której implementacja jest iden-
tyczna jak ta używana w rozdziale 4. (listing 4.4). Wszystkie te elementy widoczne są
na listingu 15.6.

2
Identyczna nazwa klasy z modelu widoku i modelu może powodować zamieszanie. Na szczęście obie klasy
znajdują się w różnych przestrzeniach nazw: ZadaniaWPF.Model.Zadanie i ZadaniaWPF.ModelWidoku.Zadanie.
170 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Listing 15.6. Model widoku zadania


using System;

using System.ComponentModel;
using System.Windows.Input;

namespace ZadaniaWPF.ModelWidoku
{
public class Zadanie : INotifyPropertyChanged
{
private Model.Zadanie model;

public string Opis


{
get
{
return model.Opis;
}
}

public Model.PriorytetZadania Priorytet


{
get
{
return model.Priorytet;
}
}

public DateTime DataUtworzenia


{
get
{
return model.DataUtworzenia;
}
}

public DateTime PlanowanyTerminRealizacji


{
get
{
return model.PlanowanyTerminRealizacji;
}
}

public bool CzyZrealizowane


{
get
{
return model.CzyZrealizowane;
}
}

public bool CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie


{
get
{
return !CzyZrealizowane && (DateTime.Now > PlanowanyTerminRealizacji);
Rozdział 15.  Kolekcje w MVVM i XAML 171

}
}

public Zadanie(Model.Zadanie zadanie)


{
this.model = zadanie;
}

public Zadanie(string opis, DateTime dataUtworzenia,


DateTime planowanyTerminRealizacji, Model.PriorytetZadania priorytetZadania,
bool czyZrealizowane)
{
model = new Model.Zadanie(opis, dataUtworzenia,
planowanyTerminRealizacji, priorytetZadania, czyZrealizowane);
}

public Model.Zadanie GetModel()


{
return model;
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(params string[] nazwyWłasności)


{
if (PropertyChanged != null)
{
foreach (string nazwaWłasności in nazwyWłasności)
PropertyChanged(this, new PropertyChangedEventArgs
(nazwaWłasności));
}
}
}
}

Implementacja przez klasę Zadanie z warstwy modelu widoku interfejsu INotify


PropertyChanged zapewni aktualizowanie kontrolek powiązanych z własnościami tej
klasy. Jednak aby umożliwić widokowi wpływanie na stan instancji klasy Zadanie,
musimy zdefiniować w niej jeszcze polecenia. Potrzebne są przynajmniej dwa: pierwsze
będzie oznaczało zadanie jako zrealizowane, przełączając własność CzyZrealizowane
na true, a drugie będzie pozwalało z powrotem oznaczyć je jako niezrealizowane po-
przez ustawienie własności CzyZrealizowane na false. Do ich implementacji używamy
poznanej w rozdziale 6. klasy RelayCommand (listing 6.8), którą należy dodać do pro-
jektu. Oba polecenia widoczne są na listingu 15.7.

Listing 15.7. Polecenia, które należy dodać do klasy Zadanie z modelu widoku
ICommand oznaczJakoZrealizowane;

public ICommand OznaczJakoZrealizowane


{
get
{
if (oznaczJakoZrealizowane == null)
oznaczJakoZrealizowane = new RelayCommand(
172 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

o =>
{
model.CzyZrealizowane = true;
OnPropertyChanged("CzyZrealizowane",
"CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie");
},
o =>
{
return !model.CzyZrealizowane;
});
return oznaczJakoZrealizowane;
}
}

ICommand oznaczJakoNiezrealizowane = null;

public ICommand OznaczJakoNiezrealizowane


{
get
{
if (oznaczJakoNiezrealizowane == null)
oznaczJakoNiezrealizowane = new RelayCommand(
o =>
{
model.CzyZrealizowane = false;
OnPropertyChanged("CzyZrealizowane",
"CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie");
},
o =>
{
return model.CzyZrealizowane;
});
return oznaczJakoNiezrealizowane;
}
}

Kolekcja w modelu widoku


Wróćmy do zasadniczej nowości tego rozdziału, czyli do kolekcji. Jak sobie poradzić
z kolekcjami w modelu widoku? W szczególności problemem, z którym będziemy
musieli się zmierzyć, jest powiadamianie widoku o zmianach w kolekcji. Służy do tego
interfejs INotifyCollectionChanged, który model widoku lub udostępniona przez niego
własność powinny implementować. A to oznacza, że jeżeli będę chciał zastosować
swoje ulubione podejście do modelu widoku, w którym przechowuje on instancję mo-
delu i udostępnia jego własności, to musiałbym zbudować kolejną „nakładkę” na ko-
lekcję (podobną jak w klasie Zadania z warstwy modelu), która sama byłaby kolekcją,
a dodatkowo implementowałaby interfejs INotifyCollectionChanged i jednocześnie
zawierałaby polecenia pozwalające dodawać i usuwać elementy. Postanowiłem jednak
zrobić inaczej: zamiast implementować interfejs INotifyCollectionChanged przez głów-
ną klasę warstwy modelu widoku, dodałem do niego publiczną kolekcję Observable
Collection<Zadanie>, która ten interfejs już implementuje. Oznacza to jednak, że
174 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

model.DodajZadanie(new Model.Zadanie("Drugie", DateTime.Now,


DateTime.Now.AddDays(2), Model.PriorytetZadania.Ważne));
model.DodajZadanie(new Model.Zadanie("Trzecie", DateTime.Now,
DateTime.Now.AddDays(1), Model.PriorytetZadania.MniejWażne));
model.DodajZadanie(new Model.Zadanie("Czwarte", DateTime.Now,
DateTime.Now.AddDays(3), Model.PriorytetZadania.Krytyczne));
model.DodajZadanie(new Model.Zadanie("Piąte", DateTime.Now, new
DateTime(2015, 03, 15, 1, 2, 3), Model.PriorytetZadania.Krytyczne));
model.DodajZadanie(new Model.Zadanie("Szóste", DateTime.Now, new
DateTime(2015, 03, 14, 1, 2, 3), Model.PriorytetZadania.Krytyczne));
//testy – koniec

KopiujZadania();
}

private void SynchronizacjaModelu(object sender,


NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Zadanie noweZadanie = (Zadanie)e.NewItems[0];
if (noweZadanie != null)
model.DodajZadanie(noweZadanie.GetModel());
break;
case NotifyCollectionChangedAction.Remove:
Zadanie usuwaneZadanie = (Zadanie)e.OldItems[0];
if (usuwaneZadanie != null)
model.UsuńZadanie(usuwaneZadanie.GetModel());
break;
}
}
}
}

Zapowiedziany już problem, jaki pojawia się przy utrzymywaniu dwóch kolekcji
w modelu widoku, to ich synchronizacja. W przypadku aplikacji omawianej w tym roz-
dziale synchronizacja ta jest jednostronna — użytkownik zmieniać może tylko kolekcję
modeli widoku, a z nią uzgadniać powinniśmy kolekcję modeli, którą na końcu zapi-
szemy do pliku. Wypełnianie kolekcji modeli widoku następuje wyłącznie podczas
uruchamiania aplikacji i jest realizowane przez konstruktor (zob. metodę KopiujZadania).
Możemy wobec tego skorzystać z mechanizmu powiadamiania oferowanego przez inter-
fejs INotifyCollectionChanged i wykorzystać zdefiniowane w nim zdarzenie Collection
Changed. Podpinamy do niego metodę SynchronizacjaModelu, w której rozpoznajemy
dwie akcje: dodanie pojedynczego nowego elementu i usunięcie istniejącego. Tylko te dwie
czynności będzie mógł wykonać użytkownik; nie przewidujemy możliwości edycji
istniejących zadań. Na potrzeby metody SynchronizacjaModelu umożliwiliśmy pobranie
modelu zadania z jego modelu widoku (służy do tego metoda GetModel z listingu 15.6).
Bez tego musielibyśmy oprzeć synchronizacje na mniej pewnych indeksach obu ko-
lekcji. Dzięki automatycznej synchronizacji modelu, definiując polecenia, które zaraz
dodamy do klasy Zadania, będziemy mogli skupić się jedynie na modyfikacji kolekcji
modeli widoków przechowywanych we własności ListaZadań.
176 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Opis, Mode=OneWay}"
FontSize="20" />
<Button Content="Zrealizowane" Command="{Binding
Path=OznaczJakoZrealizowane}" />
<Button Content="Niezrealizowane" Command="{Binding
Path=OznaczJakoNiezrealizowane}" />
</StackPanel>
<TextBlock>
Termin: <Run Text="{Binding Path=PlanowanyTerminRealizacji,
Mode=OneWay, StringFormat={}{0:dd MMMM yyyy},
ConverterCulture=pl-PL}" />,
Utworzone: <Run Text="{Binding Path=DataUtworzenia,
Mode=OneWay, StringFormat={}{0:dd MMMM yyyy},
ConverterCulture=pl-PL}" />
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

Co zawiera pojedynczy element kontrolki ListBox? To określa jej własność ItemTemplate,


definiująca szablon wszystkich elementów. I tu jest miejsce na element DataTemplate,
który umożliwia nie tylko ustalenie zawartości elementów listy, ale również ich wią-
zania z elementami kolekcji ListaZadań, a więc instancjami klasy Zadanie z modelu
widoku. Jak widać na listingu 15.9, w elemencie listy pokazujemy opis zadania w kon-
trolce TextBlock, dwa przyciski, które związane są z poleceniami zdefiniowanymi
w zadaniu, oraz dwie kolejne kontrolki TextBlock, które prezentują datę utworzenia
zadania i jego zakładany termin realizacji. W ich przypadku warto zwrócić uwagę na
konwersję daty na łańcuch, za co odpowiada atrybut StringFormat w wiązaniu. Ko-
rzystamy do tego z istniejącego konwertera, który ma jednak tę wadę, że domyślnie
używa lokalizacji dla języka angielskiego. Dlatego jawnie wskazujemy język polski
(atrybut ConvertCulture).

Po uruchomieniu zobaczymy, jak brzydki jest interfejs aplikacji opisany powyższym


kodem (rysunek 15.1); za chwilę trochę go poprawimy. Na dole okna zostawiłem
miejsce na miniformularz, który umożliwi dodawanie kolejnych zadań.

Rysunek 15.1.
Widok zbudowany
z kontrolki ListBox
prezentującej
zadania
Rozdział 15.  Kolekcje w MVVM i XAML 177

Style elementów kontrolki ListBox


Aby nieco poprawić ogólny wygląd elementów kontrolki ListBox, korzystając ze stylu,
dodamy ramkę wokół elementu oraz szare tło, gdy myszka znajduje się nad tym ele-
mentem. Do tego ostatniego użyty zostanie wyzwalacz (por. rozdział 11.). Aby to
uzyskać, kod XAML kontrolki ListBox należy uzupełnić o element wyróżniony na li-
stingu 15.10.

Listing 15.10. Ustalanie stylu elementów listy


<ListBox x:Name="lbListaZadań" Margin="10,35,10,200"
ItemsSource="{Binding Path=ListaZadań}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="3">
...
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Control.Margin" Value="3" />
<Setter Property="Control.BorderBrush" Value="Black" />
<Setter Property="Control.BorderThickness" Value="1" />
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="Control.Background" Value="LightGray" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>

Natomiast żeby poprawić wygląd przycisków w elementach ListBox, zdefiniujemy


w zasobach okna styl przycisków określający ich rozmiar, wielkość czcionki i usta-
wienie względem rodzica, a następnie użyjemy go do sformatowania obu przycisków.
Pokazują to listingi 15.11 i 15.12. Po tych zabiegach wygląd aplikacji nieco się po-
prawi, co widoczne jest na rysunku 15.2.

Listing 15.11. Styl przycisku


<Window x:Class="ZadaniaWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ZadaniaWPF"
xmlns:mw="clr-namespace:ZadaniaWPF.ModelWidoku"
mc:Ignorable="d"
Title="ZadaniaWPF" Height="500" Width="500">
<Window.DataContext>
<mw:Zadania />
</Window.DataContext>
Rozdział 15.  Kolekcje w MVVM i XAML 179

Listing 15.13. Przycisk nieaktywny jest schowany


<Button Content="Zrealizowane" Command="{Binding Path=OznaczJakoZrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding RelativeSource={RelativeSource Self}, Path=IsEnabled,
Mode=OneWay, Converter={StaticResource boolToVisibility}}"/>
<Button Content="Niezrealizowane" Command="{Binding Path=OznaczJakoNiezrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding RelativeSource={RelativeSource Self}, Path=IsEnabled,
Mode=OneWay, Converter={StaticResource boolToVisibility}}"/>

Konwertery
Prezentując zadania w liście, pominąłem ich priorytet. Aby móc go pokazać, musimy
najpierw zdefiniować konwerter zmieniający go na opisujący priorytet łańcuch. Korzy-
stając z okazji, zdefiniujemy także konwertery ustalające pędzel na podstawie priorytetu
oraz styl czcionki na podstawie wartości bool. Tego ostatniego użyjemy do wyraźniej-
szego oznaczenia zadań już zrealizowanych. Dodajmy do projektu plik Konwertery.cs
z klasami konwerterów widocznymi na listingu 15.14.

Listing 15.14. Konwertery


using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

using System.Windows.Media;

namespace ZadaniaWPF
{
public class BoolToBrushConverter : IValueConverter
{
public Brush KolorDlaFałszu { get; set; } = Brushes.Black;
public Brush KolorDlaPrawdy { get; set; } = Brushes.Gray;

public object Convert(object value, Type targetType, object parameter,


CultureInfo culture)
{
bool bvalue = (bool)value;
return !bvalue ? KolorDlaFałszu : KolorDlaPrawdy;
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
throw new NotImplementedException();
}
}

public class PriorytetZadaniaToString : IValueConverter


{
180 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

public object Convert(object value, Type targetType, object parameter,


CultureInfo culture)
{
Model.PriorytetZadania priorytetZadania = (Model.PriorytetZadania)value;
return Model.Zadanie.OpisPriorytetu(priorytetZadania);
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
string opisPriorytetu = (value as string).ToLower();
return Model.Zadanie.ParsujOpisPriorytetu(opisPriorytetu);
}
}

public class PriorytetZadaniaToBrush : IValueConverter


{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
Model.PriorytetZadania priorytetZadania = (Model.PriorytetZadania)value;
switch (priorytetZadania)
{
case Model.PriorytetZadania.MniejWażne:
return Brushes.Olive;
case Model.PriorytetZadania.Ważne:
return Brushes.Orange;
case Model.PriorytetZadania.Krytyczne:
return Brushes.OrangeRed;
default:
throw new Exception("Nierozpoznany priorytet zadania");
}
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
throw new NotImplementedException();
}
}

public class BoolToTextDecorationConverter : IValueConverter


{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
bool bvalue = (bool)value;
return bvalue ? TextDecorations.Strikethrough : null;
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Rozdział 15.  Kolekcje w MVVM i XAML 181

Tylko pierwszy z konwerterów, który służy do konwersji wartości logicznej na kolor


pędzla, wymaga jakiegoś komentarza. Zwróćmy uwagę, że poza metodami Convert
i ConvertBack zdefiniowane są w nim także dwie własności KolorDlaFałszu i KolorDla
Prawdy, które wyznaczają kolory pędzla, jakie odpowiadają wartościom false i true.
To zwiększa elastyczność tego konwertera i umożliwia jego używanie w różnych kon-
tekstach bez konieczności tworzenia osobnych konwerterów, jeżeli zechcemy użyć
innych kolorów. Dzięki wartościom domyślnym własności ― co jest nowością C# 6.0
dostępną w Visual Studio 2015 ― unikamy wyjątków, gdy te własności nie zostaną
zainicjowane z poziomu XAML. W starszych wersjach Visual Studio należy w zamian
zdefiniować konstruktor domyślny nadający tym własnościom KolorDlaFałszu i Kolor
DlaPrawdy wartości:
public Brush KolorDlaFałszu { get; set; } = Brushes.Black;
public Brush KolorDlaPrawdy { get; set; } = Brushes.Gray;

public BoolToBrushConverter()
{
KolorDlaFałszu = Brushes.Black;
KolorDlaPrawdy = Brushes.Gray;
}

Aby móc użyć konwerterów, należy utworzyć ich instancje w zasobach okna:
<Window.Resources>
...
<BooleanToVisibilityConverter x:Key="boolToVisibility" />
<local:PriorytetZadaniaToString x:Key="priorytetToString" />
<local:PriorytetZadaniaToBrush x:Key="priorytetToBrush" />
<local:BoolToBrushConverter x:Key="czyZrealizowaneToBrush" />
<local:BoolToBrushConverter x:Key="czyPoTerminieToBrush"
KolorDlaFałszu="Green" KolorDlaPrawdy="Red" />
<local:BoolToTextDecorationConverter x:Key="czyZrealizowaneToTextDecoration" />
</Window.Resources>

Mając tyle narzędzi, możemy teraz znakomicie podnieść czytelność interfejsu użyt-
kownika aplikacji, co niestety nie sprawi, że stanie się on dużo bardziej elegancki.
Użycie stylów w kodzie XAML widoczne jest na listingu 15.15, a ich efekt przedstawia
rysunek 15.3.

Listing 15.15. Użycie stylów


<DataTemplate>
<StackPanel Orientation="Vertical" Margin="3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Opis, Mode=OneWay}" FontSize="20"
Foreground="{Binding CzyZrealizowane,
Converter={StaticResource czyZrealizowaneToBrush}}"
TextDecorations="{Binding Path=CzyZrealizowane, Mode=OneWay,
Converter={StaticResource czyZrealizowaneToTextDecoration}}" />
<Button Content="Zrealizowane"
Command="{Binding Path=OznaczJakoZrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding RelativeSource={RelativeSource Self},
Path=IsEnabled,
Converter={StaticResource boolToVisibility}}"/>
Rozdział 15.  Kolekcje w MVVM i XAML 183

Oczywiście warto również, żeby zawartość tego pliku była aktualizowana w momen-
cie zamykania aplikacji. Napotkamy tu jednak problem, z którym borykaliśmy się już
wcześniej: jak zrobić to bez zdarzeń informujących o zamknięciu okna? Ponownie za-
stosujemy rozwiązanie zaproponowane w rozdziale 6., aby związać ze zdarzeniem
polecenie.
1. Zacznijmy od zdefiniowania odpowiedniego polecenia w modelu widoku.
W tym celu dodajmy do klasy Zadania następujący kod, w którym ponownie
korzystamy z klasy RelayCommand:
private ICommand zapiszCommand;

public ICommand Zapisz


{
get
{
if (zapiszCommand == null)
zapiszCommand = new RelayCommand(
argument =>
{
Model.PlikXML.Zapisz(ścieżkaPlikuXml, model);
});
return zapiszCommand;
}
}

2. Kolejne kroki będą takie same, jak opisałem w rozdziale 6.:


a) musimy zacząć od dodania do projektu dwóch bibliotek: System.Windows.
Interactivity.dll i Microsoft.Expression.Interaction.dll;
b) następnie w kodzie XAML z pliku MainWindows.xaml dodajemy do
znacznika Window przestrzeń nazw:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

c) i wreszcie do elementu Window wstawiamy element Interaction.Triggers:


<i:Interaction.Triggers>
<i:EventTrigger EventName="Closed">
<i:InvokeCommandAction Command="{Binding Zapisz}" />
</i:EventTrigger>
</i:Interaction.Triggers>

3. Uruchommy teraz aplikację i od razu ją zamknijmy, żeby sprawdzić, czy powstał


plik zadania.xml. Powinien wyglądać tak samo jak ten z listingu 15.16.

Listing 15.16. Fragment pliku XML z zapisanymi zadaniami


<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Data zapisania 2015-03-21 11 49 36-->
<Zadania>
<Zadanie>
<Opis>Pierwsze</Opis>
<DataUtworzenia>2015-03-21T11:48:15.145+01:00</DataUtworzenia>
<PlanowanaDataRealizacji>2015-03-23 11:48:15</PlanowanaDataRealizacji>
<Priorytet>1</Priorytet>
</Zadanie>
Rozdział 15.  Kolekcje w MVVM i XAML 185

MessageBoxButton.YesNo, MessageBoxImage.Question,
MessageBoxResult.No);
if (mbr == MessageBoxResult.No) return;
}
ListaZadań.Remove(zadanie);
},
o =>
{
if (o == null) return false;
int indeksZadania = (int)o;
return indeksZadania >= 0;
});
return usuńZadanie;
}
}

private ICommand dodajZadanie;

public ICommand DodajZadanie


{
get
{
if (dodajZadanie == null)
dodajZadanie = new RelayCommand(
o =>
{
Zadanie zadanie = o as Zadanie;
if (zadanie != null) ListaZadań.Add(zadanie);
},
o =>
{
return (o as Zadanie) != null;
});
return dodajZadanie;
}
}

Oba polecenia są bardzo typowe. Jak zwykle, definiując je, skorzystałem z klasy Relay
Command. W obu wykorzystuję możliwość sprawdzenia, czy wykonanie polecenia
ma sens. Stosuję do tego przesyłany do polecenia parametr. W poleceniu UsuńZadanie
parametrem jest indeks zadania, a w poleceniu DodajZadanie jest nim obiekt nowego
zadania.

Jest tylko jedno „ale”. Zwróćmy uwagę na polecenie UsuńZadanie z listingu 15.17.
Przed usunięciem zadania, które nie jest jeszcze zrealizowane, chcemy pokazać użyt-
kownikowi okno dialogowe z prośbą o potwierdzenie. Wykorzystujemy do tego klasę
MessageBox z przestrzeni System.Windows. Niestety robiąc to z klasy modelu widoku,
mieszamy dwie warstwy: w warstwie modelu widoku wyświetlamy okno dialogowe,
czyli element, który ewidentnie należy do warstwy widoku i z niej powinien być kontro-
lowany. Możemy na to machnąć ręką — okna dialogowe są „autonomiczne”, to zna-
czy nie wiążą się w żaden sposób z pozostałymi elementami widoku. Zwróćmy jednak
uwagę, że ich użycie czyni polecenie niemożliwym do testowania za pomocą testów
jednostkowych — okno dialogowe zatrzymuje wykonywanie polecenia i czeka na re-
akcję użytkownika. Dlatego w następnym rozdziale poszukamy sposobu, aby kontrolo-
186 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

wać je wyłącznie z warstwy widoku. Tymczasem pozostańmy przy aktualnym rozwiąza-


niu. Aby kod poleceń z listingu 15.17 mógł być skompilowany, w pliku ModelWidoku\
Zadania.cs w sekcji poleceń using muszą być obecne dwie przestrzenie nazw: System.
Windows i System.Windows.Input.

Wróćmy do warstwy widoku. Do kodu XAML dodajmy przycisk służący do usuwania


zaznaczonego w liście zadania. Umieśćmy go pod kontrolką ListBox:
...
</ListBox>
<Button Content="Usuń zadanie"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="10,0,0,165" Width="100" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding Path=UsuńZadanie}"
CommandParameter="{Binding ElementName=lbListaZadań, Path=SelectedIndex}"
/>
...

Wiążemy go oczywiście z poleceniem UsuńZadanie. Parametrem jest indeks zaznaczone-


go elementu w kontrolce ListBox. W metodzie CanExecute polecenia sprawdzamy,
czy indeks ów jest większy lub równy zeru. Warto oczywiście uruchomić aplikację,
żeby sprawdzić, jak działa nowy przycisk zarówno w przypadku zadania oznaczonego
jako zrealizowane, jak i jeszcze niezrealizowanego.

Więcej wysiłku będzie wymagało dodanie nowego zadania. Po pierwsze konieczne


jest przygotowanie formularza zbierającego informacje o zadaniu, a konkretnie jego
opis, priorytet i planowany termin realizacji. Po drugie niezbędne jest zbudowanie
obiektu zadania, którego oczekuje polecenie DodajZadanie z klasy Zadania modelu
widoku. Jedynym rozsądnym sposobem jest tworzenie go w konwerterze, który składać
go będzie z informacji zebranych w formularzu. Tam możemy sprawdzić jego popraw-
ność, co wykorzystamy w akcji CanExecute polecenia.

Zacznijmy od rozbudowy interfejsu aplikacji, dodając do kodu XAML, pod przyci-


skiem usuwającym zadania, kontrolkę GroupBox widoczną na listingu 15.18. W niej
znajduje się formularz pozwalający zebrać informacje o zadaniu. W szczególności on
zawiera pole tekstowe umożliwiające wpisanie opisu, rozwijaną listę pozwalającą na
wybór priorytetu i kontrolkę DatePicker ułatwiającą wskazanie daty realizacji. W przy-
padku tej ostatniej warto zwrócić uwagę na to, w jaki sposób ustalana jest wartość po-
czątkowa; odwołujemy się do statycznego obiektu DateTime.Now, co jest możliwe dzięki
zadeklarowaniu przestrzeń nazw s (atrybut xmlns:s="clr-namespace:System;assembly=
mscorlib" w elemencie Window) i rozszerzeniu x:Static, które pozwala odczytywać
własności ze statycznej klasy platformy .NET.

W kodzie z listingu 15.17 elementy rozwijanej listy ustalane są „na sztywno”. Możliwe
jest jednak ich odczytanie bezpośrednio z typu wyliczeniowego. Opis kilka sposobów
można znaleźć na stronie http://stackoverflow.com/questions/6145888/how-to-bind-
an-enum-to-a-combobox-control-in-wpf.
188 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

W konsekwencji metoda CanExecute polecenia zwraca false. Zanim jednak ustawimy


parametr polecenia, który powinien zbierać dane z formularza, napiszmy konwerter,
który przekształci je w obiekt zadania. Prezentuje go listing 15.19.

Listing 15.19. Konwerter tworzący nowe zadanie


public class ZadanieConverter : IMultiValueConverter
{
PriorytetZadaniaToString pzts = new PriorytetZadaniaToString();

public object Convert(object[] values, Type targetType, object parameter,


CultureInfo culture)
{
string opis = (string)values[0];
DateTime terminUtworzenia = DateTime.Now;
DateTime? planowanyTerminRealizacji = (DateTime?)values[1];
Model.PriorytetZadania priorytet = (Model.PriorytetZadania)pzts.ConvertBack(
values[2], typeof(Model.PriorytetZadania), null,
CultureInfo.CurrentCulture);
if (!string.IsNullOrWhiteSpace(opis) && planowanyTerminRealizacji.HasValue)
return new ModelWidoku.Zadanie(opis, terminUtworzenia,
planowanyTerminRealizacji.Value, priorytet, false);
else return null;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter,


CultureInfo culture)
{
throw new NotImplementedException();
}
}

Konwerter implementuje interfejs IMultiValueConverter i zakłada, że pierwszym pa-


rametrem jest opis, drugim — planowany termin realizacji, a trzecim — priorytet. Co
do tego ostatniego zakładam, że jest tekstem opisującym priorytet, a nie numerem po-
branym choćby z własności SelectedIndex rozwijanej listy. Robię tak w zasadzie tylko
dlatego, żeby mieć okazję użyć parsowania opisu priorytetu, jaki zaimplementowałem
w klasie Zadanie modelu (por. zadanie 1. na końcu rozdziału). Data utworzenia zadania
jest pobierana ze statycznej własności DateTime.Now, a własność CzyZrealizowane —
ustawiana na false. Tworzenie zadania w konwerterze może wydawać się niezbyt ele-
ganckie, ale moim zdaniem takie rozwiązanie jest do zaakceptowania. Alternatywa,
a więc tworzenie zadania w modelu widoku, była gorszym wyjściem. Model widoku
nie jest świadomy widoku, więc nie może odczytać zawartości jego kontrolek. Takie
rozwiązanie byłoby naturalne w architekturze autonomicznego widoku (z kodem
w code-behind), ale nie w MVVM. Model widoku może wprawdzie udostępnić wła-
sności, z którymi związalibyśmy kontrolki formularza, to jednak znacznie zwiększy-
łoby liczbę wiązań. Dlatego konwerter przekształcający dane z formularza w zadanie
wydaje mi się w tej sytuacji rozwiązaniem optymalnym.

Znając konwerter, wiemy, jak związać parametr polecenia. Stwórzmy wobec tego in-
stancję konwertera, dodając do elementu Window.Resources element:
<local:ZadanieConverter x:Key="twórzZadanie" />
Rozdział 15.  Kolekcje w MVVM i XAML 189

Następnie uzupełnijmy element przycisku służącego do dodania nowego zadania o pa-


rametr polecenia związany z modelem widoku za pomocą multibindingu:
<Button Content="Dodaj zadanie"
Margin="0,83,9.8,0" HorizontalAlignment="Right" VerticalAlignment="Top"
Width="100" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding Path=DodajZadanie}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource twórzZadanie}">
<Binding ElementName="tbOpis" Path="Text" />
<Binding ElementName="dpTerminRealizacji" Path="SelectedDate" />
<Binding ElementName="cbPriorytet" Path="Text" />
</MultiBinding>
</Button.CommandParameter>
</Button>

Możemy teraz uruchomić aplikację i przetestować nową funkcję. Zapewne uwagę


Czytelnika zwrócą dwie rzeczy. Po pierwsze, format daty pokazywany w kontrolce
DatePicker nie jest zbyt wygodny. Po drugie, po dodaniu zadania może ono nie być
od razu widoczne w interfejsie aplikacji, bo pojawia się na końcu listy, co zwykle ozna-
cza, że ukryte jest pod jej dolną krawędzią. Oba problemy okazują się nie mieć pro-
stego i naturalnego rozwiązania w XAML. Pierwszy wymaga zmiany stylu pola tek-
stowego, które jest podelementem kontrolki DatePicker. Rozwiążemy to, zmieniając jego
szablon (por. rozdział 13.). Zdefiniujemy styl stosowany automatycznie dla wszystkich
pól tekstowych kontrolek DatePicker, w którym ustalamy od zera, jak owo pole tek-
stowe powinno wyglądać (listing 15.20). Ponieważ styl wskazuje tylko typ kontrolek,
a nie ich nazwę, będzie automatycznie zastosowany do kontrolek DatePicker w tym
oknie (my mamy tylko jedną).

Listing 15.20. Styl ustalający szablon, który należy umieścić w zasobach okna
<Style TargetType="{x:Type DatePickerTextBox}">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<TextBox Text="{Binding Path=SelectedDate,
StringFormat={}{0:dd MMMM yyyy}, ConverterCulture=pl-PL,
RelativeSource={RelativeSource AncestorType={x:Type DatePicker}}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Problem drugi, choć z pozoru wygląda na zupełnie banalny, jest trudny do rozwiązania
w czystym XAML. Dlatego użyję ― o zgrozo! ― metody zdarzeniowej umieszczonej
w code-behind. Na swoje usprawiedliwienie powiem tylko, że metoda będzie odnosiła
się jedynie do warstwy widoku i jej kontrolek, więc moja wina jest trochę mniejsza.
Dodajmy wobec tego do przycisku atrybut zdarzenia Click i w związanej z nim me-
todzie umieśćmy kod z listingu 15.21 wywołujący metodę ScrollToBottom paska
przewijania. W efekcie, po dodaniu nowego zadania, lista jest automatycznie przewijana
do najniższych elementów.
190 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Listing 15.21. Metoda zdarzeniowa kliknięcia przycisku


private void Button_Click(object sender, RoutedEventArgs e)
{
if (VisualTreeHelper.GetChildrenCount(lbListaZadań) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(lbListaZadań, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}

Sortowanie
W klasie List<>, będącej sercem naszego modelu, zdefiniowana jest metoda Sort,
która jednak zadziała tylko, jeżeli elementy przechowywane w kolekcji implementują
interfejs IComparable. A obiekt Zadanie tego nie robi. Istnieje jednak alternatywa: mo-
żemy metodzie Sort podpowiedzieć sposób sortowania, przekazując do niej obiekt
typu Comparison lub obiekt implementujący interfejs IComparer, który zawiera przepis
na porównywanie elementów listy. Wybiorę to rozwiązanie z obiektem Comparison.
Ułatwi nam to używanie kilku sposobów sortowania alternatywnie. Rozsądne wydają
się dwa sposoby: w pierwszym zadania będą sortowane zgodnie z priorytetami, a je-
żeli te są równe — zgodnie z zaplanowanym terminem realizacji; w drugim ważniej-
szy będzie termin, a priorytet pozostanie drugorzędny. Aby ten pomysł zrealizować,
w klasie modelu Zadania zdefiniujmy dwa pola typu Comparison<Zadanie>. Typ ten
przechowuje referencję do akcji służącej do porównywania zadań, która wykorzysty-
wana jest podczas sortowania. Wracamy wobec tego do edycji modelu i uzupełniamy
go o metodę Sort i dwa pola Comparison<Zadanie> widoczne na listingu 15.22. Nowa
metoda Sort jest ponownie ― jak cały nasz model Zadania ― tylko nakładką na
metodę Sort klasy List<>, w której przechowywane są zadania. W przypadku tej
metody jest to jednak nakładka niebanalna, bo umożliwia wybór między własno-
ściami, po których sortujemy.

Listing 15.22. Kod dodany do modelu


using System;
using System.Collections.Generic;

using System.Collections;

namespace ZadaniaWPF.Model
{
public class Zadania : IEnumerable<Zadanie>
{
private List<Zadanie> listaZadań = new List<Zadanie>();

...

private Comparison<Zadanie> porównywaniePriorytetów = new Comparison<Zadanie>(


(Zadanie zadanie1, Zadanie zadanie2) =>
Rozdział 15.  Kolekcje w MVVM i XAML 191

{
int wynik = -zadanie1.Priorytet.CompareTo(zadanie2.Priorytet);
if (wynik == 0) wynik = zadanie1.PlanowanyTerminRealizacji.
CompareTo(zadanie2.PlanowanyTerminRealizacji);
return wynik;
});

private Comparison<Zadanie> porównywaniePlanowanychTerminówRealizacji =


new Comparison<Zadanie>(
(Zadanie zadanie1, Zadanie zadanie2) =>
{
int wynik = zadanie1.PlanowanyTerminRealizacji.CompareTo(zadanie2.
PlanowanyTerminRealizacji);
if (wynik == 0) wynik = -zadanie1.Priorytet.CompareTo
(zadanie2.Priorytet);
return wynik;
});

public void SortujZadania(


bool porównywaniePriorytetówCzyPlanowanychTerminówRealizacji)
{
if (porównywaniePriorytetówCzyPlanowanychTerminówRealizacji)
listaZadań.Sort(porównywaniePriorytetów);
else
listaZadań.Sort(porównywaniePlanowanychTerminówRealizacji);
}
}
}

W warstwie modelu widoku metoda Sort zostanie udostępniona jako polecenie Sortuj
Zadania. Jego parametrem będzie wartość logiczna wskazująca sposób sortowania:
private ICommand sortujZadania;

public ICommand SortujZadania


{
get
{
if (sortujZadania == null)
sortujZadania = new RelayCommand(
o =>
{
bool porównywaniePriorytetówCzyPlanowanychTerminówRealizacji =
bool.Parse((string)o);
model.SortujZadania(
porównywaniePriorytetówCzyPlanowanychTerminówRealizacji);
KopiujZadania();
});
return sortujZadania;
}
}

I wreszcie w kodzie XAML odwołujemy się do tego polecenia w dwóch przyciskach,


które różnią się jedynie parametrem przekazywanym do polecenia:
192 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

<Button Content="Sortuj wg priorytetów"


HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="120,0,0,165" Width="130" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding Path=SortujZadania}" CommandParameter="True" />
<Button Content="Sortuj wg terminów"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="260,0,0,165" Width="120" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding Path=SortujZadania}" CommandParameter="False" />

Przyciski powinny pojawić się obok przycisku służącego do usuwania zadań, czyli
pod kontrolką ListBox.

Zadania
1. Zmodyfikuj kod XAML widoku i konwerter ZadanieConverter tak, aby priorytet
był przekazywany w wiązaniu jako liczba typu byte odczytana z indeksu
rozwijanej listy. Po tym usuń nieużywaną metodę ParsujOpisPriorytetu
z klasy Zadanie modelu.
2. Styl używany do formatowania przycisków zmień tak, aby używany był
automatycznie do wszystkich przycisków w oknie. Usuń jawne odwołania
do niego w kodzie XAML przycisków.
3. Dodaj możliwość edycji opisu i planowanego terminu realizacji istniejących
zadań. W tym celu zastąp dwie kontrolki TextBlock z szablonu elementów
kontrolki ListBox kontrolkami TextBox i DatePicker lub wykorzystaj do edycji
formularz używany przy dodawaniu nowych zadań.
4. Do formularza zbierającego dane o nowym zadaniu dodaj rozwijaną listę ComboBox
zawierającą wszystkie kolory z klasy Colors, dzięki której można wybrać kolor
zadania — w liście będzie on używany jako kolor tła elementu.
5. Do projektu asystenta zakupów z rozdziału 9. dodaj listę zakupów wzorowaną
na aplikacji przedstawionej w tym rozdziale.
6. Zaimplementuj alternatywną architekturę projektu: w modelu użyj listy
ObservableCollection<> i zbuduj model widoku, który udostępnia
bezpośrednio tę kolekcję widokowi.
7. Do zapisywania i odczytywania plików XML użyj mechanizmu serializacji
do XML.
8. Stwórz alternatywną warstwę DAL zapisującą zadania do pliku JSON.
Wykorzystaj bibliotekę Newtonsoft.JSON.
9. Zmodyfikuj oryginalny model widoku w taki sposób, aby implementował
interfejs INotifyDataErrorInfo i, korzystając z możliwości tego interfejsu,
weryfikował poprawność danych przesyłanych do modelu widoku.
Rozdział 16.
Okna dialogowe w MVVM
Poniższy rozdział został opublikowany w czasopiśmie „Programista” (6/2015).

Na problem okien dialogowych w WPF natknie się prędzej czy później każdy pro-
gramista próbujący pisać aplikacje zgodne ze wzorcem MVVM. Problem ów polega
na tym, że z punktu widzenia programisty najwygodniejszym miejscem do wywoły-
wania okien dialogowych, które ewidentnie należą do warstwy widoku, są klasy modelu
widoku, a czasem nawet klasy samego modelu. Ulegając pokusie, naruszamy ścisły
podział na warstwy, będący jedną z głównych zalet wzorca MVVM. Z typowym przy-
kładem takiej sytuacji mieliśmy do czynienia w poprzednim rozdziale, gdy w klasie
Zadania modelu widoku chcieliśmy się upewnić, czy użytkownik rzeczywiście chce
skasować jeszcze niezrealizowane zadanie, i użyliśmy do tego celu okna dialogowego
MessageBox.

W internecie można znaleźć wiele prób rozwiązania tego problemu, w praktyce jednak
jest to często moment, w którym kierownik projektu stwierdza, że „kurczowe trzymanie
się wzorca MVVM prowadzi do przesady i większych problemów niż te, które roz-
wiązuje”. Mimo to myślę, że podział na warstwy i zachowanie możliwości testowania
modelu i modelu widoku jest wartością, dla której warto poświęcić trochę dodatkowej
pracy nad projektem. Poniżej przedstawiam rozwiązanie, które sam stosuję w przy-
padku pasywnego modelu i modelu widoku, a więc w sytuacji, gdy inicjatywę ma zaw-
sze użytkownik obsługujący widok. Polega ono na zapakowaniu okien dialogowych
w elementy XAML, które można umieścić w kodzie widoku, konfigurować ich wła-
sności, a same okna otworzyć, korzystając ze zdefiniowanego w klasie tych elementów
polecenia Show. Siłą tego rozwiązania jest możliwość podpięcia do tych elementów
poleceń zdefiniowanych w modelu widoku i w ten sposób powiadamianie go o wyborze,
którego dokonał użytkownik. Dzięki temu model widoku może również zareagować na
dokonany wybór.
194 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Klasa bazowa okna dialogowego


Dodajmy do projektu plik o nazwie OknaDialogowe.cs. Będzie on należał do warstwy
widoku. Zdefiniujmy w nim klasę DialogBox dziedziczącą z FrameworkElement (klasy
bazowej klasy Control), która z kolei dziedziczy z klasy UIElement. To w tej klasie poja-
wia się własność DataContext, która będzie nam potrzebna. Oprócz tego nowa klasa
implementuje interfejs INotifyPropertyChanged, aby mogła powiadamiać o zmienionych
wartościach własności. Klasy użyte w klasie DialogBox wymagają kilku przestrzeni nazw,
które, razem z tą klasą, widoczne są na listingu 16.1.

Listing 16.1. Abstrakcyjna klasa bazowa


using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
namespace ZadaniaWPF
{
public abstract class DialogBox : FrameworkElement, INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string nazwaWłasności)


{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
#endregion
protected Action<object> execute = null;
public string Caption { get; set; }
protected ICommand show;

public virtual ICommand Show


{
get
{
if (show == null) show = new RelayCommand(execute);
return show;
}
}
}
}

Część klasy odpowiedzialna za implementację interfejsu INotifyPropertyChanged jest


standardowa — nie warto jej jeszcze raz omawiać. W jej drugiej części jest natomiast
definicja własności Caption odpowiedzialnej za nadanie oknu dialogowemu tytułu
oraz własności-polecenia Show, które będzie służyło do pokazania modalnego okna. W po-
leceniu tym tradycyjnie korzystam z klasy RelayCommand (por. rozdział 6.). Wykorzystuję
tylko jej akcję Execute, pomijając CanExecute. Akcji Execute jednak nie definiuję
w klasie DialogBox. W zamian używam niezainicjowanej akcji execute zdefiniowanej
Rozdział 16.  Okna dialogowe w MVVM 195

jako pole klasy. Ta akcja, a więc kod odpowiedzialny za pokazywanie okien dialogo-
wych i reagowanie na wybór użytkownika, będzie definiowana w klasach potomnych.
Zdefiniujmy dla przykładu klasę potomną wyświetlającą najprostsze okno dialogowe,
pokazujące jedynie komunikat przesłany w parametrze polecenia i wyświetlające tylko
przycisk OK. Aby określić rodzaj i sposób pokazywania okna dialogowego, musimy
w klasie potomnej przypisać do akcji execute wyrażenie lambda wywołujące metodę
MessageBox.Show (listing 16.2).

Listing 16.2. Klasa najprostszego okna dialogowego


public class SimpleMessageDialogBox : DialogBox
{
public SimpleMessageDialogBox()
{
execute =
o =>
{
MessageBox.Show((string)o, Caption);
};
}
}

Zaprezentuję, jak użyć tego okna z kodu XAML, wyświetlając prostą informację
o autorze aplikacji. Dodajmy do kodu element SimpleMessageDialogBox i przycisk, który
wiąże się z poleceniem Show tego elementu:
<local:SimpleMessageDialogBox x:Name="simpleMessageDialogBox" Caption="ZadaniaWPF" />
<Button Content="O..." HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="395,0,10,165" Width="60" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding ElementName=simpleMessageDialogBox, Path=Show}"
CommandParameter="ZadaniaWPF&#x0a;(c) Jacek Matulewski 2015&#x0a;WWW:
http://www.fizyka.umk.pl/~jacek" />

Efekt widoczny jest na rysunku 16.1.

Rysunek 16.1.
Proste okno
dialogowe
196 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Polecenia wykonywane
przed wyświetleniem
i po wyświetleniu okna dialogowego
Takie proste użycie okien dialogowych nie jest jednak w wielu przypadkach wystar-
czające. Zazwyczaj chcemy powiadomić użytkownika o czynności, która została już
wykonana, lub uprzedzić o czynności, która będzie wykonana za chwilę. Aby umożliwić
oba scenariusze, zdefiniujmy kolejną klasę abstrakcyjną CommandDialogBox, w której
zdefiniowane będą polecenia CommandBefore i CommandAfter. Przypisane do nich polecenia
na przykład z modelu widoku będą wykonywane odpowiednio przed wyświetleniem
i po wyświetleniu komunikatu. Zakładam, że zwykle używane będzie tylko jedno z nich,
ale nic nie stoi na przeszkodzie, aby użyć obu. Nadpisujemy polecenie Show, uwzględ-
niając w nim wykonywanie obu nowych poleceń. Zdefiniowana jest także własność
CommandProperty, która jest przekazywana do tych poleceń. Wszystkie te własności,
a więc CommandProperty, CommandBefore i CommandAfter, nie są zwykłymi własnościami,
lecz własnościami zależności opisanymi w rozdziale 7. To powoduje, że kod klasy się
wydłuża i staje się nieco mniej przejrzysty (listing 16.3), ale tylko taki sposób ich
zdefiniowania umożliwia stosowanie wiązań. Klasa CommandDialogBox jest klasą abs-
trakcyjną — nie określa zatem tego, jak wyglądać będzie okno dialogowe, które ma być
pokazane użytkownikowi. Dopiero w jej klasach potomnych będziemy definiować akcję
execute, która to dookreśli. Postępując w ten sposób, zdefiniujmy klasę Notification
DialogBox, w której inicjujemy akcję execute w taki sposób, aby wyświetlała okno
dialogowe MessageBox z opcją pokazywania ikony powiadomienia (także widoczna na
listingu 16.3).

Listing 16.3. Rozszerzenie o możliwość uruchamiania poleceń przed wyświetleniem lub po


wyświetleniu okna dialogowego
public abstract class CommandDialogBox : DialogBox
{
public override ICommand Show
{
get
{
if (show == null) show = new RelayCommand(
o =>
{
ExecuteCommand(CommandBefore, CommandParameter);
execute(o);
ExecuteCommand(CommandAfter, CommandParameter);
});
return show;
}
}

public static DependencyProperty CommandParameterProperty =


DependencyProperty.Register("CommandParameter", typeof(object),
typeof(CommandDialogBox));
Rozdział 16.  Okna dialogowe w MVVM 197

public object CommandParameter


{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}

protected static void ExecuteCommand(ICommand command, object commandParameter)


{
if (command != null)
if (command.CanExecute(commandParameter))
command.Execute(commandParameter);
}

public static DependencyProperty CommandBeforeProperty =


DependencyProperty.Register("CommandBefore", typeof(ICommand),
typeof(CommandDialogBox));

public ICommand CommandBefore


{
get
{
return (ICommand)GetValue(CommandBeforeProperty);
}
set
{
SetValue(CommandBeforeProperty, value);
}
}

public static DependencyProperty CommandAfterProperty =


DependencyProperty.Register("CommandAfter", typeof(ICommand),
typeof(CommandDialogBox));

public ICommand CommandAfter


{
get
{
return (ICommand)GetValue(CommandAfterProperty);
}
set
{
SetValue(CommandAfterProperty, value);
}
}
}

public class NotificationDialogBox : CommandDialogBox


{
public NotificationDialogBox()
{
execute =
o =>
198 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

{
MessageBox.Show((string)o, Caption, MessageBoxButton.OK,
MessageBoxImage.Information);
};
}
}

Aby zaprezentować działanie nowej klasy, zmodyfikujmy przycisk, którym dodawane


jest nowe zadanie. W tej chwili uruchamia ono polecenie DodajZadanie zdefiniowane
w modelu widoku, podając jako parametr zbiór wartości, z których konwerter tworzy
zadanie (zob. listing 15.17). Po zmianach przycisk będzie uruchamiał jedynie polece-
nie Show obiektu klasy NotificationDialogBox, a to ona będzie wywoływała polecenie
DodajZadanie z modelu widoku (listing 16.4). Użyliśmy polecenia CommandBefore,
w efekcie już po dodaniu zadania wyświetlane jest okno dialogowe z informacją o tym,
co się stało (rysunek 16.2). Gdybyśmy w zamian użyli CommandAfter, okno dialogowe
byłoby wyświetlane przed właściwym dodaniem zadania do kolekcji przechowywanej
w modelu widoku.

Mylące mogą być określenia „after” i „before”. Należy jednak pamiętać, że odnoszą
się do momentu pokazania polecenia względem okna dialogowego, a nie odwrotnie.

Listing 16.4. Zmodyfikowany kod przycisku z oknem dialogowym


<local:NotificationDialogBox x:Name="notificationDialogBox"
Caption="ZadaniaWPF" CommandBefore="{Binding Path=DodajZadanie}">
<local:NotificationDialogBox.CommandParameter>
<MultiBinding Converter="{StaticResource twórzZadanie}">
<Binding ElementName="tbOpis" Path="Text" />
<Binding ElementName="dpTerminRealizacji" Path="SelectedDate" />
<Binding ElementName="cbPriorytet" Path="Text" />
</MultiBinding>
</local:NotificationDialogBox.CommandParameter>
</local:NotificationDialogBox>
<Button Content="Dodaj zadanie"
Margin="0,83,9.8,0" HorizontalAlignment="Right" VerticalAlignment="Top"
Width="100" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding ElementName=notificationDialogBox, Path=Show}"
CommandParameter="Zadanie zostało dodane"
Click="Button_Click">
</Button>

Przypominam, że zdarzenie Click widoczne w kodzie przycisku nie jest w żaden spo-
sób związane z oknem dialogowym. Wykorzystywane jest tylko do przesunięcia listy
do dołu tak, żeby widoczne było dodane zadanie.

W powyższych klasach okien dialogowych, a konkretnie w ich poleceniach Show,


nie używam akcji CanExecute. To oznacza, że przycisk Dodaj zadanie będzie stale
aktywny, bez względu na to, czy formularz ma wypełnione pole opisu.
200 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageBoxResult.Yes;
}
}

public bool IsLastResultNo


{
get
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageBoxResult.No;
}
}

public bool IsLastResultCancel


{
get
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageBoxResult.Cancel;
}
}

public bool IsLastResultOK


{
get
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageBoxResult.OK;
}
}

public MessageDialogBox()
{
execute = o =>
{
LastResult = MessageBox.Show((string)o, Caption, Buttons, Icon);
OnPropertyChanged("LastResult");
switch (LastResult)
{
case MessageBoxResult.Yes:
OnPropertyChanged("IsLastResultYes");
ExecuteCommand(CommandYes, CommandParameter);
break;
case MessageBoxResult.No:
OnPropertyChanged("IsLastResultNo");
ExecuteCommand(CommandNo, CommandParameter);
break;
case MessageBoxResult.Cancel:
OnPropertyChanged("IsLastResultCancel");
ExecuteCommand(CommandCancel, CommandParameter);
break;
case MessageBoxResult.OK:
OnPropertyChanged("IsLastResultOK");
ExecuteCommand(CommandOK, CommandParameter);
break;
}
Rozdział 16.  Okna dialogowe w MVVM 201

};
}

public static DependencyProperty CommandYesProperty =


DependencyProperty.Register("CommandYes", typeof(ICommand),
typeof(MessageDialogBox));
public static DependencyProperty CommandNoProperty =
DependencyProperty.Register("CommandNo", typeof(ICommand),
typeof(MessageDialogBox));
public static DependencyProperty CommandCancelProperty =
DependencyProperty.Register("CommandCancel", typeof(ICommand),
typeof(MessageDialogBox));
public static DependencyProperty CommandOKProperty =
DependencyProperty.Register("CommandOK", typeof(ICommand),
typeof(MessageDialogBox));

public ICommand CommandYes


{
get
{
return (ICommand)GetValue(CommandYesProperty);
}
set
{
SetValue(CommandYesProperty, value);
}
}

public ICommand CommandNo


{
get
{
return (ICommand)GetValue(CommandNoProperty);
}
set
{
SetValue(CommandNoProperty, value);
}
}

public ICommand CommandCancel


{
get
{
return (ICommand)GetValue(CommandCancelProperty);
}
set
{
SetValue(CommandCancelProperty, value);
}
}

public ICommand CommandOK


{
get
{
return (ICommand)GetValue(CommandOKProperty);
}
202 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

set
{
SetValue(CommandOKProperty, value);
}
}
}

Klasa MessageDialogBox dziedziczy z klasy CommandDialogBox, co oznacza, że obecne


są w niej polecenia CommandBefore i CommandAfter, które mogą być uruchamiane bez
względu na to, który przycisk zostanie kliknięty w oknie dialogowym. Dodatkowo,
aby umożliwić reakcję na wybranie jednego z przycisków okna dialogowego, w klasie
MessageDialogBox zdefiniowane zostaną kolejne polecenia. Dodamy do niej również
dwie własności Buttons i Icon pozwalające konfigurować wygląd okna dialogowego
(odpowiadają analogicznym argumentom metody MessageBox.Show). Zdefiniowane jest
także pole LastResult, z którego można będzie odczytać wartość zwracaną przez metodę
MessageBox.Show, oraz seria własności pomocniczych typu bool: IsLastResultYes,
IsLastResultNo, IsLastResultCancel, IsLastResultOK, które ułatwią wiązanie z oknem
własności innych kontrolek należących do widoku.

Zapowiedziałem już, że w klasie będą zdefiniowane polecenia umożliwiające reakcję


modelu widoku na wybór przez użytkownika jednego z przycisków okna dialogowego
MessageBox. Są to polecenia CommandYes, CommandNo, CommandCancel i CommandOK, które
odpowiadają każdej z możliwych odpowiedzi, jakiej może za pomocą okna dialogowego
udzielić użytkownik, a więc wartościom typu wyliczeniowego MessageBoxResult (pomi-
nąłem tylko MessageBoxResult.None). W konstruktorze zdefiniowana jest natomiast akcja
execute, która wyświetla okno dialogowe, zapisuje zwracaną wartość do LastResult
i w zależności od jej wartości uruchamia odpowiednie polecenie, dbając przy tym o po-
wiadamianie o zmianach poszczególnych własności.

Jak pamiętamy, w projekcie ZadaniaWPF opisanym w poprzednim rozdziale przycisk


z etykietą Usuń zadanie uruchamia polecenie UsuńZadanie zdefiniowane w klasie Zadania
modelu widoku, przekazując mu indeks zaznaczonego zadania w liście. Jeżeli zadanie
jest niezrealizowane, w akcji wykonywanej w poleceniu wyświetlane jest okno dialo-
gowe z prośbą o potwierdzenie żądania usunięcia takiego zadania. Teraz pytanie przenie-
siemy do widoku i uruchomimy polecenie tylko, jeżeli użytkownik wybierze przycisk
Tak (listing 16.6). Dzięki temu kod modelu widoku uprości się — polecenie Zadania.
UsuńZadanie będzie odpowiedzialne jedynie za bezwarunkowe usuwanie zadania
(listing 16.7). A co ważniejsze, będzie można je testować, korzystając ze zwykłych testów
jednostkowych!

Listing 16.6. Przycisk i towarzyszący mu element MessageDialogBox


<local:MessageDialogBox
x:Name="questionYesNo"
Caption="ZadaniaWPF" Icon="Question" Buttons="YesNo"
CommandYes="{Binding Path=UsuńZadanie}"
CommandParameter="{Binding ElementName=lbListaZadań, Path=SelectedIndex}" />
<Button Content="Usuń zadanie"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Rozdział 16.  Okna dialogowe w MVVM 203

Margin="10,0,0,165" Width="100" Height="25"


Style="{StaticResource stylPrzycisku}"
Command="{Binding ElementName=questionYesNo, Path=Show}"
CommandParameter="Czy jesteś pewien, że chcesz usunąć zadanie?" />

Listing 16.7. Zmodyfikowane polecenie usuwające zadanie


public ICommand UsuńZadanie
{
get
{
if (usuńZadanie == null)
usuńZadanie = new RelayCommand(
o =>
{
int indeksZadania = (int)o;
Zadanie zadanie = ListaZadań[indeksZadania];
if (!zadanie.CzyZrealizowane)
{
MessageBoxResult mbr = MessageBox.Show(
"Czy jesteś pewien, że chcesz usunąć niezrealizowane
zadanie?",
"Zadania WPF",
MessageBoxButton.YesNo,
MessageBoxImage.Question,
MessageBoxResult.No);
if (mbr == MessageBoxResult.No) return;
}
ListaZadań.Remove(zadanie);
},
o =>
{
if (o == null) return false;
int indeksZadania = (int)o;
return indeksZadania >= 0;
});
return usuńZadanie;
}
}

Warunkowe wyświetlenie okna


dialogowego
Zwróćmy uwagę, że w nowym podejściu pytanie wyświetlane będzie zawsze. W naszej
aplikacji oznacza to, że będzie wyświetlane bez względu na to, czy zadanie jest zre-
alizowane, czy nie. Chcielibyśmy je jednak wyświetlać tylko w sytuacji, w której speł-
niony jest jakiś warunek? To wymaga rozszerzenia klasy MessageDialogBox. Dodana
do niej własność IsDialogBypassed reprezentuje warunek, którego wartość będzie ustala-
na poprzez wiązanie z innymi elementami XAML lub jakąś własnością modelu widoku.
Rozdział 16.  Okna dialogowe w MVVM 205

if (!IsDialogBypassed) OnPropertyChanged("IsLastResultCancel");
ExecuteCommand(CommandCancel, CommandParameter);
break;
case MessageBoxResult.OK:
if (!IsDialogBypassed) OnPropertyChanged("IsLastResultOK");
ExecuteCommand(CommandOK, CommandParameter);
break;
}
};
}
}

Aby użyć nowej klasy, zmodyfikujemy kod XAML, zmieniając użytą klasę „wrappera”
okna dialogowego i dodając do jego elementu dwie własności (listing 16.9).

Listing 16.9. Warunkowe wywoływanie okna dialogowego


<local:ConditionalMessageDialogBox
x:Name="questionYesNo"
Caption="ZadaniaWPF" Icon="Question" Buttons="YesNo"
IsDialogBypassed="{Binding ElementName=lbListaZadań,
Path=SelectedValue.CzyZrealizowane}"
DialogBypassButton="Yes"
CommandYes="{Binding Path=UsuńZadanie}"
CommandParameter="{Binding ElementName=lbListaZadań, Path=SelectedIndex}" />
<Button Content="Usuń zadanie"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="10,0,0,165" Width="100" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding ElementName=questionYesNo, Path=Show}"
CommandParameter="Czy jesteś pewien, że chcesz usunąć niezrealizowane zadanie?" />

Okna dialogowe wyboru pliku


W sprawie prostego okna dialogowego implementowanego w klasie System.Windows.
MessageBox zrobiliśmy już chyba wszystko. Są jednak jeszcze inne okna dialogowe.
Spróbujmy dla przykładu zmierzyć się z oknami dialogowymi OpenFileDialog i SaveFile
Dialog, które służą do wybierania plików. Tego drugiego użyjemy w naszej aplikacji,
aby wskazać plik tekstowy, do którego zapisana zostanie wyeksportowana lista zadań.

Listing 16.10 prezentuje abstrakcyjną klasę bazową dla obu tych okien dialogowych.
Klasa ta dziedziczy z klasy CommandDialogBox, umożliwia wobec tego uruchomienie
dowolnych poleceń przed wyświetleniem i po wyświetleniu okna dialogowego. Do-
datkowo zawiera jeszcze jedno polecenie, CommandFileOk, które będzie uruchamiane
tylko, jeżeli użytkownik wybrał i zaakceptował jakiś plik. Poza tym klasa zawiera kilka
własności konfigurujących okno dialogowe: zestaw filtrów, aktualnie wybrany filtr,
domyślne rozszerzenie pliku i oczywiście ścieżka wybranego przez użytkownika pliku.
W klasach potomnych OpenFileDialogBox i SaveFileDialogBox, również widocznych
na listingu 16.10, pozostaje jedynie wskazać, które konkretnie okno dialogowe ma
zostać użyte.
206 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Listing 16.10. Klasy okien dialogowych wyboru pliku


public abstract class FileDialogBox : CommandDialogBox
{
public bool? FileDialogResult { get; protected set; }
public string FilePath { get; set; }
public string Filter { get; set; }
public int FilterIndex { get; set; }
public string DefaultExt { get; set; }

protected Microsoft.Win32.FileDialog fileDialog = null;

protected FileDialogBox()
{
execute =
o =>
{
fileDialog.Title = Caption;
fileDialog.Filter = Filter;
fileDialog.FilterIndex = FilterIndex;
fileDialog.DefaultExt = DefaultExt;
string ścieżkaPliku = "";
if (FilePath != null) ścieżkaPliku = FilePath; else FilePath = "";
if (o != null) ścieżkaPliku = (string)o;
if (!string.IsNullOrWhiteSpace(ścieżkaPliku))
{
fileDialog.InitialDirectory =
System.IO.Path.GetDirectoryName(ścieżkaPliku);
fileDialog.FileName = System.IO.Path.GetFileName(ścieżkaPliku);
}
FileDialogResult = fileDialog.ShowDialog();
OnPropertyChanged("FileDialogResult");
if (FileDialogResult.HasValue && FileDialogResult.Value)
{
FilePath = fileDialog.FileName;
OnPropertyChanged("FilePath");
ExecuteCommand(CommandFileOk, FilePath);
};
};
}

public static DependencyProperty CommandFileOkProperty =


DependencyProperty.Register("CommandFileOk",
typeof(ICommand),
typeof(FileDialogBox));

public ICommand CommandFileOk


{
get
{
return (ICommand)GetValue(CommandFileOkProperty);
}
set
{
SetValue(CommandFileOkProperty, value);
}
}
}
Rozdział 16.  Okna dialogowe w MVVM 207

public class OpenFileDialogBox : FileDialogBox


{
public OpenFileDialogBox()
{
fileDialog = new Microsoft.Win32.OpenFileDialog();
}
}

public class SaveFileDialogBox : FileDialogBox


{
public SaveFileDialogBox()
{
fileDialog = new Microsoft.Win32.SaveFileDialog();
}
}

Aby użyć jednej z nowych klas „opakowujących” okna dialogowe, musimy rozsze-
rzyć możliwości modelu i modelu widoku w naszym projekcie o możliwość wyeks-
portowania zadań do pliku tekstowego. W tym celu do folderu Model dodajmy plik
PlikTXT.cs, w którym zdefiniujmy klasę statyczną z jedną metodą widoczną na listingu
16.11. Idąc dalej, w modelu widoku definiujemy widoczne na listingu 16.12 polecenie
służące do zapisywania informacji o zadaniach w pliku tekstowym. I wreszcie możemy
dodać do kodu XAML widoku jeszcze jeden przycisk związany z obiektem reprezen-
tującym okno dialogowe (listing 16.13). Efekt widoczny jest na rysunku 16.3.

Listing 16.11. Obsługa plików tekstowych


using System.Collections.Generic;

namespace ZadaniaWPF.Model
{
public static class PlikTXT
{
public static void Zapisz(string ścieżkaPliku, Zadania zadania)
{
if (!string.IsNullOrWhiteSpace(ścieżkaPliku))
{
List<string> opisyZadań = new List<string>();
foreach (Zadanie zadanie in zadania)
opisyZadań.Add(zadanie.ToString());
System.IO.File.WriteAllLines(ścieżkaPliku, opisyZadań.ToArray());
}
}
}
}

Listing 16.12. Użyta została standardowa konstrukcja polecenia z modelu widoku


private ICommand eksportujZadaniaDoPlikuTekstowego;

public ICommand EksportujZadaniaDoPlikuTekstowego


{
get
{
if (eksportujZadaniaDoPlikuTekstowego == null)
Rozdział 16.  Okna dialogowe w MVVM 209

Łańcuch okien dialogowych


Jeżeli po zamknięciu jednego okna dialogowego chcielibyśmy wyświetlić drugie, mo-
żemy sprząc owe okna w łańcuch, choćby taki, jaki widoczny jest na listingu 16.14.
Jego ostatnim ogniwem jest klasa NotoficationDialogBox, która nie jest do tego celu
najlepsza, gdyż po prostu wyświetla parametr przekazany przez SaveFileDialogBox,
czyli ścieżkę do pliku. Można jednak z łatwością napisać klasę bardzo podobną do
NotificationDialogBox, która wykorzysta ów parametr do wyświetlenia pełniejszego
komunikatu. Przykład widoczny jest na listingu 16.15. Aby go użyć, wystarczy jedynie
zmienić klasę NotificationDialogBox na FileSavedNotificationDialogBox w pierwszej
linii kodu XAML widocznym na listingu 16.14. Innym rozwiązaniem byłoby użycie
konwertera StringFormat do połączenia łańcuchów na poziomie XAML.

Listing 16.14. Kolejka okien dialogowych


<local:NotificationDialogBox
x:Name="saveConfirmation"
Caption="ZadaniaWPF"
CommandBefore="{Binding EksportujZadaniaDoPlikuTekstowego}"
CommandParameter="{Binding ElementName=saveFileDialogBox, Path=FilePath}" />
<local:SaveFileDialogBox
x:Name="saveFileDialogBox"
Caption="Zapisz do pliku tekstowego"
Filter="Pliki tekstowe (*.txt)|*.txt|Wszystkie pliki (*.*)|*.*"
FilterIndex="0" DefaultExt="txt" Margin="0,1,0,-1"
CommandFileOk="{Binding ElementName=saveConfirmation, Path=Show}" />
<Button
Content="Eksportuj..."
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Margin="0,0,10,165" Width="90" Height="25"
Style="{StaticResource stylPrzycisku}"
Command="{Binding Show, ElementName=saveFileDialogBox}" />

Listing 16.15. Klasa okna dialogowego informującego o zapisaniu pliku


public class FileSavedNotificationDialogBox : CommandDialogBox
{
public FileSavedNotificationDialogBox()
{
execute =
o =>
{
MessageBox.Show("Plik " + (string)o + " został zapisany", Caption,
MessageBoxButton.OK, MessageBoxImage.Information);
};
}
}
210 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Okna dialogowe z dowolną zawartością


Na koniec zostawiłem możliwość tworzenia własnych okien dialogowych. Nie chcę
jej jednak zbytnio rozwijać, pokażę jedynie główną ideę. Jest to rozwiązanie w pełni
funkcjonalne, które można rozbudowywać. Wykorzystam fakt, że zawartość okien
WPF może zostać „wstrzyknięta” poprzez ich własność Content. Może to być dowolna
zawartość zdefiniowana za pomocą XAML. Pomysł ten realizuje klasa widoczna na
listingu 16.16. Atrybut ContentProperty z argumentem WindowContent użyty przed dekla-
racją klasy wskazuje, że ewentualną zawartość elementu CustomContentDialogBox należy
traktować jako wartość własności WindowContent. Atrybut ten wymaga dodania prze-
strzeni nazw System.Windows.Markup do sekcji poleceń using na początku pliku.

Listing 16.16. Okno dialogowe z dowolną zawartością


[ContentProperty("WindowContent")]
public class CustomContentDialogBox : CommandDialogBox
{
bool? LastResult;

public double WindowWidth { get; set; } = 640;


public double WindowHeight { get; set; } = 480;
public object WindowContent { get; set; } = null;

public CustomContentDialogBox()
{
execute =
o =>
{
Window window = new Window();
window.Width = WindowWidth;
window.Height = WindowHeight;
window.Title = Caption;
window.Content = WindowContent;
LastResult = window.ShowDialog();
OnPropertyChanged("LastResult");
};
}
}

Sprawdźmy, jak użycie tej klasy wygląda w praktyce. Listing 16.17 prezentuje przy-
cisk z instancją klasy okna dialogowego, która zawiera formularz służący do dodawa-
nia zadania (kopia formularza z listingu 15.18 z późniejszymi zmianami, zob. rysunek
16.4). Formularz jest na razie nieaktywny.

Listing 16.17. Przykład użycia okna dialogowego z dowolną treścią


<local:CustomContentDialogBox
x:Name="dodajZadanieDialogBox"
Caption="Dodaj zadanie"
WindowWidth="440" WindowHeight="160">
<Grid>
<Label Content="Opis:" Margin="10,5,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top"/>
Rozdział 16.  Okna dialogowe w MVVM 213

case null:
ExecuteCommand(CommandNull, CommandParameter);
break;
}
}
};
}

public static bool? GetCustomContentDialogResult(DependencyObject d)


{
return (bool?)d.GetValue(DialogResultProperty);
}

public static void SetCustomContentDialogResult(DependencyObject d, bool? value)


{
d.SetValue(DialogResultProperty, value);
}

public static readonly DependencyProperty DialogResultProperty =


DependencyProperty.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(CustomContentDialogBox),
new PropertyMetadata(null, DialogResultChanged));

private static void DialogResultChanged(DependencyObject d,


DependencyPropertyChangedEventArgs e)
{
bool? dialogResult = (bool?)e.NewValue;
if (d is Button)
{
Button button = d as Button;
button.Click +=
(object sender, RoutedEventArgs _e) =>
{
window.DialogResult = dialogResult;
};
}
}

public static DependencyProperty CommandTrueProperty =


DependencyProperty.Register("CommandTrue", typeof(ICommand),
typeof(CustomContentDialogBox));
public static DependencyProperty CommandFalseProperty =
DependencyProperty.Register("CommandFalse", typeof(ICommand),
typeof(CustomContentDialogBox));
public static DependencyProperty CommandNullProperty =
DependencyProperty.Register("CommandNull", typeof(ICommand),
typeof(CustomContentDialogBox));

public ICommand CommandTrue


{
get
{
return (ICommand)GetValue(CommandTrueProperty);
}
set
214 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

{
SetValue(CommandTrueProperty, value);
}
}

public ICommand CommandFalse


{
get
{
return (ICommand)GetValue(CommandFalseProperty);
}
set
{
SetValue(CommandFalseProperty, value);
}
}

public ICommand CommandNull


{
get
{
return (ICommand)GetValue(CommandNullProperty);
}
set
{
SetValue(CommandNullProperty, value);
}
}
}

Zadania
1. Na wzór klasy FileDialog przygotuj klasy dziedziczące z DialogBox obsługujące
inne okna dialogowe z przestrzeni System.Windows.Forms, a więc ColorDialog,
FontDialog, okno dialogowe służące do wyboru katalogu i okna dialogowe
związane z drukowaniem.
2. Zmodyfikuj własność Caption i inne własności przedstawionych wyżej klas tak,
żeby były własnościami zależności i w efekcie umożliwiały ich wiązanie
w kodzie XAML.
Rozdział 17.
Grafika kształtów w XAML
W WPF rysowanie realizowane jest zgodnie z duchem języka opisu interfejsu XAML,
czyli poprzez deklarowanie za pomocą odpowiednich znaczników1 kształtów, jakie
chcemy zobaczyć w oknie. Znacznie różni się to od typowego sposobu tworzenia grafiki
na przykład w Windows Forms, w którym reagowaliśmy na zdarzenie Paint i za każdym
razem odmalowywaliśmy zawartość okna, korzystając z klasy Graphics oraz jej metod
Draw.. i Fill... W XAML mamy do dyspozycji kilka kontrolek opisujących kształty,
które dziedziczą z klasy Shape. Są to między innymi Ellipse, Line, Path i poznany już
w pierwszym rozdziale Rectangle. Wszystkie te kontrolki można znaleźć w podoknie
Toolbox i po prostu przeciągnąć na podgląd okna, a następnie ustalić szczegóły ich wy-
glądu, korzystając z własności widocznych w podoknie Properties. To właśnie z tych
prostych kształtów zbudowane są widoki wszystkich pozostałych kontrolek (por.
rozdział 13.)2.

Moc kształtów (kontrolek dziedziczących po Shape) w dużym stopniu polega na moż-


liwości korzystania z pędzli. Możemy użyć pędzla do wypełnienia całego kształtu
(własność Fill) i do pokolorowania jego krawędzi (własność Stroke). Oczywiście nie
musi to być jednolity kolor, jaki reprezentuje pędzel SolidColorBrush — mamy w obu
przypadkach całkowitą dowolność. Grubość krawędzi można ustalić za pomocą wła-
sności StrokeThickness. Podobnie jak inne elementy interfejsu, także kształty możemy
dowolnie przekształcać za pomocą transformacji kompozycji (własność LayoutTransform)
i rysowania (własność RenderTransform) oraz animacji, które poznaliśmy w rozdziale
12. To oznacza, że używanie kształtu nie różni się zasadniczo od używania innych
kontrolek WPF. Myślę wobec tego, że nie ma większego sensu omawianie po kolei
wszystkich kształtów dostępnych w WPF3. Zamiast tego przedstawię bardzo prosty
projekt zegara, w którym użyję kształtów, a konkretnie elipsy i linii.

1
Oczywiście można je również tworzyć dynamicznie z code-behind, ale to jest poza zakresem naszego
zainteresowania w tej książce.
2
Jeżeli szukamy sposobu, aby rysować piksel po pikselu, to należy to robić za pośrednictwem wyświetlanych
w oknie obrazów rastrowych (tzw. bitmap). Dotyczy to zresztą zarówno WPF, jak i Windows Forms.
3
Po taki opis odsyłam do MSDN na stronę https://msdn.microsoft.com/pl-pl/library/ms747393(v=vs.110).aspx.
216 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Warto wspomnieć też o klasie Geometry, choć jawnie się do niej w tym rozdziale nie
odwołamy. W odróżnieniu od Shape klasa ta nie ma metod umożliwiających rysowanie.
Służy raczej do bardziej abstrakcyjnego określania kształtu, ale zawiera też specjalne
umiejętności, których w Shape brakuje: może być umieszczana w zasobach, współdzielo-
na między obiektami i wątkami. Poza tym dziedziczy z klasy Freezable, co umożliwia
wprowadzanie jej w stan tylko do odczytu, w którym jej użycie jest wydajniejsze.

Model widoku
Co powinno być modelem aplikacji przeznaczonej dla platformy .NET, której jedy-
nym zadaniem jest pokazywanie aktualnej daty i czasu? Wydaje mi się, że struktura
DateTime jest wszystkim, czego nam w takim przypadku potrzeba. Nie musimy jednak
utrzymywać jej instancji w modelu widoku — zamiast tego zdefiniujemy w niej wła-
sność AktualnyCzas, która będzie zwracała obiekt typu DateTime dostępny dzięki sta-
tycznej własności DateTime.Now. To będzie zresztą jedyna własność udostępniana wi-
dokowi. Warto wobec tego zastanowić się, czy model widoku jest w ogóle potrzebny.
Otóż tak. Model widoku będzie bowiem za pośrednictwem interfejsu INotifyProperty
Changed generować powiadomienia informujące widok o potrzebie odświeżenia i zak-
tualizowania pokazywanego w widoku czasu. W odróżnieniu od poprzednich projektów
tym razem będzie więc aktywną warstwą aplikacji.

Dodajmy do projektu folder ModelWidoku. W nim umieśćmy plik o nazwie Zegar.cs


z klasą modelu widoku widoczną na listingu 17.1. Klasa Zegar podejmuje próbę wy-
syłania powiadomienia cztery razy na sekundę. Powiadomienie nie jest jednak reali-
zowane, jeżeli w wartości zwracanej przez własność AktualnyCzas zmieniły się tylko
milisekundy, a sekundy, minuty i godziny pozostały niezmienione. W ten sposób mak-
symalny błąd wyświetlanego czasu to 250 ms. Zwróćmy uwagę, że minuty, godziny
i data mogą być aktualizowane znacznie rzadziej niż sekundy. Jeżeli ich prezentacja
w widoku wiązałaby się ze znacznym obciążeniem procesora, warto byłoby stworzyć
dla nich osobną własność, także typu DateTime, ale znacznie rzadziej aktualizowaną.
To wpłynęłoby na rzadsze odświeżanie związanej z tą własnością części widoku.

Listing 17.1. Klasa modelu widoku


using System;
using System.ComponentModel;
using System.Windows.Threading;

namespace ZegarWPF.ModelWidoku
{
public class Zegar : INotifyPropertyChanged
{
private DateTime poprzedniCzas = DateTime.Now;

public DateTime AktualnyCzas


{
get
{
return DateTime.Now;
Rozdział 17.  Grafika kształtów w XAML 217

}
}

public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged()


{
if (AktualnyCzas - poprzedniCzas < TimeSpan.FromSeconds(1) &&
AktualnyCzas.Second == poprzedniCzas.Second)
return;

if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("AktualnyCzas"));
}

private const int okresWolnegoOdświeżaniaWidokuMs = 250; //0.25s

public Zegar()
{
Action<object, EventArgs> odświeżanieWidoku = (object sender,
EventArgs e) => { OnPropertyChanged(); };

DispatcherTimer timerOdświeżaniaWidoku = new DispatcherTimer();


timerOdświeżaniaWidoku.Tick += new EventHandler(odświeżanieWidoku);
timerOdświeżaniaWidoku.Interval =
TimeSpan.FromMilliseconds(okresWolnegoOdświeżaniaWidokuMs);
timerOdświeżaniaWidoku.Start();
odświeżanieWidoku(this, EventArgs.Empty);
}
}
}

Klasa Zegar składa się z dwóch części: pierwsza to własność AktualnyCzas wraz z kodem
odpowiedzialnym za powiadamianie o jej zmianach, druga to konstruktor, w którym
uruchamiany jest timer z interwałem równym jednej czwartej sekundy cyklicznie uru-
chamiający akcję odświeżanieWidoku, której jedynym zadaniem jest powiadamianie
widoku o ewentualnych zmianach własności AktualnyCzas.

Widok
Bez wątpienia najważniejszy w tej aplikacji jest widok. Zanim zbudujemy klasyczny
zegar ze wskazówkami, zróbmy szybki test i sprawdźmy, czy model widoku w ogóle
działa poprawnie. W tym celu umieśćmy w widoku dwie kontrolki TextBlock wyświe-
tlające aktualną datę i czas. Używamy do tego wbudowanego konwertera dat i czasu do
łańcuchów dostępnego poprzez atrybut StringFormat wiązania (listing 17.2). Używa-
liśmy go już w aplikacji ZadaniaWPF. Jeżeli zobaczymy napisy analogiczne do tych
z rysunku 17.1, oczywiście z bieżącym czasem, będzie to znaczyło, że model widoku
działa prawidłowo.
Rozdział 17.  Grafika kształtów w XAML 219

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ZegarWPF"
xmlns:mw="clr-namespace:ZegarWPF.ModelWidoku"
mc:Ignorable="d"
Background="White"
Title="ZegarWPF" Height="550" Width="500" ResizeMode="NoResize">
<Window.DataContext>
<mw:Zegar />
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontFamily" Value="Calibri" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style TargetType="Line">
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeEndLineCap" Value="Round" />
<Setter Property="StrokeStartLineCap" Value="Round" />
</Style>
<mw:KonwerterKątaWskazówek x:Key="wskazówkaGodzinowa" Wskazówka="Godzinowa" />
<mw:KonwerterKątaWskazówek x:Key="wskazówkaMinutowa" Wskazówka="Minutowa" />
<mw:KonwerterKątaWskazówek x:Key="wskazówkaSekundowa" Wskazówka="Sekundowa" />
</Window.Resources>
<StackPanel>
<TextBlock FontSize="30"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:dd MMMM yyyy}, ConverterCulture=pl-PL}" />
<TextBlock FontSize="40"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:hh:mm:ss}, ConverterCulture=pl-PL}" />
<Canvas>
<Ellipse
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="50" Canvas.Top="15"
Height="400" Width="400"
StrokeThickness="2"
Stroke="Black">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.5,0.5">
<GradientStop Offset="1" Color="#DDDDDD" />
<GradientStop Offset="0.75" Color="#EEEEEE" />
<GradientStop Offset="0" Color="White" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<TextBlock Text="12" FontSize="75" Foreground="DarkGray"
Canvas.Left="210" Canvas.Top="10" />
<TextBlock Text="3" FontSize="75" Foreground="DarkGray"
Canvas.Left="390" Canvas.Top="165" />
<TextBlock Text="6" FontSize="75" Foreground="DarkGray"
Canvas.Left="230" Canvas.Top="325" />
<TextBlock Text="9" FontSize="75" Foreground="DarkGray"
Canvas.Left="70" Canvas.Top="165" />
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
220 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Canvas.Left="250" Canvas.Top="210"
StrokeThickness="4"
X1="0" Y1="0"
X2="0" Y2="-75" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas, Mode=OneWay,
Converter={StaticResource ResourceKey=wskazówkaGodzinowa}}" />
</Line.RenderTransform>
</Line>
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="250" Canvas.Top="210"
StrokeThickness="2"
X1="0" Y1="0"
X2="0" Y2="-150" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas, Mode=OneWay,
Converter={StaticResource ResourceKey=wskazówkaMinutowa}}" />
</Line.RenderTransform>
</Line>
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="250" Canvas.Top="210"
StrokeThickness="1"
X1="0" Y1="0"
X2="0" Y2="-150" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas, Mode=OneWay,
Converter={StaticResource ResourceKey=wskazówkaSekundowa}}" />
</Line.RenderTransform>
</Line>
<Ellipse
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="240" Canvas.Top="200"
Height="20" Width="20"
Fill="Black"
Stroke="Black"/>
</Canvas>
</StackPanel>
</Window>

Do obracania wskazówek używam transformacji obrotu. Kąt obrotu związany jest z wła-
snością AktualnyCzas modelu widoku poprzez instancję konwertera KonwerterKąta
Wskazówek (listing 17.4), w których własność Wskazówka identyfikuje wskazówkę zega-
ra, o którą nam chodzi. Na podstawie tej własności konwerter wybiera godzinę, minutę
lub sekundę z własności AktualnyCzas modelu widoku i oblicza kąt wskazówki. Po-
nieważ wskazówki godzinowa i minutowa mają poruszać się płynnie, w ich przypadku
do kąta dodawane są odpowiednio ułamek minut w godzinie lub sekund w minucie.
W przypadku wskazówki sekundowej pozostajemy przy całych sekundach. Efekt wi-
doczny jest na rysunku 17.2.
Rozdział 17.  Grafika kształtów w XAML 221

W Visual Studio 2013 i wcześniejszych wersjach własność Wskazówka powinna być


zainicjowana w konstruktorze klasy konwertera.

Listing 17.4. Konwerter godzin, minut i sekund na kąty wskazówek


using System;
using System.Globalization;
using System.Windows.Data;

namespace ZegarWPF.ModelWidoku
{
public enum Wskazówka { Godzinowa, Minutowa, Sekundowa };

public class KonwerterKątaWskazówek : IValueConverter


{
public Wskazówka Wskazówka { get; set; } = Wskazówka.Godzinowa;

public object Convert(object value, Type targetType, object parameter,


CultureInfo culture)
{
DateTime dt = (DateTime)value;
double wartość = 0;
switch (Wskazówka)
{
case Wskazówka.Godzinowa:
wartość = dt.Hour;
if (wartość >= 12) wartość -= 12;
wartość += dt.Minute / 60.0;
wartość /= 12.0;
break;
case Wskazówka.Minutowa:
wartość = dt.Minute;
wartość += dt.Second / 60.0;
wartość /= 60.0;
break;
case Wskazówka.Sekundowa:
wartość = dt.Second;
wartość /= 60.0;
break;
}
wartość *= 360.0;
return (object)wartość;
}

public object ConvertBack(object value, Type targetType, object parameter,


CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Rozdział 17.  Grafika kształtów w XAML 223

</i:Interaction.Behaviors>
<StackPanel>
<TextBlock FontSize="30"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:dd MMMM yyyy}, ConverterCulture=pl-PL}" />
<TextBlock FontSize="40"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:hh:mm:ss}, ConverterCulture=pl PL}" />
<Canvas>
<Ellipse
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="50" Canvas.Top="15"
Height="400" Width="400"
StrokeThickness="2"
Stroke="Black">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.5,0.5">
<GradientStop Offset="1" Color="#DDDDDD" />
<GradientStop Offset="0.75" Color="#EEEEEE" />
<GradientStop Offset="0" Color="White" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<TextBlock FontSize="30" Foreground="#888888"
Canvas.Left="150" Canvas.Top="255"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:dd MMMM yyyy}, ConverterCulture=pl-PL}" />
<TextBlock FontSize="40" Foreground="#888888"
Canvas.Left="180" Canvas.Top="285"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:hh:mm:ss}, ConverterCulture=pl-PL}" />
<TextBlock Text="12" FontSize="75" Foreground="DarkGray"
Canvas.Left="210" Canvas.Top="10" />
<TextBlock Text="3" FontSize="75" Foreground="DarkGray"
Canvas.Left="390" Canvas.Top="165" />
<TextBlock Text="6" FontSize="75" Foreground="DarkGray"
Canvas.Left="230" Canvas.Top="325" />
<TextBlock Text="9" FontSize="75" Foreground="DarkGray"
Canvas.Left="70" Canvas.Top="165" />
<Line
...
</Canvas>
</StackPanel>
</Window>

Bez paska tytułu tracimy jednak możliwość przenoszenia okna z miejsca na miejsce.
Aby to zrekompensować, zdefiniujemy zachowanie, które umożliwi jego przesuwanie
myszą za dowolny punkt (por. rozdział 7.). Dodatkowo, aby zastąpić przycisk „X”
z usuniętego paska tytułu, zachowanie będzie zamykało okno zegara po naciśnięciu
klawisza Escape. Pełen kod zachowania widoczny jest na listingu 17.6. Zapiszmy je
do osobnego pliku Zachowania.cs. W listingu 17.5 widoczny jest kod „podłączający”
to zachowanie do okna. Pamiętajmy, aby, jak zwykle przy użyciu zachowań, dodać
do projektu referencje do bibliotek System.Windows.Interactivity.dll i Microsoft.
Expression.Interaction.dll.
Rozdział 17.  Grafika kształtów w XAML 225

private void Window_MouseMove(object sender, MouseEventArgs e)


{
Window window = (Window)sender;
if (trwaPrzesuwanie && e.LeftButton == MouseButtonState.Pressed)
{
Point pozycjaKursora = e.GetPosition(window);
Vector przesuniecie = pozycjaKursora - początkowaPozycjaKursora;
window.Left += przesuniecie.X;
window.Top += przesuniecie.Y;
}
}

private void window_MouseUp(object sender, MouseEventArgs e)


{
Window window = (Window)sender;
if (e.LeftButton == MouseButtonState.Released)
{
trwaPrzesuwanie = false;
}
}
}
}

Co dalej? Zegar może być potraktowany jako punkt wyjścia do dalszej pracy. Warto
dodać do niego możliwość ustawienia alarmu, którego godzina będzie wskazywana
przez dodatkową czerwoną wskazówkę. Podczas alarmu tło tarczy może zmieniać się
na czerwone. Aby móc ustawić alarm, stopień przezroczystości oraz rozmiar tarczy
(transformacja skalowania na całym panelu Canvas), warto dodać menu kontekstowe.
Warto też rozważyć możliwość użycia ikony w zasobniku z tym samym menu kontek-
stowym i możliwością przywołania zegara na wierzch wszystkich okien. Inną możli-
wością jest dodanie synchronizacji NTP lub zrobienie z zegara wygaszacza ekranu.
Większość z tych pomysłów sformułowana została jako zadania wymienione na końcu
rozdziału.

Wielkim nieobecnym tego rozdziału są ścieżki (znaczniki Path) i używany w ich


przypadku skrótowy zapis ich przebiegu. Nie było jednak moim celem kompletne
przedstawienie aplikacji graficznych w WPF (to bardzo szeroki temat), a jedynie
pokazanie, że ich tworzenie może być podobne do tworzenia aplikacji z interfejsem
zbudowanym ze „zwykłych” kontrolek WPF. Ten cel, mam nadzieję, został osiągnięty.
Szerszy opis zagadnień związanych z grafiką w WPF można natomiast znaleźć
w książkach Tworzenie nowoczesnych aplikacji graficznych w WPF (Jarosław Cisek,
Helion 2012) i Tworzenie aplikacji graficznych w .NET 3.0 (Krzysztof Rychlicki-Kicior,
Helion 2007).
226 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Zadania
1. Zegar nie musi być szary. Warto dobrać jakieś żywsze kolory wskazówek i tarczy.
2. Dodaj czerwoną wskazówkę pokazującą, na jaką godzinę ustawiony jest alarm,
i okno dialogowe służące do konfiguracji alarmu. W trakcie alarmu zegar
powinien zmieniać kolorystykę na odcienie czerwieni. Odtwarzany powinien
być także wybrany przez użytkownika plik dźwiękowy.
3. Dodaj menu kontekstowe z poleceniami ustawiającymi rozmiar (transformacja
skalowania na panelu Canvas), stopień przezroczystości, ustawienia alarmu,
informacje o programie i możliwość zamknięcia okna.
4. Korzystając z protokołu NTP, synchronizuj zegar (nie częściej niż co godzinę)
z jakimś serwerem udostępniającym wiarygodny czas, na przykład time.nist.gov,
time.windows.com lub ntp.zegar.umk.pl. W przypadku tego ostatniego czas
pochodzi z pierwszego w Polsce optycznego zegara atomowego zbudowanego
w Toruniu na UMK. Podpowiedź znajdziesz na stronie http://stackoverflow.com/
questions/1193955/how-to-query-an-ntp-server-using-c.
5. Przekształć aplikację zegara w wygaszacz ekranu. Podpowiedź, jak to zrobić,
znajdziesz na stronie http://stuff.seans.com/2008/09/01/writing-a-screen-saver-
-in-wpf/.
Rozdział 18.
Aplikacja WPF
w przeglądarce (XBAP)
Aplikacja WPF może być uruchomiona w przeglądarce. Mechanizm działania tego typu
aplikacji jest podobny jak w przypadku apletów Javy lub bogatych aplikacji HTML5,
czyli (w najprostszym scenariuszu) kod programu wykonywany jest na komputerze-
kliencie, na którym uruchomiona jest przeglądarka, i korzysta z zainstalowanej na
tym komputerze platformy .NET. Ze względów bezpieczeństwa aplikacje takie do-
myślnie nie mają jednak dostępu do zasobów komputera, a to bardzo ogranicza ich
użyteczność. Właśnie ze względu na trudności związane z bezpieczeństwem aplikacje
WPF w przeglądarce nie są zbyt popularnym rozwiązaniem. Ucząc się XAML, warto
jednak choć raz „przećwiczyć” tego typu projekt, co też zrobimy, przenosząc do
przeglądarki zegar z poprzedniego rozdziału. To nie będzie trudne zadanie, bo zegar
nie przechowuje swojego stanu na dysku ani w żaden inny sposób nie odwołuje się do
zasobów komputera. Przeniesienie aplikacji do przeglądarki nie będzie wobec tego
wymagało żadnych poważnych zmian w kodzie.

Stwórzmy nowy projekt typu WPF Browser Application o nazwie ZegarXBAP1. Projekt
tego typu w Visual Studio 2015 znajdziemy w kategorii Visual C#, Windows, Classic
Desktop. Po utworzeniu projektu zobaczymy zwykłe okno projektowania interfejsu,
jakie znamy z aplikacji WPF. Zwróćmy jednak uwagę, że tym razem nie mamy do
czynienia z oknem, lecz ze stroną, to znaczy w kodzie XAML element Window jest za-
stąpiony przez Page. Widokiem zajmiemy się jednak za chwilę. Najpierw skopiujmy
do nowego projektu dwa pliki z projektu zegara WPF opisanego w poprzednim roz-
dziale. Chodzi o pliki z podkatalogu ModelWidoku. Umieśćmy je w nowym projekcie
w podkatalogu o tej samej nazwie. Jedyna zmiana, w zasadzie czysto estetyczna, ja-
ką powinniśmy w nich wprowadzić, to zmiana nazwy przestrzeni nazw z ZegarWPF.
ModelWidoku na ZegarXBAP.ModelWidoku. Po dodaniu tych plików warto skom-
pilować projekt, żeby się przekonać, czy ta „transplantacja” się powiodła.

1
Przyrostek „XBAP” pochodzi od nieużywanego już przez Microsoft, ale nadal popularnego określenia
tego typu aplikacji z ang. XAML Browser Application.
228 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Następnie możemy do pliku Page1.xaml skopiować cały element Canvas, oczywiście


z zawartością, z pliku MainWindow.xaml projektu-pierwowzoru. Rzecz jasna wiąza-
nia, dzięki którym wskazówki obracane są do właściwych położeń, nie będą działać,
póki nie ustawimy kontekstu danych oraz nie dodamy do zasobów strony odpowiednich
stylów i konwerterów. Jednak i one mogą zostać bez modyfikacji skopiowane z projektu
WPF. Pamiętajmy tylko, że zamiast elementu Window mamy teraz element Page, zatem
element kontekstu danych to Page.DataContext, a element zasobów — Page.Resources.
Cały uzupełniony kod XAML z pliku Page1.xaml widoczny jest na listingu 18.1 (por.
listing 17.5). I to w zasadzie kończy naszą pracę — prosta aplikacja XBAP jest już
gotowa!

Listing 18.1. Widok zegara


<Page x:Class="ZegarXBAP.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ZegarXBAP"
xmlns:mw="clr-namespace:ZegarWPF.ModelWidoku"
mc:Ignorable="d"
d:DesignHeight="430" d:DesignWidth="500"
Title="Zegar">
<Page.DataContext>
<mw:Zegar />
</Page.DataContext>
<Page.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontFamily" Value="Calibri" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style TargetType="Line">
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeEndLineCap" Value="Round" />
<Setter Property="StrokeStartLineCap" Value="Round" />
</Style>
<mw:KonwerterKątaWskazówek x:Key="wskazówkaGodzinowa" Wskazówka="Godzinowa" />
<mw:KonwerterKątaWskazówek x:Key="wskazówkaMinutowa" Wskazówka="Minutowa" />
<mw:KonwerterKątaWskazówek x:Key="wskazówkaSekundowa" Wskazówka="Sekundowa" />
</Page.Resources>
<Canvas>
<Ellipse
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="50" Canvas.Top="15"
Height="400" Width="400"
StrokeThickness="2"
Stroke="Black">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.5,0.5">
<GradientStop Offset="1" Color="#DDDDDD" />
<GradientStop Offset="0.75" Color="#EEEEEE" />
<GradientStop Offset="0" Color="White" />
</RadialGradientBrush>
</Ellipse.Fill>
Rozdział 18.  Aplikacja WPF w przeglądarce (XBAP) 229

</Ellipse>
<TextBlock FontSize="30" Foreground="#888888"
Canvas.Left="150" Canvas.Top="255"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:dd MMMM yyyy}, ConverterCulture=pl-PL}" />
<TextBlock FontSize="40" Foreground="#888888"
Canvas.Left="180" Canvas.Top="285"
Text="{Binding Path=AktualnyCzas, Mode=OneWay,
StringFormat={}{0:hh:mm:ss}, ConverterCulture=pl-PL}" />

<TextBlock Text="12" FontSize="75" Foreground="DarkGray"


Canvas.Left="210" Canvas.Top="10" />
<TextBlock Text="3" FontSize="75" Foreground="DarkGray"
Canvas.Left="390" Canvas.Top="165" />
<TextBlock Text="6" FontSize="75" Foreground="DarkGray"
Canvas.Left="230" Canvas.Top="325" />
<TextBlock Text="9" FontSize="75" Foreground="DarkGray"
Canvas.Left="70" Canvas.Top="165" />
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="250" Canvas.Top="210"
StrokeThickness="4"
X1="0" Y1="0"
X2="0" Y2="-75" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas,
Mode=OneWay, Converter={StaticResource
ResourceKey=wskazówkaGodzinowa}}" />
</Line.RenderTransform>
</Line>
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="250" Canvas.Top="210"
StrokeThickness="2"
X1="0" Y1="0"
X2="0" Y2="-150" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas,
Mode=OneWay, Converter={StaticResource
ResourceKey=wskazówkaMinutowa}}" />
</Line.RenderTransform>
</Line>
<Line
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="250" Canvas.Top="210"
StrokeThickness="1"
X1="0" Y1="0"
X2="0" Y2="-150" >
<Line.RenderTransform>
<RotateTransform Angle="{Binding Path=AktualnyCzas,
Mode=OneWay, Converter={StaticResource
ResourceKey=wskazówkaSekundowa}}" />
</Line.RenderTransform>
</Line>
<Ellipse
HorizontalAlignment="Left" VerticalAlignment="Top"
Canvas.Left="240" Canvas.Top="200"
Height="20" Width="20"
230 Część II  Zaawansowane zagadnienia budowania interfejsu w XAML

Fill="Black"
Stroke="Black"/>
</Canvas>
</Page>

Rysunek 18.1.
Zegar uruchomiony
w przeglądarce

W zależności od typu konta, używanej przeglądarki i wersji systemu Windows mo-


żemy mieć różne problemy z uruchomieniem aplikacji związane z ochroną systemu
przed potencjalnie niebezpiecznymi programami. Ja, żeby zobaczyć ją w Internet
Explorerze, musiałem w ustawieniach projektu, na zakładce Security, zmienić usta-
wienie na This is a full trust application. A i tak nie uda się jej bez dodatkowych za-
biegów uruchomić z serwera WWW.
Część III
Aplikacje uniwersalne
(Universal Apps)
Ta część książki, jak wynika z tytułu, poświęcona będzie aplikacjom uniwersalnym —
nowemu typowi projektów dostępnych w Visual Studio 2013 (Update 2) i 2015, dzięki
któremu możemy tworzyć aplikacje dla urządzeń z platformami Windows Runtime
i Windows Phone Runtime. W żadnej mierze nie mam aspiracji, aby był to wyczer-
pujący przewodnik po tego typu aplikacjach. Moim celem jest przede wszystkim po-
kazanie, że wiedza dotycząca XAML i MVVM, którą zdobył Czytelnik, czytając
poprzednie rozdziały, może być użyta nie tylko w projektach aplikacji WPF, ale także
w projektach aplikacji uniwersalnych, w tym aplikacji dla urządzeń przenośnych. W cen-
trum naszego zainteresowania pozostają nadal kod XAML i wzorzec MVVM. W kon-
sekwencji nie będę opisywał poszczególnych technologii dostępnych w urządzeniach
przenośnych, pozwalających na przykład uzyskać położenie geograficzne dzięki od-
biornikowi GPS, orientację urządzenia dzięki akcelerometrowi, wykorzystać dostęp
do listy kontaktów, telefonii, komunikacji WiFi i innych tego typu unikalnych możli-
wości urządzeń przenośnych. Co więcej, nie będę tworzył nowych aplikacji, lecz je-
dynie przenosił na urządzenia mobilne te, które opisane zostały w pierwszych dwóch
częściach.

Wielką zaletą stosowania wzorca MVVM, oprócz możliwości testowania i współpracy


programistów i grafików przy tworzeniu projektu, jest to, że ułatwia przenoszenie przy-
gotowanych zgodnie z tym wzorcem projektów na inne platformy z systemami z ro-
dziny Windows. Model, model widoku, a w pewnym zakresie także kod XAML widoku,
mogą być przenoszone do nowych projektów bez większych zmian. W praktyce ozna-
czać to będzie, że w kolejnych rozdziałach, po utworzeniu nowych projektów aplika-
cji uniwersalnych, będziemy po prostu kopiować do niego całe pliki zawierające kla-
sy z warstw modelu i modelu widoku. Oznacza to także, że łatwiej będzie czytać tę
część, jeżeli Czytelnik zapoznał się z poprzednimi.
234 Część III  Aplikacje uniwersalne (Universal Apps)

dostępu do danych (DAL). Dla przykładu używane w aplikacji Kolory ustawienia


aplikacji (z zakładki Settings okna ustawień projektu) w aplikacjach dla platformy
WinRT w ogóle nie są obsługiwane.

Co więcej, także tworząc widok, nie musimy zaczynać od zera — podobnie jak w apli-
kacjach WPF budować go będziemy w języku XAML, co powoduje, że znaczne
fragmenty kodu XAML opisującego interfejs będą mogły być ponownie użyte w apli-
kacjach uniwersalnych. Wymagać jednak będą modyfikacji, choćby ze względu na
rozmiar ekranu czy inny wygląd niektórych kontrolek, a także dlatego, że w aplikacjach
uniwersalnych nie ma wszystkich mechanizmów obecnych w WPF.

Tak jak uprzedziłem we wstępie, aplikacje uniwersalne to znacznie więcej, niż pokażę
w tej części książki. Wiąże się z nimi sporo zagadnień, które nie są bezpośrednio
związane ze wzorcem MVVM, będącym naszym głównym obszarem zainteresowania:
dostęp do plików, akcelerometru i innych czujników, zmiana orientacji ekranu, ob-
sługa sieci, w tym również WiFi, obsługa mediów czy obsługa kafelków na ekranie
Start. Trochę wspomnę tylko o tych ostatnich. Omówienie wszystkich zagadnień to
materiał na osobną książkę, i to sporych rozmiarów.

Projekt
Zacznijmy od aplikacji AsystentZakupów, która wydaje się ze wszystkich aplikacji
omówionych w poprzednich dwóch częściach najprostsza, a jednocześnie, po przenie-
sieniu na urządzenia mobilne, najbardziej w praktyce przydatna. Nowy projekt tworzy-
my, korzystając z szablonu Visual C#/Store Apps/Blank App (Universal App). Projekt
nazwijmy AsystentZakupówUA.

Powstanie rozwiązanie zawierające nie jeden, a trzy projekty (rysunek 19.1): projekt
aplikacji na ekran Start i tablety z platformą WinRT (dla Windows w wersji 8.1), projekt
aplikacji dla Windows Phone 8.1 oraz tak zwany projekt współdzielony, który nie
produkuje żadnego pliku wykonywalnego lub biblioteki, a w którym powinien zna-
leźć się kod, który może być wspólny dla projektów przeznaczonych dla obu plat-
form. W najlepszym przypadku, jeżeli korzystamy z MVVM, w projektach dedyko-
wanych dla obu platform powinien znaleźć się tylko widok. Pozostała część ― warstwy
modelu i modelu widoku — powinny być wspólne, a więc umieszczone w projekcie
współdzielonym. Do niego powinny też trafić konwertery z warstwy widoku, a także
samodzielnie zaprojektowane kontrolki, przynajmniej te proste. W najgorszym przy-
padku wspólny powinien być przynajmniej cały model, choć niekoniecznie z warstwą
DAL. Jeżeli to się nie uda, wskazuje to, że model nie został dobrze zaprojektowany.

Co z innymi typami aplikacji w kategorii Universal App? Różnią się one wstępną orga-
nizacją interfejsu, ale z punktu widzenia wzorca MVVM wielkich różnic w nich nie ma.
236 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 19.1. Przystosowany do aplikacji uniwersalnych kod klasy RelayCommand


using System;
using System.Diagnostics;
using System.Windows.Input;

public class RelayCommand : ICommand


{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields

#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}

public RelayCommand(Action<object> execute, Predicate<object> canExecute)


{
if (execute == null) throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors

#region ICommand Members


[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}

public event EventHandler CanExecuteChanged;


{
add
{
if (_canExecute != null) CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null) CommandManager.RequerySuggested = value;
}
}

public void OnCanExecuteChanged()


{
if (CanExecuteChanged != null) CanExecuteChanged(this, EventArgs.Empty);
}

public void Execute(object parameter)


{
_execute(parameter);
}
#endregion // ICommand Members
}
Rozdział 19.  Kod współdzielony 237

Konwertery
Do projektu współdzielonego możemy również skopiować plik zawierający konwer-
ter BoolToBrushConverter. Może być współdzielony, pomimo że formalnie należy do
warstwy widoku. Dostosowując się do konwencji nazw plików, zmieniłem jego nazwę
na Widok.Konwertery.cs. Zmieniłem także przestrzeń nazw na AsystentZakupówUA.

Inaczej, niż było w przypadku klas modelu i modelu widoku, plik konwertera wymaga
kilku zmian, zanim uda się go skompilować. Interfejs IValueConverter, który w plat-
formie .NET znajdował się w przestrzeni System.Windows.Data, w platformie WinRT
zdefiniowany jest w przestrzeni nazw Windows.UI.Xaml.Data. Co więcej, nie jest to
dokładnie taki sam interfejs — zmieniły się sygnatury obu zadeklarowanych w nim
metod. Ostatni argument, którym w oryginale było CultureInfo, został zastąpiony
przez zwykły łańcuch przekazujący kod języka. Również pędzle Brush, które w WPF
dostępne są w przestrzeni nazw System.Windows.Media, teraz znajdują się w Windows.
UI.Xaml.Media. W WinRT brakuje niestety klasy Brushes udostępniającej zbiór jed-
nolitych pędzli typu SolidColorBrush, dlatego do klasy konwertera dodałem dwa pola,
które definiują pędzle czarny i czerwony. Używana w ich definicji klasa Color także
zdefiniowana jest w innej przestrzeni — w Windows.UI. Cały zmieniony kod kon-
wertera widoczny jest na listingu 19.2.

Listing 19.2. Konwerter dostosowany do platformy WinRT


using System;
using System.Windows.Data;
using System.Windows.Media;
using Windows.UI;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;

namespace AsystentZakupówUA
{
class BoolToBrushConverter : IValueConverter
{
private Brush brushBlack = new SolidColorBrush(Colors.Black);
private Brush brushRed = new SolidColorBrush(Colors.Red);

public object Convert(object value, Type targetType, object parameter,


string language)
{
bool b = (bool)value;
return b ? brushBlack : brushRed;
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
throw new NotImplementedException();
}
}
}
238 Część III  Aplikacje uniwersalne (Universal Apps)

Zadanie
Uzupełnij model widoku o polecenie resetowania sumy i możliwość ustalania limitu.
Aby uniknąć zmian w klasie modelu, w obu przypadkach może być po prostu two-
rzona jego nowa instancja.
240 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 20.1. „Nagłówek” pliku XAML


<Page
x:Class="AsystentZakupówUA.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AsystentZakupówUA"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mw="using:AsystentZakupówUA.ModelWidoku"
mc:Ignorable="d">
<Page.DataContext>
<mw:ModelWidoku />
</Page.DataContext>
<Page.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush" />
</Page.Resources>

Po tych przygotowaniach możemy skopiować cały element Grid z polem tekstowym,


polem edycyjnym i przyciskiem (listing 20.2, por. listing 9.4). Nie spodziewajmy się
jednak, że to będzie dobrze wyglądać. Najważniejsze jest to, że po skopiowaniu kodu
XAML wiązania z własnością Suma i poleceniem DodajKwotę z modelu widoku będą
działać prawidłowo i w efekcie aplikacja będzie zliczała dodawane kwoty. Naciśnijmy
klawisz F5, aby się o tym przekonać (rysunek 20.1). Testując aplikację, zwróćmy uwagę,
że pomimo braku menedżera poleceń i jawnych wywołań metody OnCanExecuteChanged
działa blokowanie przycisku, gdy zawartość pola edycyjnego jest nieprawidłowa. Jest
tak, ponieważ przycisk sam dba o wywołanie metody CanExecute zdefiniowanej w inter-
fejsie ICommand. Korzystając z możliwości debugowania Visual Studio, możemy łatwo
potwierdzić, że akcja CanExecute polecenia ModelWidoku.DodajKwotę rzeczywiście jest
wywoływana przy każdej zmianie zawartości okna edycyjnego.

Listing 20.2. Kod XAML skopiowany z projektu WPF


<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left" Grid.Row="0"
FontSize="25" Foreground="Navy" Margin="10,10,10,10">
Suma:
<Run Foreground="Black" FontFamily="Courier New"
Text="{Binding Path=Suma, Mode=OneWay}" />
</TextBlock>
<TextBox x:Name="tbKwota" FontSize="30" FontFamily="Courier New"
TextAlignment="Right" Margin="10,10,10,10" Grid.Row="1" Text="0"
Foreground="{Binding ElementName=btnDodaj, Path=IsEnabled,
Mode=OneWay, Converter={StaticResource boolToBrush}}" />
<Button x:Name="btnDodaj" Content="Dodaj" FontSize="20" Margin="10,10,10,10"
Grid.Row="2" Command="{Binding DodajKwotę}"
CommandParameter="{Binding ElementName=tbKwota, Path=Text}" />
</Grid>
242 Część III  Aplikacje uniwersalne (Universal Apps)

<TextBlock Margin="20" FontSize="65"


Foreground="{ThemeResource ApplicationForegroundThemeBrush}" >
Suma:
<Run Foreground="White" FontFamily="Courier New"
Text="{Binding Path=Suma, Mode=OneWay}" />
</TextBlock>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<TextBox x:Name="tbKwota" Margin="20" Grid.Column="0"
FontSize="70" FontFamily="Courier New"
Text="0" TextWrapping="Wrap" TextAlignment="Right"
Foreground="{Binding ElementName=btnDodaj, Path=IsEnabled,
Mode=OneWay, Converter={StaticResource boolToBrush}}"
Background="White" />
<Button x:Name="btnDodaj" Margin="20" Grid.Column="1"
FontSize="50" HorizontalAlignment="Stretch"
Content="Dodaj"
Command="{Binding DodajKwotę}"
CommandParameter="{Binding ElementName=tbKwota, Path=Text}"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"/>
</Grid>
</StackPanel>

Rysunek 20.2. Zmieniony interfejs

Aplikacja, którą uruchomiliśmy z poziomu Visual Studio, została zainstalowana w sys-


temie Windows 8.1 i jest dostępna na liście wszystkich aplikacji na ekran Start w taki
sam sposób jak aplikacje pobrane ze sklepu (rysunek 20.3).
Rozdział 21.
Cykl życia aplikacji
i przechowywanie
jej stanu

Cykl życia aplikacji


W przypadku aplikacji przeznaczonych na urządzenia przenośne krytycznie ważne staje
się zapisywanie stanu aplikacji. Nie przewidzieliśmy tego w wersji WPF, ale teraz nie
możemy tego pominąć. Każda aplikacja może zostać nagle przerwana lub przesunięta
w tło, choćby z powodu przychodzącego połączenia telefonicznego. Aby jednak zrobić to
prawidłowo, musimy najpierw zrozumieć inny niż w przypadku aplikacji na pulpit cykl
życia aplikacji.

W przypadku aplikacji na ekran Start w systemie Windows 8, aplikacji na tablet z WinRT


i smartfony to nie użytkownik, lecz system powinien decydować o ich zamknięciu. Sys-
tem może również wstrzymać aplikację, jeżeli nie jest widoczna na ekranie, a użyt-
kownik otwiera nowe aplikacje, lub jeżeli komputer przechodzi w stan niższego poboru
energii. Wstrzymana aplikacja nadal znajduje się w pamięci, więc jej wznowienie jest
bardzo szybkie. Dzięki przeniesieniu odpowiedzialności za kontrolę cyklu życia apli-
kacji z rąk użytkownika do systemu ten może sprawniej zarządzać pamięcią i użyciem
procesora. Schemat cyklu życia tego typu aplikacji widoczny jest na rysunku 21.1.

Wstrzymanie i wznowienie aplikacji zasadniczo nie powinno wymagać od programi-


sty żadnych działań. Problemem jest natomiast możliwość zamknięcia aplikacji przez
system. Należy zadbać, aby aplikacja zapisywała stan modelu, na którego bazie tworzony
jest model widoku i oczywiście widok. W naszej aplikacji będą to dwie liczby: limit wy-
datków i bieżąca suma.
250 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 21.2. Odczytywanie i zapisywanie stanu modelu


namespace AsystentZakupówUA.Model
{
using Windows.Storage;

public static class Ustawienia


{
private const string kluczLimitu = "Limit";
private const string kluczSumy = "Suma";

private static ApplicationDataContainer ustawienia


{
get
{
return ApplicationData.Current.LocalSettings;
}
}

public static void ZapiszStanModelu(SumowanieKwot model)


{
ustawienia.Values[kluczLimitu] = model.Limit.ToString();
ustawienia.Values[kluczSumy] = model.Suma.ToString();
}

public static SumowanieKwot OdczytajStanModelu(decimal domyślnyLimit,


decimal domyślnaSuma = 0)
{
decimal limit = domyślnyLimit, suma = domyślnaSuma;
if (ustawienia.Values.ContainsKey(kluczLimitu)) limit =
decimal.Parse((string)ustawienia.Values[kluczLimitu]);
if (ustawienia.Values.ContainsKey(kluczSumy)) suma =
decimal.Parse((string)ustawienia.Values[kluczSumy]);
return new SumowanieKwot(limit, suma);
}
}
}

Pojemnik LocalSettings, podobnie zresztą jak RoamingSettings, to słownik, w któ-


rym są przechowywane i z którego są odtwarzane wartości identyfikowane przez łań-
cuchy-klucze. Niestety nie można zapisać w nich wartości typu decimal. Stąd widoczna
na listingu 21.2 konwersja do łańcucha.

Zwróćmy uwagę, że jeżeli zabraknie odpowiednich kluczy w ustawieniach lokalnych, co


sugeruje, że aplikacja jest uruchamiana po raz pierwszy, to metoda OdczytajStanModelu
utworzy obiekt modelu z wartościami limitu i sumy wskazanymi w parametrach tej
metody. Bez obawy możemy wobec tego użyć jej w modelu widoku do inicjacji modelu
(listing 21.3), zastępując używany do tej pory konstruktor.

Listing 21.3. Zmiana sposobu tworzenia instancji modelu w modelu widoku


public class ModelWidoku : INotifyPropertyChanged
{
private SumowanieKwot model = Ustawienia.OdczytajStanModelu(1000);
...
Rozdział 21.  Cykl życia aplikacji i przechowywanie jej stanu 251

Takie rozwiązanie spowoduje jednak, że aplikacja będzie w nieskończoność przywra-


cała swój stan, co czyni ją po pierwszych zakupach bezużyteczną. Aby to naprawić,
konieczne jest dodanie przycisku pozwalającego na jej zresetowanie (zob. zadania
z rozdziałów 19. i 20.). Zalecenia Microsoftu (zob. stronę wskazaną w przypisie 1) mó-
wią ponadto, żeby nie przywracać stanu aplikacji, jeżeli jej zamknięcie zostało wymu-
szone przez użytkownika. Użytkownik może bowiem chcieć zamknąć aplikację, bo we-
szła w stan, który uniemożliwia jej użycie. Powód zamknięcia aplikacji można rozpoznać
na podstawie własności PreviousExecutionState. Jeżeli aplikację zamknął użytkownik,
własność ta będzie miała wartość ClosedByUser.

Do zapisu stanu aplikacji użyjemy metody OnSuspending klasy App (plik App.xaml.cs).
Jest ona uruchamiana już w momencie wstrzymywania aplikacji (zob. rysunek 21.1), co
daje nam pewność zapisania stanu zarówno wtedy, gdy aplikacja zamykana jest przez
użytkownika, jak i wtedy, gdy zamyka ją system. Listing 21.4 pokazuje kod tej metody
z dodanym poleceniem, które wykorzystuje dodaną wcześniej do modelu statyczną
własność SumowanieKwot.BieżącaInstancja.

Listing 21.4. Zapisywanie stanu aplikacji


private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();

if (Model.SumowanieKwot.BieżącaInstanja != null)
Model.Ustawienia.ZapiszStanModelu(
Model.SumowanieKwot.BieżącaInstanja);

deferral.Complete();
}

W przypadku tak prostej aplikacji zapisywanie stanu modelu nie będzie trwało zbyt długo,
ale aby pozostać w zgodzie z wytycznymi Microsoftu, powinniśmy wykonać je asyn-
chronicznie. Dodajmy wobec tego do klasy Ustawienia metodę ZapiszStanModeluAsync
(listing 21.5), która wywoła metodę ZapiszStanModelu w utworzonym do tego zadaniu,
zwracając referencję tego zadania, aby umożliwić synchronizację. Używając nowej me-
tody, należy skorzystać z operatora await (listing 21.6)3.

Listing 21.5. Asynchroniczne metody służące do zapisu i odczytu stanu modelu


public static Task ZapiszStanModeluAsync(SumowanieKwot model)
{
return Task.Run(
() =>
{
ZapiszStanModelu(model);
});
}

3
Opis metod asynchronicznych, modyfikatora async i operatora await, znajdzie Czytelnik w książkach
Visual Studio 2013. Podręcznik programowania w C# z zadaniami oraz Programowanie równoległe
i asynchroniczne w C# 5.0 wydawnictwa Helion.
252 Część III  Aplikacje uniwersalne (Universal Apps)

public static Task<SumowanieKwot> OdczytajStanModeluAsync(decimal domyślnyLimit,


decimal domyślnaSuma = 0)
{
return Task<SumowanieKwot>.Run(
() =>
{
return OdczytajStanModelu(domyślnyLimit, domyślnaSuma);
});
}

Listing 21.6. Użycie asynchronicznej metody służącej do zapisu stanu aplikacji


private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();

if (Model.SumowanieKwot.BieżącaInstanja != null)
await Model.Ustawienia.ZapiszStanModeluAsync
(Model.SumowanieKwot.BieżącaInstanja);

deferral.Complete();
}

Zwróćmy uwagę na zmienną deferral użytą w metodzie App.OnSuspending. Wywołanie


metody SuspendingOperation.GetDeferral powoduje odroczenie wstrzymania aplikacji.
Metoda ta zwraca obiekt typu SuspendingDeferral, który ma tylko jedną metodę o na-
zwie Complete. Wywołanie jej to sygnał, że wszystkie czynności zostały zakończone
i można kontynuować wstrzymywanie aplikacji. Właśnie z tego powodu ważne jest,
żeby metody asynchroniczne wywoływane w OnSuspending były w niej synchronizowane,
jak jest w przypadku metody zapisującej stan modelu.

Testowanie zapisu stanu aplikacji może być trudne — wymaga skutecznego wstrzymania
aplikacji przez system, co zależy od nie do końca kontrolowanych przez nas czynników.
Wygodna jest wobec tego możliwość wymuszenia odpowiednich zdarzeń, w czym
pomaga rozwijane menu Lifecycle Events dostępne w Visual Studio po uruchomieniu
aplikacji. W przypadku testowania aplikacji dla Windows Phone jego użycie jest wręcz
niezbędne.

Zadanie
Dodaj do projektu przeznaczonego dla Windowsa nową stronę, korzystając z szablo-
nu Basic Page (rysunek 21.2). Przenieś do niej kod z pliku MainPage.xaml i zastąp
starą stronę nową w pliku App.xaml.cs. Nowa strona jest nam potrzebna ze względu
na klasę SuspensionManager dodawaną do projektu po użyciu tego szablonu strony. Użyj
jej do zapisania stanu aplikacji, korzystając z opisu na stronie https://msdn.microsoft.
com/en-us/library/windows/apps/hh986968.aspx.
254 Część III  Aplikacje uniwersalne (Universal Apps)
Rozdział 22.
Kafelek
W nowszych wersjach Visual Studio kafelek (ang. tile) projektowanej i uruchamianej
w tym środowisku aplikacji nie jest automatycznie przypinany do głównej części
ekranu Start zawierającej kafelki. W Windows 8.1 aplikacja jest jedynie dodawana do
listy aplikacji (rysunek 20.3). Z łatwością możemy jednak utworzyć kafelki i dla tych
aplikacji. W tym celu przejdźmy do ekranu Start komputera-gospodarza lub emulatora
z Windows 8.1, a następnie na liście aplikacji (należy kliknąć przycisk ze strzałką skie-
rowaną w dół, w lewym dolnym rogu) odszukajmy aplikację Asystent zakupów i z jej
menu kontekstowego wybierzmy polecenie Przypnij do ekranu startowego. W ogól-
ności kafelek może mieć cztery rozmiary: mały (70×70 pikseli), średni (domyślny,
150×150 pikseli), szeroki (310×150 pikseli) i duży (310×310). Na razie dla naszej
aplikacji dostępne są tylko dwa rozmiary: mały i średni. Ponadto kafelek, jaki zobaczymy,
wyświetla tylko logo aplikacji, a mógłby wyświetlać na przykład informacje o założo-
nym limicie wydatków i bieżącej kwocie. To wymaga jednak dodatkowych zabiegów:
1. W Visual Studio, w podoknie Solution Explorer, kliknij dwukrotnie poznany już
wcześniej plik Package.appxmanifest zawierający manifest aplikacji. W edytorze
manifestu przejdź na zakładkę Visual Assets.
2. W pliku manifestu, na zakładce Visual Assets, możemy określić kolor tła kafelka.
Ja użyłem granatowego (#000080). To będzie również kolor tła ekranu
powitalnego.
3. W drzewie z lewej strony podokna widoczne są typy plików przechowujące
obraz logo. Jeżeli je przejrzymy, zwrócimy uwagę, że przypisane są tylko
obrazy dla małego i średniego logo. Właśnie dlatego tylko takie rozmiary
kafelka są w tej chwili dostępne. Jest tam jednak również miejsce na szerokie
logo (pozycja Wide 310x150 Logo), czyli obraz o rozmiarze 310×150 pikseli.
Przygotujmy zatem nowy plik z obrazem o takim rozmiarze.
4. Można to zrobić chociażby za pomocą systemowego edytora Paint,
powiększając obraz z pliku AsystentZakupówUA\AsystentZakupówUA\
AsystentZakupówUA.Windows\Assets\Logo.scale-100.png i zapisując go
w podkatalogu Assets na przykład pod nazwą WideLogo.scale-100.png1.
1
Opis wymagań i wskazówek przy projektowaniu kafelka dostępny jest w MSDN na stronie
https://msdn.microsoft.com/en-us/library/windows/apps/hh465403.aspx.
256 Część III  Aplikacje uniwersalne (Universal Apps)

5. Następnie wróćmy do edycji manifestu i po zaznaczeniu pozycji Wide 310x150


Logo kliknijmy przycisk z trzema kropkami przy obrazku ze znakiem wodnym
Scale 100. Wskażmy stworzony przed chwilą plik WideLogo.scale-100.png.
6. To połowa pracy. Druga to przygotowanie metody generującej zawartość
kafelka. W naszym przypadku będzie nią tekst umieszczany na kafelku. Metodę
tę razem z polem reprezentującym kafelek zdefiniujemy w klasie App w projekcie
współdzielonym (listing 22.1). Wymaga ona zadeklarowania dwóch przestrzeni
nazw: Windows.Data.Xml.Dom i Windows.UI.Notifications.

Listing 22.1. Metoda aktualizująca kafelek


private TileUpdater tu = TileUpdateManager.CreateTileUpdaterForApplication();

private void zmieńWyglądKafelka()


{
XmlDocument xml =

TileUpdateManager.GetTemplateContent(TileTemplateType.TileWide310x150Text01);
IXmlNode węzełTekst = xml.GetElementsByTagName("text").First();
węzełTekst.AppendChild(xml.CreateTextNode("Asystent zakupów:"));
węzełTekst = xml.GetElementsByTagName("text").Item(1);
węzełTekst.AppendChild(xml.CreateTextNode("Suma: " +
Model.SumowanieKwot.BieżącaInstanja.Suma.ToString()));
węzełTekst = xml.GetElementsByTagName("text").Item(2);
węzełTekst.AppendChild(xml.CreateTextNode("Limit: " +
Model.SumowanieKwot.BieżącaInstanja.Limit.ToString()));
tu.Update(new TileNotification(xml));
}

7. Metodę zmieńWyglądKafelka należy uruchomić przed wstrzymaniem aplikacji,


a więc z metody OnSuspending. Pokazuje to listing 22.2.

Listing 22.2. Zmiana wyglądu kafelka przed wstrzymaniem aplikacji


private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();

if (Model.SumowanieKwot.BieżącaInstanja != null)
{
await Model.Ustawienia.ZapiszStanModeluAsync(
Model.SumowanieKwot.BieżącaInstanja);
zmieńWyglądKafelka();
}

deferral.Complete();
}

Użyty w tym przykładzie szablon kafelka to TileTemplateType.TileWide310x150Text01,


czyli szeroki kafelek o rozmiarze 310×150 zawierający jedynie tekst. Aby sprawdzić
działanie metody, przejdźmy do ekranu Start i zmieńmy rozmiar kafelka aplikacji.
Należy kliknąć go prawym klawiszem myszy i wybrać pozycję Zmień rozmiar, a na-
stępnie Szeroki. Po uruchomieniu aplikacji i jej zamknięciu kafelek zmieni się — będzie
teraz wyświetlał bieżącą sumę i limit (rysunek 22.1).
Rozdział 22.  Kafelek 257

Rysunek 22.1.
Kafelek aplikacji
z informacją
o wybranych
(i zapisanych
w ustawieniach)
składowych koloru
258 Część III  Aplikacje uniwersalne (Universal Apps)
Rozdział 23.
Tworzenie i testowanie
pakietu AppX
Kilka lat temu Microsoft poszedł w ślady Google’a i Apple’a — uruchomił dwa skle-
py internetowe z aplikacjami, Windows Store i Windows Phone Store, umożliwiające
sprzedaż i kupno aplikacji dla ekranu Start i tabletów z Windows 8 i Windows RT
oraz smartfonów z systemem Windows Phone. Aplikacje, które chcemy tam umieścić,
muszą być spakowane do pakietu AppX. Ponadto muszą spełniać szereg warunków
wymienionych na stronie http://msdn.microsoft.com/library/windows/apps/hh694083.aspx.
Na szczęście Visual Studio umożliwia zautomatyzowane sprawdzenie, czy aplikacja
spełnia te warunki.

Pakiet AppX umożliwia ponadto instalację programu na innym komputerze z Win-


dows 8.1. Wymagane będzie jednak konto z licencją deweloperską (będzie pobrana
automatycznie przy instalacji).

Zacznijmy od nadania pakietowi unikalnej nazwy. Należy w tym celu użyć edytora
manifestu (plik Package.appmanifest w podoknie Solution Explorer) i przejść na za-
kładkę Packaging. Zmieńmy opis w polu Package name z ciągu liczb szesnastkowych
na dowolny, unikalny w skali świata opis bez spacji i bez polskich liter. Ja użyłem po
prostu JacekMatulewski.AsystentZakupow (rysunek 23.1). Niżej możemy ustalić numer
wersji. Później, przy tworzeniu pakietu, zaznaczymy, żeby jej część Revision była auto-
matycznie inkrementowana.
264 Część III  Aplikacje uniwersalne (Universal Apps)
Rozdział 24.
Warstwa widoku
dla Windows Phone 8.1
Większość czynności omówionych w poprzednich rozdziałach, a więc między innymi
obsługa kafelka czy zapis i odczyt stanu modelu w danych lokalnych, prowadziła do
zmiany klasy App umieszczonej w projekcie współdzielonym. Siłą rzeczy dotyczyło to
również projektu Windows Phone. W efekcie, aby uruchomić także ten projekt, musimy
jedynie przygotować interfejs aplikacji i plik manifestu. Ustawmy wobec tego projekt
aplikacji dla Windows Phone jako projekt domyślny (polecenie Set as StartUp Pro-
ject z menu kontekstowego projektu w Solution Explorer), dzięki czemu naciśnięcie
klawisza F5 spowoduje uruchomienie projektu na emulatorze smartfona.

Następnie otwórzmy plik MainPage.xaml z projektu dla Windows Phone. Dłuższą


chwilę może potrwać uruchomienie podglądu strony w widoku projektowania. Podobnie
jak w przypadku wersji na tablet, także teraz zastąpiłem główny pojemnik Grid po-
jemnikiem StackPanel. Wewnątrz niego umieściłem nieco zmodyfikowany kod XAML
z tabletu (listing 24.1). Zmniejszyłem w nim czcionki i marginesy oraz przeniosłem
z powrotem przycisk do osobnego wiersza.

Listing 24.1. Widok dla aplikacji Windows Phone


<Page
x:Class="AsystentZakupówUA.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AsystentZakupówUA"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mw="using:AsystentZakupówUA.ModelWidoku"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.DataContext>
<mw:ModelWidoku />
</Page.DataContext>
<Page.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush" />
</Page.Resources>
266 Część III  Aplikacje uniwersalne (Universal Apps)

<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">


<Border Background="Navy" >
<TextBlock Text="Asystent zakupów" Margin="10" FontSize="40"
Foreground="White" />
</Border>
<TextBlock Margin="20,20,20,10" FontSize="55"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}" >
Suma:
<Run Foreground="White" FontFamily="Courier New"
Text="{Binding Path=Suma, Mode=OneWay}" />
</TextBlock>
<TextBox x:Name="tbKwota" Margin="20,10,20,10"
FontSize="55" FontFamily="Courier New"
Text="0"
TextAlignment="Right"
Foreground="{Binding ElementName=btnDodaj, Path=IsEnabled,
Mode=OneWay, Converter={StaticResource boolToBrush}}"
Background="White" />
<Button x:Name="btnDodaj" Margin="20,10,20,10" FontSize="40"
HorizontalAlignment="Stretch"
Content="Dodaj"
Command="{Binding DodajKwotę}"
CommandParameter="{Binding ElementName=tbKwota, Path=Text}"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"/>
</StackPanel>
</Page>

Bez żadnych zmian w kodzie modelu i modelu widoku uruchommy aplikację, żeby
sprawdzić, czy działa prawidłowo. Sumowanie jest przeprowadzane prawidłowo (ry-
sunek 24.1, lewy), ale zawodzi przechowywanie stanu. Wystarczy zamknąć aplikację
i uruchomić ją od nowa, aby się przekonać, że jej stan nie jest przechowywany. Jednak to
wcale nie jest spowodowane przez błąd w projekcie. Okazuje się, że w trakcie debu-
gowania aplikacja Windows Phone nigdy nie jest wstrzymywana, a w efekcie nie jest
wywoływana jej metoda App.OnSuspending, która odpowiedzialna jest za zapisanie stanu
modelu. Ten problem można obejść, wymuszając odpowiednie zdarzenia za pomocą
rozwijanej listy Lifecycle Events z menu debugowania lub uruchamiając aplikację
w trybie Release. Wówczas przekonamy się również, że prawidłowo działa kafelek
aplikacji w Windows Phone (rysunek 24.1, prawy) — należy tylko zadbać, aby był sze-
roki. W przypadku aplikacji Windows Phone możemy to zrobić, nawet jeżeli w pliku
manifestu nie ma ustawionego logo dla tego rozmiaru.

Po zamknięciu aplikacji możemy sprawdzić, czy została ona dodana do listy aplikacji
zainstalowanych na urządzeniu (rysunek 24.1, środkowy). Domyślna ikona w kształ-
cie krzyżyka przypomina nam, żeby zajrzeć do pliku manifestu i powtórzyć czynności
opisane w rozdziale 20.: po otwarciu edytora pliku manifestu, na karcie Application,
zmieniamy nazwę, język i opis zgodnie ze wzorem z rysunku 24.2. W odróżnieniu od
tabletu wspierać będziemy tylko pionowe ustawienia smartfona (Portrait). Następnie
oderwijmy się na chwilę od edycji pliku manifestu, żeby zmodyfikować pliki z logo
przechowywane w podkatalogu Assets. Jak tylko się z tym uporamy, wróćmy do
edytora manifestu i przejdźmy do zakładki Visual Assets. Przypnijmy logo do wszystkich
rozmiarów kafelka, choć dzięki wcześniejszym zmianom w klasie App w przypadku
szerokiego i tak będzie ono zastępowane przez informacje o limicie i bieżącej sumie.
268 Część III  Aplikacje uniwersalne (Universal Apps)

Zadania
1. Postępując analogicznie jak w rozdziałach 19.–24., przenieś aplikację KoloryWPF
do projektu aplikacji uniwersalnej. Należy pamiętać, że w aplikacjach uniwersalnych
niedostępne będą wiązania typu multibinding. Należy także zamienić sposób
przechowywania ustawień. Prosty widok tej aplikacji powinien umożliwić jego
umieszczenie w projekcie współdzielonym. W projektach dla konkretnych
systemów pozostałby tylko plik manifestu. Rozwiązanie tego zadania znajduje
się w kodach źródłowych dodanych do książki.
2. Osobnym zagadnieniem są zachowania (ang. behaviors). W aplikacjach
uniwersalnych są dostępne dzięki rozszerzeniu (zob. menu Tools, Extensions
and Updates...). Do projektu można je dodać z menu kontekstowego podokna
Solution Explorer. Wybieramy Add Reference..., następnie zaznaczamy kategorię
Windows 8.1 lub Windows Phone 8.1, podkategorię Extensions. W środkowej
części okna zawierającego listę rozszerzeń zaznaczamy Behaviors SDK (XAML)
i klikamy OK.
W projekcie KoloryUA z poprzedniego punktu zdefiniuj zachowania
odpowiadające zachowaniom opisanym w rozdziale 7. Niestety nie znajdziesz
klasy Behavior<>. Zamiast tego należy w klasie zachowania zaimplementować
interfejs IBehavior, a jako klasę bazową wskazać DependencyObject. Listing
24.2 pokazuje, jak można odtworzyć w aplikacji uniwersalnej klasę z listingu
7.1, a listing 24.3 — jak tego zachowania użyć w kodzie XAML.

Listing 24.2. Zachowanie w aplikacji uniwersalnej


using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;

namespace KoloryUA
{
public class ZamknięcieAplikacjiPoNaciśnieciuKlawisza : DependencyObject,
IBehavior
{
public VirtualKey Klawisz { get; set; }

void page_KeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)


{
if (e.Key == Klawisz) Application.Current.Exit();
}

public DependencyObject AssociatedObject { get; set; }

public void Attach(DependencyObject associatedObject)


{
AssociatedObject = associatedObject;
Page page = (Page)associatedObject;
if (page != null) page.KeyDown += page_KeyDown;
}
Rozdział 24.  Warstwa widoku dla Windows Phone 8.1 269

public void Detach()


{
Page page = (Page)AssociatedObject;
if (page != null) page.KeyDown -= page_KeyDown;
AssociatedObject = null;
}
}
}

Listing 24.3. Użycie zachowania w kodzie XAML strony


<Page
x:Class="KoloryUA.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KoloryUA"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:KoloryUA.ModelWidoku"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d" DataContext="{StaticResource modelWidoku}">
<Page.Resources>
<local:ByteToDoubleConverter x:Key="byteToDouble" />
<local:ColorToSolidColorBrushConverter x:Key="colorToBrush" />
</Page.Resources>
<i:Interaction.Behaviors>
<local:ZamknięcieAplikacjiPoNaciśnieciuKlawisza Klawisz="Escape" />
</i:Interaction.Behaviors>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Rectangle x:Name="rectangle"
...
270 Część III  Aplikacje uniwersalne (Universal Apps)
Rozdział 25.
Kolekcje
w aplikacji mobilnej
Drugą aplikacją, którą przeniesiemy na urządzenia mobilne, będzie ZadaniaWPF. To
ważny z dydaktycznego punktu widzenia projekt ze względu na używane w nim ko-
lekcje. Tym razem w ogóle nie będę zajmował się kafelkami ani żadnymi innymi do-
datkowymi zagadnieniami — moim celem będzie tylko i wyłącznie uruchomienie tej
aplikacji na Windows Phone.

Dostęp do plików w katalogu lokalnym


Stwórzmy nowy projekt typu Blank App (Universal Apps) o nazwie ZadaniaUA. W pro-
jekcie ZadaniaUA.Shared stwórzmy folder Model i skopiujmy do niego pliki Zadanie.cs,
Zadania.cs i PlikXML.cs z folderu Model projektu ZadaniaWPF. Nowe pliki powinny
być widoczne w podoknie Solution Explorer. Można dodatkowo zmienić przestrzeń
nazw tych plików z ZadaniaWPF.Model na ZadaniaUA.Model. Po przeniesieniu plików
spróbujmy skompilować projekt. Pojawi się błąd w pliku PlikXML.cs: w WinRT brakuje
tej wersji metody XDocument.Save, która pozwala na zapisanie pliku XML we wska-
zanej w argumencie ścieżce. Jest to jednak tylko objaw poważniejszego problemu, który
często występuje przy projektowaniu aplikacji dla WinRT i wiąże się z ograniczeniami,
jakie mają aplikacje w dostępie do pamięci urządzeń przenośnych. Na swobodny dostęp
nie ma co liczyć. Możemy zapisać plik w katalogu lokalnym, tam, gdzie przechowywane
są pliki ustawień (zob. rozdział 22.), lub w jednym ze znanych katalogów przeznaczo-
nych na dokumenty, obrazy, muzykę itp. (por. klasa Windows.Storage.KnownFolders). To
drugie rozwiązanie wymaga dodatkowych uprawnień zadeklarowanych w manifeście
aplikacji. Proponuję jednak pozostać przy katalogu „lokalnym” reprezentowanym
przez obiekt Windows.Storage.ApplicationData.Current.LocalFolder. Pliki z tego
katalogu reprezentowane są przez obiekty klasy StorageFile. Natomiast operacje zapisu
i odczytu zaimplementowane są w klasie FileIO. W efekcie w ogóle nie będziemy
272 Część III  Aplikacje uniwersalne (Universal Apps)

używali metod Save i Load klasy XDocument. Zmodyfikowany kod z pliku PlikXML, zapi-
sujący i czytający pliki z katalogu lokalnego, widoczny jest na listingu 25.1. Po wprowa-
dzeniu zaznaczonych na listingu zmian projekt powinien dać się już skompilować.

Aby zachować zgodność z wersją WPF, do trwałego przechowywania danych uży-


wam w tej aplikacji plików XML. Warto jednak „przećwiczyć” także alternatywne
rozwiązanie, oparte na plikach JSON (zob. zadanie na końcu rozdziału). Ten format
staje się obecnie nowym standardem zastępującym XML.

Listing 25.1. Nowa wersja warstwy DAL dla platformy WinRT


using System;
using System.Collections.Generic;
using System.Linq;

using System.Xml.Linq;
using System.Threading.Tasks;
using Windows.Storage;

namespace ZadaniaUA.Model
{
public static class PlikXML
{
public static async Task<bool> ZapiszAsync(string nazwaPliku, Zadania zadania)
{
try
{
XDocument xml = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XComment("Data zapisania: " + DateTime.Now.ToString()),
new XElement("Zadania",
from Zadanie zadanie in zadania
select new XElement("Zadanie",
new XElement("Opis", zadanie.Opis),
new XElement("DataUtworzenia", zadanie.DataUtworzenia),
new XElement("PlanowanaDataRealizacji",
zadanie.PlanowanyTerminRealizacji.ToString()),
new XElement("Priorytet", (byte)zadanie.Priorytet),
new XElement("CzyZrealizowane",
zadanie.CzyZrealizowane.ToString()))
)
);
xml.Save(ścieżkaPliku);
string zawartośćXml = xml.ToString();
StorageFile plik =
await ApplicationData.Current.LocalFolder.CreateFileAsync(
nazwaPliku, CreationCollisionOption.OpenIfExists);
if (plik != null)
{
await FileIO.WriteTextAsync(plik, zawartośćXml);
return true;
}
else return false;
}
catch (Exception exc)
Rozdział 25.  Kolekcje w aplikacji mobilnej 273

{
return false;
}
}

public static async Task<Zadania> CzytajAsync(string nazwaPliku)


{
try
{
XDocument xml = XDocument.Load(ścieżkaPliku);
StorageFile plik =
await ApplicationData.Current.LocalFolder.GetFileAsync(nazwaPliku);
if (plik != null)
{
string zawartośćXml = await FileIO.ReadTextAsync(plik);

XDocument xml = XDocument.Parse(zawartośćXml);


IEnumerable<Zadanie> dane =
from zadanie in xml.Root.Descendants("Zadanie")
select new Zadanie(
zadanie.Element("Opis").Value,
DateTime.Parse(zadanie.Element("DataUtworzenia").Value),
DateTime.Parse(
zadanie.Element("PlanowanaDataRealizacji").Value),
(PriorytetZadania)byte.Parse(
zadanie.Element("Priorytet").Value),
bool.Parse(zadanie.Element("CzyZrealizowane").Value));
Zadania zadania = new Zadania();
foreach (Zadanie zadanie in dane)
zadania.DodajZadanie(zadanie);
return zadania;
}
else return null;
}
catch (Exception exc)
{
return null;
}
}
}
}

Kolejnym krokiem będzie utworzenie w projekcie ZadaniaUA.Shared folderu ModelWidoku


i przeniesienie do niego plików Zadanie.cs i Zadania.cs z projektu ZadaniaWPF w ich
ostatecznej postaci uzyskanej po zmianach wprowadzonych w rozdziale 16. (por. ostat-
nie zmiany widoczne na listingach 15.22 i 16.7). I tym razem możemy zmienić prze-
strzeń nazw z ZadaniaWPF.ModelWidoku na ZadaniaUA.ModelWidoku. Dodatkowo sko-
piujmy do nowego podkatalogu zmodyfikowany na potrzeby aplikacji uniwersalnych
plik RelayCommand.cs z projektu AsystentZakupówUA (por. listing 19.1). Niestety po-
nownie próba kompilacji projektu nie powiedzie się od razu, a do tego błędów będzie
całkiem sporo.

Pierwszą naszą czynnością powinno być usunięcie z modelu widoku (klasa ZadaniaUA.
ModelWidoku.Zadania) polecenia EksportujZadaniaDoPlikuTekstowego i związanego
z nim pola eksportujZadaniaDoPlikuTekstowego. Nie robimy tego dlatego, że zapisu
274 Część III  Aplikacje uniwersalne (Universal Apps)

do pliku tekstowego nie można zrealizować. Jednak na urządzeniu przenośnym taka


funkcjonalność i tak na wiele nam się nie przyda.

Pozostałe zmiany są konsekwencją zmian wprowadzonych w klasie PlikXML. Za-


cznijmy od wywołania metody PlikXML.CzytajAsync, czyli wczytania danych z pliku
XML, i jednoczesnego utworzenia instancji modelu w modelu widoku. Po zmianach
metoda ta zadeklarowana jest z modyfikatorem async. To stwarza pewne trudności,
bo takie metody, jeżeli zwracana przez nie wartość ma być użyta, powinny być zsyn-
chronizowane operatorem await. A to oznacza, że i metoda, w której znajduje się takie
polecenie, powinna być oznaczona modyfikatorem async. Taki modyfikator nie jest
jednak dozwolony w przypadku konstruktora. Zastosujemy wobec tego następujący
wybieg: zdefiniujemy metodę Inicjuj, w której tworzymy model i w której umiesz-
czamy także wywołanie metody KopiujZadania, która przenosi zadania z modelu do
listy zadań typu ObservableCollection. Czynność ta powinna oczywiście być wyko-
nana po inicjacji modelu. Natomiast metodę Inicjuj wywołujemy z konstruktora bez
synchronizacji. W efekcie, zanim plik XML zostanie w pełni odczytany, lista, którą
dodamy do widoku, będzie pozostawała pusta. Ale nie potrwa to długo i raczej nie ma
szans, żeby użytkownik mógł to zauważyć. Z kolei modyfikacja metody PlikXML.
ZapiszAsync wymusza zmiany w poleceniu Zapisz modelu widoku — wyrażenie
lambda przypisywane do akcji Execute klasy RelayCommand oznaczone powinno być
teraz jako async, bo użyty został w nim operator await. Zmodyfikowany kod klasy
Zadania z modelu widoku widoczny jest na listingu 25.2.

Listing 25.2. Zmiany w głównej klasie modelu widoku


using System.Collections.ObjectModel;
using System.Collections.Specialized;

using System.Windows.Input;

namespace ZadaniaUA.ModelWidoku
{
public class Zadania
{
public const string NazwaPlikuXml = "zadania.xml";

//przechowywanie dwóch kolekcji


private Model.Zadania model;
public ObservableCollection<Zadanie> ListaZadań { get; } =
new ObservableCollection<Zadanie>();

private void KopiujZadania()


{
ListaZadań.CollectionChanged -= SynchronizacjaModelu;
ListaZadań.Clear();
foreach (Model.Zadanie zadanie in model)
ListaZadań.Add(new Zadanie(zadanie));
ListaZadań.CollectionChanged += SynchronizacjaModelu;
}

private async void Inicjuj()


{
model = await Model.PlikXML.CzytajAsync(NazwaPlikuXml);
if (model == null) model = new Model.Zadania();
Rozdział 25.  Kolekcje w aplikacji mobilnej 275

//testy - początek
model.DodajZadanie(new Model.Zadanie("Pierwsze", DateTime.Now,
DateTime.Now.AddDays(2), Model.PriorytetZadania.Ważne, false));
model.DodajZadanie(new Model.Zadanie("Drugie", DateTime.Now,
DateTime.Now.AddDays(2), Model.PriorytetZadania.Ważne, false));
model.DodajZadanie(new Model.Zadanie("Trzecie", DateTime.Now,
DateTime.Now.AddDays(1), Model.PriorytetZadania.MniejWażne, false));
model.DodajZadanie(new Model.Zadanie("Czwarte", DateTime.Now,
DateTime.Now.AddDays(3), Model.PriorytetZadania.Krytyczne, false));
model.DodajZadanie(new Model.Zadanie("Piąte", DateTime.Now, new
DateTime(2015, 03, 15, 1, 2, 3), Model.PriorytetZadania.Krytyczne,
false));
model.DodajZadanie(new Model.Zadanie("Szóste", DateTime.Now, new
DateTime(2015, 03, 14, 1, 2, 3), Model.PriorytetZadania.Krytyczne,
false));
//testy - koniec

KopiujZadania();
}

public Zadania()
{
//usunięta pierwotna zawartość konstruktora
Inicjuj();
}

...

public ICommand Zapisz


{
get
{
if (zapiszCommand == null)
zapiszCommand = new RelayCommand(async
argument =>
{ await Model.PlikXML.ZapiszAsync(NazwaPlikuXml, model); });
return zapiszCommand;
}
}

Instrukcje z metody Inicjuj, które dodają zadania do listy zadań przechowywanej


w instancji modelu, potrzebne są tylko do momentu, w którym stan modelu będzie już
zapisywany w pliku XML. Potem należy je usunąć. Prywatne pole ścieżkaPliku
zmieniłem na publiczne pole NazwaPliku. W odróżnieniu od projektu WPF teraz prze-
chowuje ono samą nazwę pliku, a nie całą ścieżkę — katalog jest ustalony na sztywno;
jest nim katalog lokalny. Pole to będzie używane w metodzie OnSuspending klasy App,
w której zapiszemy stan aplikacji, dlatego musi być publiczne. Podobnie jak było
w przypadku aplikacji AsystentZakupówUA, także teraz będziemy potrzebowali dostępu
z tej metody do instancji modelu, aby móc zapisać jego stan. Ponownie wykorzystamy
do tego prosty wybieg polegający na zdefiniowaniu statycznej własności BieżącaInstancja
udostępniającej referencję do ostatnio utworzonego obiektu modelu (listing 25.3).
276 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 25.3. Statyczne pole i inicjujący je konstruktor domyślny, które należy dodać do klasy
ZadaniaUA.Model.Zadania
using System;
using System.Collections.Generic;

using System.Collections;

namespace ZadaniaUA.Model
{
public class Zadania : IEnumerable<Zadanie>
{
public static Zadania BieżącaInstancja { get; private set; } = null;

public Zadania()
{
BieżącaInstancja = this;
}

...

Po tych przygotowaniach możemy przejść do edycji pliku App.xaml.cs, aby zmienić


metodę OnSuspending w taki sposób, by stan modelu, a tym samym stan aplikacji, był
zapisywany do pliku w momencie wstrzymania działania aplikacji. To oznacza dodanie
polecenia wyróżnionego na listingu 25.4. Po jednokrotnym zapisaniu danych można
usunąć polecenia dodające zadania w metodzie ZadaniaUA.ModelWidoku.Zadania. Na
razie nie możemy sprawdzić, czy przechowywanie stanu działa prawidłowo, nawet
korzystając z narzędzi debugowania Visual Studio, bo nie tworzymy jeszcze instancji
modelu widoku, a tym samym także instancji modelu.

Listing 25.4. Zapisywanie stanu aplikacji


private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
await Model.PlikXML.ZapiszAsync(
ModelWidoku.Zadania.NazwaPlikuXml, Model.Zadania.BieżącaInstancja);
deferral.Complete();
}

Współdzielony kod z warstwy widoku


Do projektu współdzielonego dodajmy plik Konwerter.cs. Będziemy do niego stop-
niowo kopiowali konwertery z projektu ZadaniaWPF. Większość z nich będzie wy-
magała drobnych modyfikacji. Pierwsza grupa konwerterów z zaznaczonymi zmianami
widoczna jest na listingu 25.5.
Rozdział 25.  Kolekcje w aplikacji mobilnej 277

Listing 25.5. Konwertery skopiowane z projektu WPF z zaznaczonymi zmianami


using System;

using Windows.UI;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;

namespace ZadaniaUA
{
public class BoolToBrushConverter : IValueConverter
{
public Brush KolorDlaFałszu { get; set; } = new
SolidColorBrush(Colors.White);
public Brush KolorDlaPrawdy { get; set; } = new
SolidColorBrush(Colors.Gray);

public object Convert(object value, Type targetType, object parameter,


string language)
{
bool bvalue = (bool)value;
return !bvalue ? KolorDlaFałszu : KolorDlaPrawdy;
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
throw new NotImplementedException();
}
}

public class PriorytetZadaniaToString : IValueConverter


{
public object Convert(object value, Type targetType, object parameter,
string language)
{
Model.PriorytetZadania priorytetZadania = (Model.PriorytetZadania)value;
return Model.Zadanie.OpisPriorytetu(priorytetZadania);
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
string opisPriorytetu = (value as string).ToLower();
return Model.Zadanie.ParsujOpisPriorytetu(opisPriorytetu);
}
}

public class PriorytetZadaniaToBrush : IValueConverter


{
private Brush kolorMniejWażne = new SolidColorBrush(Colors.Olive);
private Brush kolorWażne = new SolidColorBrush(Colors.Orange);
private Brush kolorKrytyczne = new SolidColorBrush(Colors.OrangeRed);

public object Convert(object value, Type targetType, object parameter,


string language)
{
Model.PriorytetZadania priorytetZadania = (Model.PriorytetZadania)value;
278 Część III  Aplikacje uniwersalne (Universal Apps)

switch (priorytetZadania)
{
case Model.PriorytetZadania.MniejWażne:
return kolorMniejWażne;
case Model.PriorytetZadania.Ważne:
return kolorWażne;
case Model.PriorytetZadania.Krytyczne:
return kolorKrytyczne;
default:
throw new Exception("Nierozpoznany priorytet zadania");
}
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
throw new NotImplementedException();
}
}
}

Zaskakujący może być brak wbudowanego konwertera BooleanToVisibilityConverter,


którego używaliśmy w WPF. Jeszcze bardziej dziwi brak bardzo wygodnego atrybutu
StringFormat, którego używaliśmy do formatowania daty. Pierwszy konwerter możemy
z łatwością napisać sami1. Wykorzystamy tę okazję, żeby go rozszerzyć o własność
OdwróćZależność, która odwróci zależność wartości logicznej i widoczności kontrolki.
Kod konwertera wymaga zadeklarowania przestrzeni nazw Windows.UI.Xaml. Natomiast
atrybut StringFormat zastąpimy konwerterem DateConverter2, który potrzebuje dwóch
kolejnych przestrzeni: Windows.Globalization i Windows.Globalization.DateTime
Formatting. Oba konwertery widoczne są na listingu 25.6.

Listing 25.6. Konwertery zastępujące braki WinRT


public class BooleanToVisibilityConverter : IValueConverter
{
public bool OdwróćZależność { get; set; } = false;

public object Convert(object value, Type targetType, object parameter,


string language)
{
bool b = (bool)value;
if (OdwróćZależność) b = !b;
return b ? Visibility.Visible : Visibility.Collapsed;
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
Visibility v = (Visibility)value;

1
Zwróćmy przy tym uwagę, że w WinRT typ wyliczeniowy Visibility ma tylko dwie wartości: Visible
i Collapsed.
2
Por. http://stackoverflow.com/questions/23030120/how-to-format-a-date-in-an-windows-store-
-universal-app-w8-1-wp8-1.
Rozdział 25.  Kolekcje w aplikacji mobilnej 279

bool wynik = v == Visibility.Visible;


if (OdwróćZależność) wynik = !wynik;
return wynik;
}
}

public class DateConverter : IValueConverter


{
public object Convert(object value, Type targetType, object parameter,
string language)
{
if (value == null || !(value is DateTime)) return null;

DateTime dateTime = (DateTime)value;


DateTimeFormatter dateTimeFormatter = new DateTimeFormatter(
YearFormat.Full,
MonthFormat.Full,
DayFormat.Default,
DayOfWeekFormat.None,
HourFormat.None,
MinuteFormat.None,
SecondFormat.None,
new[] { "pl-PL" },
"PL",
CalendarIdentifiers.Gregorian,
ClockIdentifiers.TwentyFourHour);

return dateTimeFormatter.Format(dateTime);
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
throw new NotImplementedException();
}
}

Lista zadań w widoku


dla Windows Phone 8.1
Tak uzbrojeni, możemy wreszcie wyświetlić listę zadań w widoku. Kod XAML prze-
znaczony dla Windows Phone będzie w dużym stopniu podobny do tego, który przy-
gotowaliśmy dla aplikacji WPF. Należy jednak pamiętać o tym, że w aplikacjach
uniwersalnych nie ma wielu elementów, do których przyzwyczaił nas WPF. Nie ma
multibindingu, wspomnianego już atrybutu StringFormat czy banalnego z pozoru
atrybutu TextDecoration, którego używaliśmy do przekreślenia zrealizowanych zadań.
To powoduje, że różnic między kodem XAML dla Windows Phone i WPF jest całkiem
sporo ― zbyt wiele, aby sensowne było zaznaczanie ich wszystkich na listingu 25.7.
280 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 25.7. Kod XAML widoku dla Windows Phone z zaznaczonymi najbardziej charakterystycznymi
różnicami względem kodu XAML z projektu WPF
<Page
x:Class="ZadaniaUA.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ZadaniaUA"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mw="using:ZadaniaUA.ModelWidoku"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.DataContext>
<mw:Zadania />
</Page.DataContext>
<Page.Resources>
<Style x:Key="stylPrzycisku" TargetType="Button">
<Setter Property="FontSize" Value="13" />
<Setter Property="Margin" Value="0,0,5,0" />
<Setter Property="Width" Value="150" />
</Style>
<local:BooleanToVisibilityConverter x:Key="czyZrealizowaneToVisibility" />
<local:BooleanToVisibilityConverter x:Key="czyNiezrealizowaneToVisibility"
OdwróćZależność="True" />
<local:PriorytetZadaniaToString x:Key="priorytetToString" />
<local:PriorytetZadaniaToBrush x:Key="priorytetToBrush" />
<local:BoolToBrushConverter x:Key="czyZrealizowaneToBrush" />
<local:BoolToBrushConverter x:Key="czyPoTerminieToBrush"
KolorDlaFałszu="Green" KolorDlaPrawdy="Red" />
<local:DateConverter x:Key="dataToString" />
</Page.Resources>
<Grid>
<TextBlock Margin="15,10,0,10" FontSize="20"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="Liczba zadań: ">
<Run Text="{Binding Path=ListaZadań.Count, Mode=OneWay}" />
</TextBlock>
<ListBox x:Name="lbListaZadań" Margin="10,35,10,10"
ItemsSource="{Binding Path=ListaZadań}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="3">
<TextBlock Grid.RowSpan="2" FontSize="30"
Text="{Binding Path=Opis, Mode=OneWay}"
Foreground="{Binding CzyZrealizowane,
Converter={StaticResource
czyZrealizowaneToBrush}}" />
<StackPanel Orientation="Horizontal">
<Button Content="Zrealizowane"
Command="{Binding Path=OznaczJakoZrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding Path=CzyZrealizowane,
Mode=OneWay, Converter={StaticResource
czyNiezrealizowaneToVisibility}}"
Background="Red" />
Rozdział 25.  Kolekcje w aplikacji mobilnej 281

<Button Content="Niezrealizowane"
Command="{Binding
Path=OznaczJakoNiezrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding Path=CzyZrealizowane,
Mode=OneWay, Converter={StaticResource
czyZrealizowaneToVisibility}}"
Background="Green" />
<TextBlock Margin="10,0,10,0" FontSize="13"
Foreground="{ThemeResource
ApplicationForegroundThemeBrush}">
Priorytet: <Run Text="{Binding Path=Priorytet,
Mode=OneWay, Converter={StaticResource
priorytetToString}}" Foreground="{Binding
Path=Priorytet, Mode=OneWay,
Converter={StaticResource priorytetToBrush}}" />
<LineBreak />
Termin: <Run Text="{Binding
Path=PlanowanyTerminRealizacji, Mode=OneWay,
Converter={StaticResource dataToString}}"
Foreground="{Binding Path=CzyZadaniePozostaje
NiezrealizowanePoPlanowanymTerminie, Mode=OneWay,
Converter={StaticResource czyPoTerminieToBrush}}"
/><LineBreak />
Utworzone: <Run Text="{Binding Path=DataUtworzenia,
Mode=OneWay, Converter={StaticResource
dataToString}}" />
</TextBlock>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Control.Margin" Value="3" />
<Setter Property="Control.BorderBrush"
Value="{ThemeResource ApplicationForegroundThemeBrush}" />
<Setter Property="Control.BorderThickness" Value="1" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Page>

Po pierwszym uruchomieniu aplikacji i jej zamknięciu można usunąć z metody ZadaniaUA.


ModelWidoku.Zadania.Inicjuj instrukcje dodające do listy zadań sześć przykładowych
zadań. Teraz powinny być zapisane w pliku XML i odczytane stamtąd przy kolejnych
uruchomieniach aplikacji. Ale uwaga: aby lista zadań została rzeczywiście zapisana,
musi zostać uruchomiona metoda OnSuspending. A jak pamiętamy z rozdziału 24., w try-
bie debugowania wstrzymanie aplikacji możemy uzyskać jedynie korzystając z menu
Lifecycle Events (pozycja Suspend and shutdown).
Rozdział 25.  Kolekcje w aplikacji mobilnej 283

 Zastąpiliśmy konwerter BoolToVisibilityConverter samodzielnie zbudowanym


konwerterem o tej samej nazwie. Nowy konwerter ma dodatkową własność,
która umożliwia odwrócenie zależności między wartością logiczną a widocznością
przycisku. Używamy jej w konwerterze czyNiezrealizowanyToVisibility.
 Brakujący atrybut StringFormat, którego używaliśmy do konwersji daty
na łańcuchy, zastąpiliśmy własnym konwerterem DateConvert.
 W stylach, na przykład w stylu elementu listy ListBox, nie ma wyzwalaczy.
W związku z tym pominąłem całą sekcję Style.Triggers.

Zdarzenie CanExecuteChanged poleceń


To, że do wyświetlania przycisków nie korzystamy z własności IsEnabled przycisku,
nie znaczy, iż problem poleceń OznaczJakoZrealizowane i OznaczJakoNiezrealizowane,
a konkretnie zgłaszania ich zdarzenia CanExecuteChanged, został rozwiązany. Problem
wróci, gdy klikniemy przycisk z etykietą Zrealizowane. Przycisk ten zostanie wpraw-
dzie ukryty i w jego miejsce pojawi się przycisk z etykietą Niezrealizowane, ale nowy
przycisk pozostaje wyłączony (rysunek 25.1, prawy). Jest tak, ponieważ bez mene-
dżera poleceń nie jest zgłaszane zdarzenie CanExecuteChanged, które byłoby sygnałem
do odświeżenia wiązania własności IsEnabled przycisku. Musimy to zrobić sami w trak-
cie wykonywania tych poleceń (listing 25.8). To spowoduje, że przyciski zaczną działać
prawidłowo (rysunek 25.2).

Listing 25.8. Modyfikacje klasy ModelWidoku.Zadanie. Bez menedżera poleceń sami jesteśmy
odpowiedzialni za zgłaszanie zdarzenia CanExecuteChanged
public void WywołajZdarzeniaPoleceń()
{
if (oznaczJakoNiezrealizowane != null)
(oznaczJakoNiezrealizowane as RelayCommand).OnCanExecuteChanged();
if (oznaczJakoZrealizowane != null)
(oznaczJakoZrealizowane as RelayCommand).OnCanExecuteChanged();
}

private ICommand oznaczJakoZrealizowane = null;

public ICommand OznaczJakoZrealizowane


{
get
{
if (oznaczJakoZrealizowane == null)
oznaczJakoZrealizowane = new RelayCommand(
o =>
{
284 Część III  Aplikacje uniwersalne (Universal Apps)

bool poprzedniaWartość = model.CzyZrealizowane;


model.CzyZrealizowane = true;
OnPropertyChanged("CzyZrealizowane",
"CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie");
if (model.CzyZrealizowane != poprzedniaWartość)
WywołajZdarzeniaPoleceń();
},
o =>
{
return !model.CzyZrealizowane;
});
return oznaczJakoZrealizowane;
}
}

ICommand oznaczJakoNiezrealizowane = null;

public ICommand OznaczJakoNiezrealizowane


{
get
{
if (oznaczJakoNiezrealizowane == null)
oznaczJakoNiezrealizowane = new RelayCommand(
o =>
{
bool poprzedniaWartość = model.CzyZrealizowane;
model.CzyZrealizowane = false;
OnPropertyChanged("CzyZrealizowane",
"CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie");
if (model.CzyZrealizowane != poprzedniaWartość)
WywołajZdarzeniaPoleceń();
},
o =>
{
return model.CzyZrealizowane;
});
return oznaczJakoNiezrealizowane;
}
}
286 Część III  Aplikacje uniwersalne (Universal Apps)
288 Część III  Aplikacje uniwersalne (Universal Apps)

Aby dodać do strony aplikacji taki pasek, należy z poziomu kodu XAML ustawić
własność Page.BottomAppBar, przypisując jej instancję klasy CommandBar. Do tego typu
obiektu można dodawać elementy AppBarButton, w których należy ustawić etykietę
(atrybut Label), ikonę (atrybut Icon), a także polecenie i ewentualnie parametr polecenia.
Ikony są na szczęście dostępne „z pudełka”. Wystarczy wybrać jedną z podpowiadanych
przez mechanizm IntelliSense1. Można też użyć własnej.

Na listingu 26.1 widać, że do paska aplikacji dodałem przyciski z etykietami Sortuj,


Dodaj i Usuń. Pierwszy przycisk rozwija podmenu (element MenuFlyout), w którym
możemy wybrać kryterium sortowania. Każdy z elementów podmenu związany jest
z poleceniem SortujZadania udostępnianym przez model widoku. Przycisk Dodaj, który
będzie służył do tworzenia nowego zadania, na razie w ogóle nie jest podpięty.

Listing 26.1. Kod XAML paska aplikacji


<Page.BottomAppBar>
<CommandBar>
<AppBarButton Label="Dodaj" Icon="Add" />
<!—- Przycisk Usuń nie będzie działał -->
<AppBarButton Label="Usuń" Icon="Delete" Command="{Binding UsuńZadanie}"
CommandParameter="{Binding ElementName=lbListaZadań,
Path=SelectedIndex}" />
<AppBarButton Label="Sortuj" Icon="Sort">
<AppBarButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem Text="Sortuj wg priorytetów"
Command="{Binding SortujZadania}"
CommandParameter="True" />
<MenuFlyoutItem Text="Sortuj wg terminów"
Command="{Binding SortujZadania}"
CommandParameter="False" />
</MenuFlyout>
</AppBarButton.Flyout>
</AppBarButton>
</CommandBar>
</Page.BottomAppBar>

Przycisk Usuń, który ma usunąć zaznaczone na liście zadanie, związany jest z pole-
ceniem UsuńZadanie. To polecenie zdefiniowane w klasie ModelWidoku.Zadania za-
kłada, że przez parametr dostarczony będzie indeks zadania do usunięcia. Aby to
oczekiwanie spełnić, atrybut CommandParameter przycisku związany jest z własnością
SelectedIndex kontrolki lbListaZadań (typu ListBox). Jednak to w oczywisty sposób
nasuwające się rozwiązanie nie zadziała! Do polecenia przesyłana jest tylko wartość null.
Źródłem problemu jest kolejność realizacji wiązań: wiązanie polecenia UsuńZadanie
realizowane jest, zanim powstaje instancja klasy ModelWidoku.Zadania, która powinna
być kontekstem wiązania w tym przypadku. Tym razem rozwiązałem go, definiując
w modelu widoku własność WybranyIndeksZadania, z którą wiążę indeks wybranego
przez użytkownika elementu listy. Następnie zmodyfikowałem polecenie UsuńZadanie
tak, żeby zamiast parametru używał tej nowej własności (listing 26.2).

1
Dostępne ikony można przejrzeć na stronie https://msdn.microsoft.com/en-us/library/windows/
apps/xaml/jj841127.aspx.
Rozdział 26.  Pasek aplikacji (app bar) 289

Listing 26.2. Zmodyfikowane polecenie modelu widoku i dodana własność


private int wybranyIndeksZadania = -1;

public int WybranyIndeksZadania


{
get
{
return wybranyIndeksZadania;
}
set
{
wybranyIndeksZadania = value;
if (usuńZadanie != null)
(usuńZadanie as RelayCommand).OnCanExecuteChanged();
}
}

private ICommand usuńZadanie;

public ICommand UsuńZadanie


{
get
{
if (usuńZadanie == null)
usuńZadanie = new RelayCommand(
o =>
{
Zadanie zadanie = ListaZadań[WybranyIndeksZadania];
ListaZadań.Remove(zadanie);
},
o =>
{
return WybranyIndeksZadania >= 0;
});
return usuńZadanie;
}
}

Teraz wystarczy dodać wiązanie między własnością WybranyIndeksZadania modelu


widoku z własnością SelectedIndex kontrolki ListBox (listing 26.3). Pamiętajmy tylko,
że inaczej niż w WPF domyślnym sposobem wiązania w aplikacjach uniwersalnych
jest OneWay (wiązanie jednostronne z modelu widoku do widoku), podczas gdy my
potrzebujemy wiązania w drugą stronę. Ponieważ niedostępne jest wiązanie OneWayTo
Source, wybieramy wiązanie typu TwoWay (listing 26.3). Na koniec można usunąć
nieużywany parametr wiązania przycisku Usuń.

Listing 26.3. Wiązanie kontrolki z nową własnością modelu widoku


<ListBox x:Name="lbListaZadań" Margin="10,35,10,10"
ItemsSource="{Binding Path=ListaZadań}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
SelectedIndex="{Binding WybranyIndeksZadania, Mode=TwoWay}">
290 Część III  Aplikacje uniwersalne (Universal Apps)

Zadania
1. Uruchom aplikację ZadaniaUA także w podprojekcie dla Windows 8.1.
To oznacza konieczność skopiowania kodu XAML widoku do pliku MainPage.xaml
z tego projektu. Jeżeli pliki są takie same, można je zastąpić jednym plikiem
w projekcie współdzielonym.
2. W projekcie aplikacji dla systemu Windows 8.1 zastąp dolny pasek aplikacji
górnym.
3. Do paska aplikacji dodaj przycisk typu AppBarToggleButton z etykietą
Zrealizowane. Przycisk tego typu może być zaznaczany. Przycisk powinien być
widoczny (własność Visibility) tylko wówczas, gdy w kontrolce ListBox
zaznaczone jest zadanie. Natomiast jego stan (własność IsChecked) związany
powinien być z tym, czy wybrane zadanie jest zrealizowane. Najwygodniejszym
sposobem wykonania tego ćwiczenia będzie zdefiniowanie w modelu widoku
jeszcze jednej własności typu bool, która udostępnia wartość ListaZadań[Wybrany
IndeksZadania].CzyZrealizowane. Aby widok był powiadamiany o zmianie tej
wartości, model widoku musi implementować interfejs INotifyPropertyChanged,
a wymuszane przez ten interfejs zdarzenie PropertyChanged musi być odpowiednio
wywoływane. To z kolei wymaga zdefiniowania w klasie Zadanie zdarzenia
informującego o zmianie własności CzyZrealizowane, nazwijmy je
CzyZrealizowaneZmienione, i użycie go do przekazania informacji o zmianie
tej własności do klasy Zadania.
4. Dodaj do paska aplikacji przycisk zapisujący aktualną listę zadań. Wykorzystaj
polecenie Zapisz zdefiniowane w modelu widoku. Dodaj do tego polecenia
akcję CanExecute sprawdzającą, czy dane zostały zmienione od uruchomienia
aplikacji lub od ostatniego zapisu.
Rozdział 27.
Okna dialogowe
w aplikacjach
Windows Phone

Standardowe okna dialogowe


W aplikacji WPF, w przypadku, gdy skasowane ma zostać zadanie, które nie zostało
jeszcze zrealizowane, wyświetlamy okno dialogowe z prośbą o potwierdzenie. Aby to
zrobić w zgodzie ze wzorcem MVVM, rozwinęliśmy opisany w rozdziale 16. zbiór
kontrolek odpowiedzialnych za wyświetlanie okien dialogowych i uruchamianie poleceń
zależnie od klikniętego w tym oknie dialogowym przycisku. Sprawdźmy, w jakim zakre-
sie to rozwiązanie zadziała w aplikacji uniwersalnej.

Dodajmy do współdzielonego projektu nowy pusty plik OknaDialogowe.cs. Będziemy


do niego stopniowo kopiować kod przygotowany w rozdziale 16. Zacznijmy od klasy
abstrakcyjnej DialogBox i jej prostej klasy potomnej SimpleMessageDialogBox (listing
27.1), która wyświetla okno z przekazanym w parametrze polecenia łańcuchem. Poza
zmianą deklarowanych przestrzeni nazw zmianie uległo tylko samo polecenie execute
odpowiedzialne za wyświetlenie okna dialogowego. Klasę MessageBox i jej metodę Show
zastąpiliśmy klasą MessageDialog i asynchroniczną metodą ShowAsync.

Listing 27.1. Abstrakcja klasy obsługującej okna dialogowe i najprostsza jej realizacja przeniesione do
aplikacji uniwersalnej
using System;
using System.ComponentModel;
using System.Windows.Input;
using Windows.UI.Popups;
using Windows.UI.Xaml;
292 Część III  Aplikacje uniwersalne (Universal Apps)

namespace ZadaniaUA
{
public abstract class DialogBox : FrameworkElement, INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string nazwaWłasności)


{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(nazwaWłasności));
}
#endregion

protected Action<object> execute = null;

public string Caption { get; set; } = null;

protected ICommand show;

public virtual ICommand Show


{
get
{
if (show == null) show = new RelayCommand(execute);
return show;
}
}
}

public class SimpleMessageDialogBox : DialogBox


{
public SimpleMessageDialogBox()
{
execute =
async o =>
{
MessageBox.Show((string)o, Caption);
await new MessageDialog((string)o, Caption).ShowAsync();
};
}
}
}

Sprawdźmy ją, dodając do paska aplikacji kolejny przycisk z etykietą O... wyświetlający
informacje o programie (listing 27.2). Nie możemy elementu SimpleMessageDialogBox
umieścić w zawartości paska aplikacji (elementu CommandBar), bo dopuszcza ona tylko
przyciski. Zamiast tego umieściliśmy go w zasobach paska, ale odwołujemy się do
niego poprzez nazwę. Efekt widoczny jest na rysunku 27.1.

Listing 27.2. Przykład użycia klasy SimpleMessageDialog


<Page.BottomAppBar>
<CommandBar>
<CommandBar.Resources>
<local:SimpleMessageDialogBox x:Name="simpleMessageDialogBox"
Caption="Zadania" />
294 Część III  Aplikacje uniwersalne (Universal Apps)

Listing 27.3. Abstrakcja klasy okna dialogowego z poleceniami


public abstract class CommandDialogBox : DialogBox
{
public override ICommand Show
{
get
{
if (show == null)
show = new RelayCommand(
o =>
{
ExecuteCommand(CommandBefore, CommandParameter);
execute(o);
ExecuteCommand(CommandAfter, CommandParameter);
});
return show;
}
}

public static DependencyProperty CommandParameterProperty =


DependencyProperty.Register("CommandParameter", typeof(object),
typeof(CommandDialogBox),
new PropertyMetadata(null));

public object CommandParameter


{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}

protected static void ExecuteCommand(ICommand command, object commandParameter)


{
if (command != null)
if (command.CanExecute(commandParameter))
command.Execute(commandParameter);
}

public static DependencyProperty CommandBeforeProperty =


DependencyProperty.Register("CommandBefore", typeof(ICommand),
typeof(CommandDialogBox),
new PropertyMetadata(null));

public ICommand CommandBefore


{
get
{
return (ICommand)GetValue(CommandBeforeProperty);
}
set
{
SetValue(CommandBeforeProperty, value);
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 295

}
}

public static DependencyProperty CommandAfterProperty =


DependencyProperty.Register("CommandAfter", typeof(ICommand),
typeof(CommandDialogBox),
new PropertyMetadata(null));

public ICommand CommandAfter


{
get
{
return (ICommand)GetValue(CommandAfterProperty);
}
set
{
SetValue(CommandAfterProperty, value);
}
}
}

public class NotificationDialogBox : CommandDialogBox


{
public NotificationDialogBox()
{
execute =
async o =>
{
await new MessageDialog((string)o, Caption).ShowAsync();
};
}
}

Listing 27.4. Zmiany w szablonie elementu listy związane z użyciem okna dialogowego
<Button Content="Zrealizowane"
Command="{Binding Path=OznaczJakoZrealizowane}"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding Path=CzyZrealizowane, Mode=OneWay,
Converter={StaticResource czyNiezrealizowaneToVisibility}}"
Background="Red" />
<local:NotificationDialogBox x:Name="notificationDialogBox1" Caption="Zadania"
CommandBefore="{Binding Path=OznaczJakoZrealizowane}" />
<Button Content="Zrealizowane"
Command="{Binding ElementName=notificationDialogBox1, Path=Show}"
CommandParameter="Zadanie oznaczone jako zrealizowane"
Style="{StaticResource stylPrzycisku}"
Visibility="{Binding Path=CzyZrealizowane, Mode=OneWay,
Converter={StaticResource czyNiezrealizowaneToVisibility}}"
Background="Red" />

Na tej samej zasadzie możemy spróbować powiadomić o usunięciu zadania po klik-


nięciu przycisku Usuń w pasku aplikacji. Umieśćmy w zasobach elementu CommandBar
instancję klasy NotificationDialogBox. Zdefiniowane w nim polecenie CommandBefore,
wykonywane przed pokazaniem okna dialogowego, usuwa zadanie. Natomiast polecenie
296 Część III  Aplikacje uniwersalne (Universal Apps)

przycisku powinno teraz jedynie uaktywniać obiekt NotificationDialogBox. Odpo-


wiedzialny za to kod XAML widoczny jest na listingu 27.5.

Listing 27.5. Nieudana próba użycia okna dialogowego w pasku aplikacji


<Page.BottomAppBar>
<CommandBar>
<CommandBar.Resources>
<local:SimpleMessageDialogBox x:Name="simpleMessageDialogBox"
Caption="Zadania" />
<local:NotificationDialogBox x:Name="notificationDialogBox1"
Caption="Zadania"
CommandBefore="{Binding Path=UsuńZadanie}" />
</CommandBar.Resources>
<AppBarButton Label="Sortuj" Icon="Sort">
...
<AppBarButton Label="Usuń" Icon="Delete"
Command="{Binding ElementName=notificationDialogBox1,
Path=Show}"
CommandParameter="Zadanie zostało usunięte" />
<AppBarButton Label="O..." Icon="Document"
Command="{Binding ElementName=simpleMessageDialogBox,
Path=Show}"
CommandParameter="ZadaniaUA&#x0a;(c) Jacek Matulewski
2015&#x0a;WWW: http://www.fizyka.umk.pl/~jacek" />
</CommandBar>
</Page.BottomAppBar>

To jednak nie zadziała! Jest to objaw tego samego problemu, z którym borykaliśmy
się już wcześniej, zmuszając do działania przycisk Usuń z paska aplikacji: wiązanie
polecenia UsuńZadanie w obiekcie NotificationDialogBox realizowane jest, zanim
powstaje instancja klasy ModelWidoku.Zadania, która powinna być kontekstem wiązania
w tym przypadku. Jak wobec tego wymusić wcześniejsze utworzenie modelu widoku?
Można go utworzyć w zasobach aplikacji, co nie przeszkadza użyciu utworzonej tam
instancji jako kontekstu danych strony. Oto czynności, jakie należy w tym celu wykonać:
1. Do kodu XAML pliku App.xaml dodajmy element tworzący instancję klasy
ModelWidoku.Zadania (listing 27.6).

Listing 27.6. Dodawanie instancji modelu widoku w zasobach aplikacji


<Application
x:Class="ZadaniaUA.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mw="using:ZadaniaUA.ModelWidoku"
xmlns:local="using:ZadaniaUA">
<Application.Resources>
<mw:Zadania x:Key="modelWidoku" x:Name="modelWidoku" />
</Application.Resources>
</Application>
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 297

2. Następnie w „nagłówku” strony (plik MainPage.xaml) zmieniamy sposób


przypisywania instancji modelu widoku do własności Page.DataContext
(listing 27.7).

Listing 27.7. Nowy sposób ustalenia kontekstu wiązania na stronie


<Page
...
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mw="using:ZadaniaUA.ModelWidoku"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{StaticResource modelWidoku}">
<Page.DataContext>
<mw:Zadania />
</Page.DataContext>
<Page.Resources>
...

3. Teraz w elemencie NotificationDialogBox możemy związać polecenie


bezpośrednio z elementem z zasobów aplikacji, a nie z kontekstem danych
strony. Pomimo że w obu przypadkach chodzi o ten sam obiekt, zapewni to,
iż obiekt, z którym chcemy się wiązać, na pewno będzie dostępny w momencie
wiązania (własność DataContext jest wówczas jeszcze równa null). Pokazuje
to listing 27.8.

Listing 27.8. Pasek aplikacji z jawnym wiązaniem do modelu widoku


<Page.BottomAppBar>
<CommandBar>
<CommandBar.Resources>
<local:SimpleMessageDialogBox x:Name="simpleMessageDialogBox"
Caption="Zadania" />
<local:NotificationDialogBox x:Name="notificationDialogBox1"
Caption="Zadania"
CommandBefore="{Binding Source={StaticResource modelWidoku}, Path=UsuńZadanie}" />
</CommandBar.Resources>
<AppBarButton Label="Sortuj" Icon="Sort">
...
<AppBarButton Label="Usuń" Icon="Delete"
Command="{Binding ElementName=notificationDialogBox1,
Path=Show}"
CommandParameter="Zadanie zostało usunięte" />
<AppBarButton Label="O..." Icon="Document"
Command="{Binding ElementName=simpleMessageDialogBox,
Path=Show}"
CommandParameter="ZadaniaUA&#x0a;(c) Jacek Matulewski
2015&#x0a;WWW: http://www.fizyka.umk.pl/~jacek" />
</CommandBar>
</Page.BottomAppBar>

Warto sprawdzić, czy teraz polecenie działa prawidłowo. Niestety przycisk nie re-
aguje już na to, czy wybrany został element w kontrolce ListBox. Łatwo to jednak na-
prawić (zob. zadanie 2. na końcu rozdziału).
298 Część III  Aplikacje uniwersalne (Universal Apps)

Kolejna klasa opisana w rozdziale 16., a mianowicie MessageDialogBox, wymaga więk-


szych zmian przy przenoszeniu do aplikacji uniwersalnej. Wszystkie te zmiany są wy-
różnione na listingu 27.9. Przede wszystkim musimy zdefiniować typ wyliczeniowy
opisujący zestaw przycisków, jakie mają być pokazywane w oknie dialogowym, oraz
typ wyliczeniowy, który pozwoli nam zidentyfikować, który z tych przycisków został
wybrany przez użytkownika. Oba typy zostały zdefiniowane na wzór typów wylicze-
niowych z WPF, których używaliśmy w klasie MessageDialogBox z rozdziału 16.
Kolejną różnica jest to, że zestaw przycisków tworzymy sami i że w Windows Phone
w oknie dialogowym mogą być wyświetlane tylko dwa przyciski. Dlatego w typie
MessageDialogBoxButton nie ma wartości YesNoCancel.

Listing 27.9. Zmiany w klasie MessageDialogBox konieczne przy przenoszeniu do aplikacji uniwersalnych
public class MessageDialogBox : CommandDialogBox
{
public enum MessageDialogBoxResult { None = 0, OKOrClose = 1, Cancel = 2,
Yes = 6, No = 7 }

public enum MessageDialogBoxButton { Close = 0, OKCancel = 1, YesNo = 4 }

public MessageDialogBoxResult? LastResult { get; protected set; }


public MessageDialogBoxButton Buttons { get; set; } =
MessageDialogBoxButton.Close;

public bool IsLastResultYes


{
get
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageDialogBoxResult.Yes;
}
}

...

public bool IsLastResultOKOrClose


{
get
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageDialogBoxResult.OKOrClose;
}
}

public void WykonajPolecenie(IUICommand polecenieUI)


{
LastResult = (MessageDialogBoxResult)polecenieUI.Id;
OnPropertyChanged("LastResult");
switch (LastResult)
{
case MessageDialogBoxResult.Yes:
OnPropertyChanged("IsLastResultYes");
ExecuteCommand(CommandYes, CommandParameter);
break;
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 299

case MessageDialogBoxResult.No:
OnPropertyChanged("IsLastResultNo");
ExecuteCommand(CommandNo, CommandParameter);
break;
case MessageDialogBoxResult.Cancel:
OnPropertyChanged("IsLastResultCancel");
ExecuteCommand(CommandCancel, CommandParameter);
break;
case MessageDialogBoxResult.OKOrClose:
OnPropertyChanged("IsLastResultOKOrClose");
ExecuteCommand(CommandOKOrClose, CommandParameter);
break;
}
}

public MessageDialogBox()
{
execute = async o =>
{
LastResult = MessageBox.Show((string)o, Caption, Buttons, Icon);
MessageDialog messageDialog = new MessageDialog((string)o, Caption);
switch(Buttons)
{
case MessageDialogBoxButton.Close:
messageDialog.Commands.Add(
new UICommand("OK", WykonajPolecenie,
MessageDialogBoxResult.OKOrClose));
messageDialog.DefaultCommandIndex = 0;
break;
case MessageDialogBoxButton.OKCancel:
messageDialog.Commands.Add(
new UICommand("OK", WykonajPolecenie,
MessageDialogBoxResult.OKOrClose));
messageDialog.Commands.Add(
new UICommand("Cancel", WykonajPolecenie,
MessageDialogBoxResult.Cancel));
messageDialog.DefaultCommandIndex = 1;
messageDialog.CancelCommandIndex = 1;
break;
case MessageDialogBoxButton.YesNo:
messageDialog.Commands.Add(
new UICommand("Yes", WykonajPolecenie,
MessageDialogBoxResult.Yes));
messageDialog.Commands.Add(
new UICommand("No", WykonajPolecenie,
MessageDialogBoxResult.No));
messageDialog.DefaultCommandIndex = 1;
break;
}
await messageDialog.ShowAsync();
};
}

public static DependencyProperty CommandYesProperty =


DependencyProperty.Register("CommandYes", typeof(ICommand),
typeof(MessageDialogBox),
new PropertyMetadata(null));
300 Część III  Aplikacje uniwersalne (Universal Apps)

...
public static DependencyProperty CommandOKOrCloseProperty =
DependencyProperty.Register("CommandOKOrClose", typeof(ICommand),
typeof(MessageDialogBox),
new PropertyMetadata(null));

public ICommand CommandYes


{
get
{
return (ICommand)GetValue(CommandYesProperty);
}
set
{
SetValue(CommandYesProperty, value);
}
}

...

public ICommand CommandOKOrClose


{
get
{
return (ICommand)GetValue(CommandOKOrCloseProperty);
}
set
{
SetValue(CommandOKOrCloseProperty, value);
}
}
}

Aby sprawdzić działanie nowej klasy, użyjmy jej do wyświetlenia okna dialogowego
z prośbą o potwierdzenie chęci usunięcia zadania z listy (listing 27.10).

Listing 27.10. Przykład użycia klasy MessageDialogBox


<Page.BottomAppBar>
<CommandBar>
<CommandBar.Resources>
...
<local:MessageDialogBox
x:Name="questionYesNo"
Caption="Zadania" Buttons="YesNoCancel"
CommandYes="{Binding Source={StaticResource modelWidoku},
Path=UsuńZadanie}" />
</CommandBar.Resources>
...
<AppBarButton Label="Usuń" Icon="Delete"
Command="{Binding ElementName=questionYesNo, Path=Show}"
CommandParameter="Czy jesteś pewien, że chcesz usunąć
zadanie?" />
...
</CommandBar>
</Page.BottomAppBar>
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 301

Okna dialogowe z dowolną


zawartością w Windows Phone
W tym rozdziale pominąłem okna dialogowe z warunkiem decydującym o wyborze
pliku i okna dialogowe wyboru plików omówione w przypadku WPF w rozdziale 16.
(por. zadania na końcu rozdziału). Ich przeniesienie do aplikacji uniwersalnej wymaga
podobnych czynności jak te opisane wyżej. Chciałbym jednak przenieść do aplikacji
uniwersalnej okno dialogowe z dowolną zawartością, w którym umieścimy formularz
pozwalający na utworzenie nowego zadania.

W pasku aplikacji nadal obecny jest przycisk Dodaj, do którego nie jest przypisane
żadne polecenie modelu widoku. Ze względu na niewielką ilość miejsca na ekranach
smartfonów rozsądnym rozwiązaniem wydaje się umieszczenie formularza umożli-
wiającego wpisanie opisu, terminu realizacji i priorytetu nowego zadania na osobnej
stronie lub w oknie dialogowym. Świetnie nadaje się do tego klasa ContentDialog.
Niestety jej pełna funkcjonalność dostępna jest tylko w projekcie dla Windows Phone.
W tym projekcie istnieje nawet szablon pliku ContentDialog, który pozwala dodać do
projektu plik zawierający klasę dziedziczącą z ContentDialog i projektować jej za-
wartości myszką, identycznie jak w przypadku strony. Ja chciałbym jednak zapropo-
nować inne rozwiązanie, zgodne z tym, które opisałem wyżej i w rozdziale 16. Nadal
będzie to rozwiązanie przeznaczone tylko dla systemu Windows Phone, ponieważ
niektóre własności klasy ContentDialog, których użyjemy w projekcie, są dostępne tylko
w tym systemie. Z tego powodu kodu nowej klasy nie umieścimy w projekcie współ-
dzielonym, lecz w projekcie dla Windows Phone 8.1. Z kolei systemu Windows Phone
dotyczy ograniczenie związane z tą klasą, którego nie ma w Windows 8.1, a mianowicie
okno dialogowe tego typu może mieć tylko dwa przyciski.

Zacznijmy od zdefiniowania klasy CustomContentDialogBox (listing 27.11) w nowym


pliku OknaDialogowe.cs dodanym do projektu dla Windows Phone 8.1. Klasa ta dzie-
dziczy ze zdefiniowanej wcześniej klasy CommandDialogBox, do której dodajemy wła-
sności umożliwiające reakcje na kliknięcie podstawowego i dodatkowego przycisku
okna dialogowego oraz własności pozwalające na ustalenie wysokości okna, etykiet na
obu przyciskach oraz oczywiście zawartości okna. Możliwe jest także wskazanie kon-
tekstu danych zawartości okna, co umożliwia wiązania jego elementów.

Listing 27.11. Zawartość nowego pliku OknaDialogwe.cs dodanego do projektu dla Windows Phone 8.1
using System;
using System.Collections.Generic;
using System.Text;

using System.Windows.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;

namespace ZadaniaUA
{
[ContentProperty(Name="DialogContent")]
302 Część III  Aplikacje uniwersalne (Universal Apps)

public class CustomContentDialogBox : CommandDialogBox


{
public ContentDialogResult LastResult { get; protected set; }

public double DialogHeight { get; set; }


public object DialogContent { get; set; }
public object DialogContentDataContext { get; set; }

public string PrimaryButtonText { get; set; }


public string SecondaryButtonText { get; set; }

//private ContentDialog dialog = null;

public CustomContentDialogBox()
{
DialogHeight = 480;
PrimaryButtonText = "OK";
SecondaryButtonText="Cancel";

execute =
async o =>
{
ContentDialog dialog = new ContentDialog();
dialog.Height = DialogHeight;
dialog.Title = Caption;
dialog.Content = DialogContent;
dialog.DataContext = DialogContentDataContext;
dialog.PrimaryButtonText = PrimaryButtonText;
dialog.PrimaryButtonCommand = CommandPrimary;
dialog.PrimaryButtonCommandParameter = CommandParameter;
dialog.SecondaryButtonText = SecondaryButtonText;
dialog.SecondaryButtonCommand = CommandSecondary;
dialog.SecondaryButtonCommandParameter = CommandParameter;
LastResult = await dialog.ShowAsync();
OnPropertyChanged("LastResult");
dialog.Content = null;
dialog = null;
};
}

public static DependencyProperty CommandPrimaryProperty =


DependencyProperty.Register("CommandPrimary", typeof(ICommand),
typeof(CustomContentDialogBox), new PropertyMetadata(null));
public static DependencyProperty CommandSecondaryProperty =
DependencyProperty.Register("CommandSecondary", typeof(ICommand),
typeof(CustomContentDialogBox), new PropertyMetadata(null));

public ICommand CommandPrimary


{
get
{
return (ICommand)GetValue(CommandPrimaryProperty);
}
set
{
SetValue(CommandPrimaryProperty, value);
}
}
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 303

public ICommand CommandSecondary


{
get
{
return (ICommand)GetValue(CommandSecondaryProperty);
}
set
{
SetValue(CommandSecondaryProperty, value);
}
}
}
}

Warto zwrócić uwagę na wyrażenie lambda przypisywane do akcji execute w kon-


struktorze klasy. Musi ono być asynchroniczne ze względu na to, że klasa ContentDialog
udostępnia jedynie asynchroniczną metodę ShowAsync pokazującą okno. Ze zwracanego
przez tę metodę zadania Task<ContentDialogResult> za pomocą operatora await wyłu-
skujemy parametr informujący o klikniętym przycisku i przypisujemy go do własności
LastResult zdefiniowanej w nowej klasie.

Sposób wykorzystania nowej klasy pozostaje taki sam jak klas zdefiniowanych wyżej:
instancję klasy, tym razem klasy CustomContentDialogBox, umieszczamy w zasobach
paska aplikacji i odwołujemy się do jej polecenia Show w poleceniu przycisku (listing
27.12). Z kolei w zasobach elementu okna dialogowego umieszczamy konwerter typu
wyliczeniowego Model.PriorytetZadania do liczby całkowitej int (listing 27.13), który
jest wykorzystywany w kontrolce ComboBox umieszczonej w zawartości okna dialo-
gowego. Ponieważ kontrolki zawartości wiązane są z własnościami modelu widoku,
używamy możliwości wskazania kontekstu danych, aby umożliwić to wiązanie. Nowe
okno dialogowe ma dwa przyciski: Dodaj i Anuluj. Z pierwszym z nich wiążemy zmody-
fikowane polecenie DodajZadanie modelu widoku. Ze względu na brak multibindingu
nie możemy tworzyć zadania w konwerterze na podstawie danych zebranych w for-
mularzu i gotowego przesyłać jako parametru polecenia. Zamiast tego dodałem do mo-
delu widoku kilka nowych własności, z którymi elementy formularza są związane,
a zadanie tworzone jest w akcji execute polecenia DodajZadanie. Zmodyfikowane pole-
cenie oraz nowe własności modelu widoku przedstawia listing 27.14.

Listing 27.12. Zmodyfikowany kod paska aplikacji


<Page.BottomAppBar>
<CommandBar>
<CommandBar.Resources>
...
<local:CustomContentDialogBox
x:Name="dodajZadanieDialogBox" Caption="Zadania" DialogHeight="400"
DialogContentDataContext="{StaticResource modelWidoku}"
PrimaryButtonText="Dodaj" SecondaryButtonText="Anuluj"
CommandPrimary="{Binding Source={StaticResource modelWidoku},
Path=DodajZadanie}">
<local:CustomContentDialogBox.Resources>
<local:PriorytetZadaniaToIndex x:Key="priorytetToIndex" />
</local:CustomContentDialogBox.Resources>
304 Część III  Aplikacje uniwersalne (Universal Apps)

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="100" />
<RowDefinition Height="150" />
</Grid.RowDefinitions>
<TextBox x:Name="tbOpis" Grid.Row="0" Grid.ColumnSpan="2"
Header="Opis:" Margin="10"
Text="{Binding Path=OpisNowegoZadania, Mode=TwoWay}"/>
<ComboBox Grid.Row="1" Grid.Column="0" Grid.RowSpan="2"
x:Name="cbPriorytet" Header="Priorytet:" Margin="10"
SelectedIndex="{Binding Path=PriorytetNowegoZadania,
Mode=TwoWay,
Converter={StaticResource priorytetToIndex}}">
<ComboBoxItem>Mniej ważne</ComboBoxItem>
<ComboBoxItem IsSelected="True">Ważne</ComboBoxItem>
<ComboBoxItem>Krytyczne</ComboBoxItem>
</ComboBox>
<DatePicker Grid.Row="1" Grid.Column="1"
x:Name="dpTerminRealizacji"
Header="Termin realizacji:" Margin="10"
Language="pl-PL"
MonthFormat="{}{month.full}"
Date="{Binding Path=PlanowanyTerminRealizacjiNowegoZadania, Mode=TwoWay}" />
</Grid>
</local:CustomContentDialogBox>
</CommandBar.Resources>
...
<AppBarButton Label="Dodaj" Icon="Add"
Command="{Binding ElementName=dodajZadanieDialogBox, Path=Show}" />
...
</CommandBar>
</Page.BottomAppBar>

Listing 27.13. Nowy konwerter dodany do pliku Konwertery.cs z projektu współdzielonego


public class PriorytetZadaniaToIndex : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
string language)
{
Model.PriorytetZadania priorytetZadania = (Model.PriorytetZadania)value;
return (byte)priorytetZadania;
}

public object ConvertBack(object value, Type targetType, object parameter,


string language)
{
byte index = (byte)(int)value;
return (Model.PriorytetZadania)index;
}
}
Rozdział 27.  Okna dialogowew aplikacjach Windows Phone 305

Listing 27.14. Zmiany w klasie Zadania z modelu widoku


public string OpisNowegoZadania { get; set; }
public Model.PriorytetZadania PriorytetNowegoZadania { get; set; }
public DateTime PlanowanyTerminRealizacjiNowegoZadania { get; set; }

private void CzyśćWłasnościNowegoZadania()


{
OpisNowegoZadania = "";
PriorytetNowegoZadania = Model.PriorytetZadania.Ważne;
PlanowanyTerminRealizacjiNowegoZadania = DateTime.Now;
}

private ICommand dodajZadanie;

public ICommand DodajZadanie


{
get
{
if (dodajZadanie == null)
{
dodajZadanie = new RelayCommand(
o =>
{
Zadanie zadanie = new Zadanie(OpisNowegoZadania, DateTime.Now,
PlanowanyTerminRealizacjiNowegoZadania,
PriorytetNowegoZadania, false);
ListaZadań.Add(zadanie);
CzyśćWłasnościNowegoZadania();
},
o =>
{
return string.IsNullOrWhiteSpace(OpisNowegoZadania) &&
PlanowanyTerminRealizacjiNowegoZadania > DateTime.Now;
});
}
return dodajZadanie;
}
}

Zadania
1. Zdefiniuj w modelu widoku własność CzyWybranoZadanie, która będzie
informować o tym, czy wartość własności WybranyIndeksZadania jest nieujemna.
Z własnością tą należy związać własność IsEnabled przycisku Usuń z paska
aplikacji. Zwróć uwagę na konieczność wdrożenia mechanizmu powiadamiania
(interfejs INotifyPropertyChanged) w modelu widoku.
2. Ustaw okna dialogowe MessageDialogBox i NotificationDialogBox w „łańcuch”
(por. rozdział 16.), w którym pierwsza klasa pokaże okno dialogowe z pytaniem
o usunięcie zadania, a druga potwierdzi jego usunięcie.
306 Część III  Aplikacje uniwersalne (Universal Apps)

3. Przygotuj klasy ConditionalMessageDialogBox i klasy pokazujące okna


dialogowe wyboru pliku na wzór klas opisanych w rozdziale 16.
4. Uruchom aplikację ZadaniaUA z oknami dialogowymi także w podprojekcie dla
Windows 8.1. Zmiany widoku powinny być stosunkowo niewielkie, ale okno
dialogowe z dowolną zawartością będzie trzeba przygotować w inny sposób.
Rozdział 28.
Aplikacje uniwersalne
w Windows 10
Latem 2015 roku Microsoft udostępnił nową wersję systemu operacyjnego Windows
o numerze 10. Na razie tylko na komputery PC, ale niebawem będzie ona dostępna
także na urządzeniach mobilnych, a potem na konsoli Xbox i innych urządzeniach
obsługiwanych przez Microsoft. W przypadku komputerów PC aplikacje uruchamiane
w środowisku WinRT wyglądają inaczej niż w Windows 8.1 – znowu działają w oknach.
Co więcej, w ogóle zrezygnowano z tak zwanego ekranu Start, który został zastąpiony
przez przywrócone w Windows 10, ale zmodyfikowane menu Start.

Środowisko Visual Studio 2015 umożliwia przygotowywanie aplikacji uniwersalnych


dla nowej wersji systemu. Chciałbym to zaprezentować na przykładzie dobrze nam
już znanej aplikacji AsystentZakupów. Moim celem nie jest oczywiście wyczerpujące
wprowadzenie do aplikacji nowego typu, lecz raczej przekonanie Czytelnika, że wzo-
rzec MVVM jest właściwym wyborem także w tego rodzaju aplikacjach. Wielokrot-
nie tu podkreślaną zaletą tego wzorca jest całkowita autonomiczność jego najniższej
warstwy, czyli modelu i w znacznym zakresie również modelu widoku. W praktyce
oznacza to, oczywiście trywializując, że czynności opisane w niniejszym rozdziale ogra-
niczą się do skopiowania do nowego projektu plików modelu i modelu widoku, za-
adaptowania widoku i kompilacji całości.

Projekty aplikacji uniwersalnych dla Windows 10 nie składają się już z trzech pod-
projektów, z jakimi mieliśmy do czynienia w Windows 8.1, to jest projektu współdzielo-
nego oraz dwóch projektów przeznaczonych dla Windows na PC i Windows Phone.
Dzięki wprowadzeniu zunifikowanej platformy UWP (ang. Universal Windows Platform)1
rozwijamy tylko jeden projekt, który będzie uruchamiany na tej platformie. Co cie-
kawe, platforma ta ma obejmować nie tylko komputery PC i smartfony, ale również
konsolę Xbox, urządzenia typu IoT (ang. Internet of Things) i Surface Hub – Micro-
softową wersję rozszerzonej rzeczywistości. Na wszystkich tych urządzeniach mamy
zagwarantowany „rdzenny” zestaw platformy UWP, ale nic nie stoi na przeszkodzie,
1
Wprowadzenie do UWP można znaleźć na stronie https://msdn.microsoft.com/en-us/library/windows/
apps/dn894631.aspx.
308 Część III  Aplikacje uniwersalne (Universal Apps)

aby rozszerzać go na konkretnej rodzinie urządzeń o specyficznej dla tych urządzeń


funkcjonalności. Różnorodność urządzeń, na których można uruchomić aplikację, jest
wyzwaniem dla programistów i projektantów interfejsu użytkownika czy w ogóle sze-
rzej: projektantów UX, wymusza bowiem konieczność przygotowania kodu XAML,
który jest w stanie adaptować się do różnej wielkości ekranów. Zaznaczę ten problem
poniżej, wprowadzając do widoku dwa nowe rozwiązania dodane do XAML: nowy
kontener RelativePanel i menedżer stanów wizualnych (znacznik VisualStateManager).

Domyślnie jedyną drogą instalacji nowych aplikacji dla platformy WinRT w Windows 10
jest pobranie ich ze sklepu. Aby móc uruchamiać projektowane przez siebie w Visual
Studio aplikacje uniwersalne, należy umożliwić to w ustawieniach systemu (zob.
rysunek 28.1).

Rysunek 28.1.
Ustawienia
„Dla deweloperów”
znajdziemy
w nowym
centrum ustawień,
a nie w powoli
zastępowanym
panelu sterowania

Zacznijmy od stworzenia projektu:


1. Z menu File, New, Project wybieramy kategorię Visual C#, Windows, Universal
i wskazujemy szablon Blank App (Universal Windows) (rysunek 28.2). Nowy
projekt nazwijmy „AsystentZakupówUWP”.

Co ciekawe, aplikacje tego typu można tworzyć w VS2015 nie tylko na komputerach
z systemem Windows 10, ale nawet na tych z Windows 7. Trzeba jednak wówczas
skonfigurować zdalne połączenie z komputerem z Windows 10.

2. Zgodnie z zapowiedzią rozpocznijmy od przeniesienia kodu modelu, modelu


widoku i konwerterów z projektu AsystentZakupówUA, który w rozdziałach
19.–24. przygotowywaliśmy dla systemów Windows 8.1 i Windows Phone 8.1.
a) Na początek z projektu współdzielonego skopiujmy dwa pliki: Model.cs
i Model.Ustawienia.cs. Po dodaniu ich do nowego projektu zmieniłem
w nich tylko przestrzeń nazw, a konkretnie przyrostek ..UA zmieniłem na
..UWP. Można sprawdzić, czy po ich dodaniu projekt nadal się kompiluje,
ale nie przewiduję tu żadnych problemów.

Chciałbym ― ostatni już raz w tej książce ― podkreślić, że fakt, iż da się przenieść
z jednego typu projektu do innego cały model i model widoku „ot tak po prostu”, a tym
samym można przenieść cały kod odpowiedzialny za logikę aplikacji do innego typu
projektu, jest wielką zaletą konsekwentnego trzymania się wzorca MVVM.
310 Część III  Aplikacje uniwersalne (Universal Apps)

metody OnPropertyChanged jest nazwa własności. W tej chwili wskazujemy ją,


korzystając ze zwykłego łańcucha: OnPropertyChanged("Suma");.
Jakakolwiek próba refactoringu obejmującego nazwę tej własności
spowoduje, że powiadamianie o zmianach jej wartości przestanie działać,
a mimo to kod nadal będzie się kompilował i po uruchomieniu nie będzie
zgłaszał wyjątków. Możemy temu zapobiec, korzystając z nowego słowa
kluczowego nameof: OnPropertyChanged(nameof(Suma));.
e) Ostatnim plikiem, który skopiujemy do projektu UWP, jest Widok.
Konwertery.cs, zawierający konwertery wykorzystywane w wiązaniach
widoku z modelem widoku. Konsekwentnie zmieńmy nazwę przestrzeni
nazw i sprawdźmy, czy wszystko jest w porządku, kompilując cały projekt.
3. Następnie przejdźmy do przygotowania widoku. Także tu do ponownego użycia
nadają się spore fragmenty kodu XAML przygotowanego w klonowanym projekcie.
Chcę jednak wykorzystać okazję, aby przedstawić dwa nowe znaczniki XAML,
które w kontekście aplikacji uniwersalnych Windows 10 będą pojawiać się
bardzo często i warto je poznać. Chodzi o RelativePanel i VisualStateManager.
Listing 28.1 pokazuje kod XAML widoku z zaznaczonymi zmianami.

Listing 28.1. Widok


<Page
x:Class="AsystentZakupówUWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AsystentZakupówUWP"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mw="using:AsystentZakupówUWP.ModelWidoku"
mc:Ignorable="d">
<Page.DataContext>
<mw:ModelWidoku />
</Page.DataContext>
<Page.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush" />
</Page.Resources>
<RelativePanel Background="White">
<Border x:Name="pasekTytułu" Background="LightGray" Margin="0"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True">
<TextBlock x:Name="tbTytuł" Text="Asystent zakupów"
Margin="20" FontSize="50" Foreground="White" MinHeight="75" />
</Border>
<TextBlock x:Name="tbSuma" Margin="20,0,20,20" FontSize="65"
Foreground="Black"
RelativePanel.Below="pasekTytułu">
Suma:
<Run Foreground="Black" FontFamily="Courier New" FontSize="75"
Text="{Binding Path=Suma, Mode=OneWay}" />
</TextBlock>
<TextBox x:Name="tbKwota"
RelativePanel.Below="tbSuma"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True"
Rozdział 28.  Aplikacje uniwersalnew Windows 10 311

Margin="20,0,20,20" MinWidth="250" Height="80"


FontSize="70" FontFamily="Courier New"
Text="0"
TextWrapping="Wrap" TextAlignment="Right"
Foreground="{Binding ElementName=btnDodaj,
Path=IsEnabled, Mode=OneWay,
Converter={StaticResource boolToBrush}}"
Background="White" />
<Button x:Name="btnDodaj"
RelativePanel.Below="tbKwota"
RelativePanel.AlignLeftWith="tbKwota"
RelativePanel.AlignRightWith="tbKwota"
Margin="20,0,20,20" Height="80"
FontSize="50" HorizontalAlignment="Stretch"
Content="Dodaj"
Command="{Binding DodajKwotę}"
CommandParameter="{Binding ElementName=tbKwota, Path=Text}"
Foreground="Black" />
</RelativePanel>
</Page>

Najważniejszą różnicą względem analogicznego kodu z aplikacji uniwersalnych


(por. listingi z rozdziału 20.) jest użycie pojemnika RelativePanel i związanych
z nim własności dodanych do znaczników odpowiadających kontrolkom.
W pojemniku tym możemy określać relatywne położenie elementów interfejsu
użytkownika, wskazując, że kontrolka ma leżeć pod, nad, z lewej lub z prawej
strony innej kontrolki zidentyfikowanej poprzez nazwę. Dla przykładu spójrzmy
na znacznik przycisku. Zawiera on atrybut (własność doczepianą) RelativePanel.
Below="tbKwota", która wskazuje, że powinien znaleźć się pod polem
edycyjnym o nazwie tbKwota z uwzględnieniem marginesów zdefiniowanych
w obu tych kontrolkach (20 pikseli w przypadku pola edycyjnego i 0 w przypadku
przycisku). Dodatkowo możemy wyrównać położenie i rozmiar przycisku do
lewej i/lub prawej krawędzi pola edycyjnego bądź innej kontrolki. Służą do
tego atrybuty RelativePanel.AlignLeftWith="tbKwota" i RelativePanel.Align
RightWith="tbKwota". Możemy też „przyczepić” przycisk do brzegów
panelu-pojemnika za pomocą atrybutów RelativePanel.AlignLeftWith
Panel="True" i RelativePanel.AlignRightWithPanel="True". W tym
przypadku także uwzględniane będą zdefiniowane w znaczniku marginesy.
Analogiczne własności doczepiane RelativePane.LeftOf i RelativePane.
RightOf umożliwiają ustawianie kontrolek obok siebie. Wówczas możemy
ustawić je w poziomie, korzystając z RelativePanel.AlignTopWith i Relative
Panel.AlignBottomWith oraz RelativePanel.AlignTopWithPanel i Relative
Panel.AlignBottomWithPanel.
Stworzony w ten sposób interfejs użytkownika jest elastyczny ― zadziała na
wszystkich rozmiarach ekranu. W przypadku tak prostego interfejsu to jednak
nie jest wielka sztuka. Gdy składa się z większej liczby kontrolek lub gdy
zależy nam na umieszczeniu ich jak największej liczby na ekranie, warto
bardziej uzależnić jego wygląd od wielkości ekranu. Aby zaprezentować
w naszym prostym przykładzie służący do tego znacznik, załóżmy, że jeżeli
ekran jest wystarczająco duży, to znaczy szerszy niż 600 pikseli, chcemy,
312 Część III  Aplikacje uniwersalne (Universal Apps)

aby przycisk znajdował się z prawej strony pola edycyjnego, a nie pod nim.
Oznacza to, że powinniśmy wykryć tak szeroki ekran i wówczas zwiększyć
margines z prawej strony pola edycyjnego, a także przenieść przycisk. W tym
pomoże nam menedżer stanów wizualnych. Można w nim zdefiniować dobrze
nam znane ze stylów (por. rozdział 11.) znaczniki Setter, które ustawiają
własności kontrolek. Znaczniki te zgrupowane są w zestawy „stanów
wizualnych” spełniających warunki określone w menedżerze. Można
zdefiniować dowolną liczbę takich stanów, my ograniczymy się jednak tylko
do dwóch. Pierwszy będzie dotyczył szerokich ekranów (nazwiemy go
„Tablet”), drugi pozostałych urządzeń (nazwiemy go „Smartfon”). Wykrycie
tego drugiego nie będzie zmieniało ustawień kontrolek z wyjątkiem zmiany
tła paska tytułu na granatowy. Dla odróżnienia w pierwszym stanie, tym
o nazwie „Tablet”, tło paska będzie zielone. To ułatwi nam kontrolę działania
opisywanego mechanizmu. Zwiększamy także prawy margines pola edycyjnego,
ustawiamy przycisk pod etykietą wyświetlającą sumę i wyrównujemy go
do prawej strony pojemnika. Szerokość przycisku dopasowujemy do miejsca
zostawionego z prawej strony pola edycyjnego. Wszystkie te zmiany i cały
kod zarządcy stanów wizualnych przedstawia listing 28.2, natomiast efekt
działania menedżera widoczny jest na rysunku 28.3.
Listing 28.2. Menedżer stanów wizualnych
<Page
...>
<Page.DataContext>
<mw:ModelWidoku />
</Page.DataContext>
<Page.Resources>
<local:BoolToBrushConverter x:Key="boolToBrush" />
</Page.Resources>
<RelativePanel Background="White">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup >
<VisualState x:Name="Tablet">
<VisualState.Setters>
<Setter Target="pasekTytułu.Background" Value="Green" />
<Setter Target="tbKwota.Margin" Value="20,0,290,20" />
<Setter Target="btnDodaj.(RelativePanel.Below)"
Value="tbSuma" />
<Setter Target="btnDodaj.HorizontalAlignment" Value="Right" />
<Setter Target="btnDodaj.(RelativePanel.AlignRightWithPanel)"
Value="True" />
<Setter Target="btnDodaj.Width" Value="250" />
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="Smartphone">
<VisualState.Setters>
<Setter Target="pasekTytułu.Background" Value="Navy" />
<!-- poza tym zostawiamy oryginalny wygląd -->
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
Rozdział 28.  Aplikacje uniwersalnew Windows 10 313

</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...
</RelativePanel>
</Page>

Rysunek 28.3. Z lewej strony stan „Smartfon”, z prawej ― „Tablet”

Ostatnim krokiem będzie zmiana klasy App z pliku App.xaml.cs. Musi ona realizować
dwie dodatkowe funkcje: zapisywać stan aplikacji w razie wstrzymywania aplikacji
(cykl życia pozostaje taki sam jak w aplikacjach uniwersalnych dla Windows 8.1, por.
rysunek 21.1) oraz po zamknięciu aplikacji aktualizować kafelek. W obu przypadkach
powinniśmy użyć metody OnSuspending zdefiniowanej w klasie App.
1. Zanim przejdziemy do jej modyfikowania, skopiujmy z podprojektu
współdzielonego oryginalnego projektu metodę zmieńWyglądKafelka wraz
z towarzyszącym jej polem typu TileUpdater (listing 28.3, por. listing 22.1).

Listing 28.3. Metoda służąca do aktualizacji kafelka aplikacji w menu Start systemu Windows 10
private TileUpdater tu = TileUpdateManager.CreateTileUpdaterForApplication();

private void zmieńWyglądKafelka()


{
XmlDocument xml = TileUpdateManager.GetTemplateContent(
TileTemplateType.TileWide310x150Text01);
IXmlNode węzełTekst = xml.GetElementsByTagName("text").First();
węzełTekst.AppendChild(xml.CreateTextNode("Asystent zakupów:"));
węzełTekst = xml.GetElementsByTagName("text").Item(1);
węzełTekst.AppendChild(
xml.CreateTextNode(
"Suma: " + Model.SumowanieKwot.BieżącaInstanja.Suma.ToString()));
węzełTekst = xml.GetElementsByTagName("text").Item(2);
węzełTekst.AppendChild(
xml.CreateTextNode(
"Limit: " + Model.SumowanieKwot.BieżącaInstanja.Limit.ToString()));
tu.Update(new TileNotification(xml));
}
314 Część III  Aplikacje uniwersalne (Universal Apps)

2. Następnie do metody OnSuspending, której sygnaturę wzbogacamy o modyfikator


async, wstawiamy polecenie zapisujące stan modelu i wywołanie przed chwilą
zdefiniowanej metody zmieńWyglądKafelka. Wszystko to pod warunkiem,
że instancja modelu istnieje.

Listing 28.4. Czynności wykonywane przed wstrzymaniem aplikacji


private async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();

if (Model.SumowanieKwot.BieżącaInstanja != null)
{
await Model.Ustawienia.ZapiszStanModeluAsync(
Model.SumowanieKwot.BieżącaInstanja);
zmieńWyglądKafelka();
}

deferral.Complete();
}

Aby móc sprawdzić działanie aktualizacji kafelka, należy go oczywiście dodać do


menu Start (rysunek 28.4). Jak widać, obsługa kafelka, choć pokazywany jest on ina-
czej niż w Windows 8.1, nie różni się od obsługi kafelka opisanego w rozdziale 22.

Rysunek 28.4.
Kafelek aplikacji
w menu Start
Windows 10
Skorowidz
A zasoby, 130
app bar, Patrz: aplikacja pasek
animacja, 142, 143, 145, 146, 215 AppX, 259
ColorAnimation, 147 instalowanie, 263
w stylu, 144 testowanie, 261, 262
z użyciem ramek kluczowych, 148 tworzenie, 260
aplikacja atrybut
AppX, Patrz: AppX DataContext, 38
cykl życia, 247, 248 ExpectedException, 97
dynamika, 16 Fill, 20
interfejs, Patrz: interfejs Height, 15
język domyślny, 245 Icon, 288
lista, 255 Label, 288
logo, 244, 245 RelativePanel.AlignLeftWith, 311
mobilna, Patrz: aplikacja na urządzenia RelativePanel.Below, 311
przenośne StringFormat, 278, 279
na smartfon, 247 TargetType, 127
na tablet, 247 TextDecoration, 279
na urządzenia przenośne, 247, 248, 271 Title, 15
dostęp do pamięci, 271 Width, 15
pasek, 287, 292, 295, 301 x:Class, 15
plik, Patrz: plik x:Name, 15
stan, 248 xmlns, 15
odtwarzanie, 21, 23 attached property, Patrz: własność doczepiona
przywracanie, 251
resetowanie, 251
wstrzymywanie, 252
B
zapisywanie, 21, 23, 247, 249, 313 behavior, Patrz: zachowanie
tworzenie, 10, 11 biblioteka
uniwersalna, 231, 234, 239, 279 Microsoft.Expression.Interaction.dll, 73
Windows 10, 307 Newtonsoft.JSON, 168
uruchamianie w przeglądarce, 227, 230 System.Windows.Interactivity.dll, 73
ustawienia lokalne, 249, 250 BLL, 26
Windows Phone, 291 bubbling, Patrz: bulgotanie
wstrzymywanie, 247, 248, 281 buissness logic layer, Patrz: BLL
wznowienie, 247, 248 bulgotanie, 158
zamykanie, 183, 184, 247, 248, 313
316 MVVM i XAML w Visual Studio 2015

C I
checkbox, Patrz: pole opcji interfejs, 166
code-behind, 26, 40, 61, 69, 76, 153, 188, 189 ikona, 288
czas, 216, 217 interfejs, 11, 172
ICommand, 61, 240
IComparable, 190
D IComparer, 190
DAL, 26, 30 IDataErrorInfo, 50
dane IEnumerable, 166
szablon, Patrz: szablon danych ImultiValueConverter, 57
weryfikacja, 27 IMultiValueConverter, 56, 188
wiązanie, 37, 38, 39 INotifyCollectionChanged, 174
data, 186, 188, 189, 216, 217 INotifyDataErrorInfo, 50
data access layer, Patrz: DAL INotifyPropertyChanged, 41, 42, 44, 50, 169,
data binding, Patrz: dane wiązanie 171, 194, 216
data template, Patrz: szablon danych IValueConverter, 53, 237
DDD, 25, 29, 100 użytkownika graficzny, 26, 27
domain-driven design, Patrz: DDD Internet of Things, Patrz: urządzenie IoT

E K
ekran kafelek, 255
powitalny, 244, 245 aktualizowanie, 313
wielkość, 311 kolor tła, 255
element logo, 255
AppBarButton, 288 rozmiar, 255
DoubleAnimation, 143 szablon, 256
MenuFlyout, 288 wygląd, 255, 256
Page, 228 klasa
Setter, 127, 129, 312 App, 249, 251, 265, 313
emulator Application, 249
smartfona, 233 ApplicationCommands, 69
tabletu, 233, 243 ApplicationData, 249
uruchamianie, 243 Brush, 39
etykieta, 288 Brushes, 237
CommandBar, 288
CommandDialogBox, 293
F CommandManager, 235
formularz, 184, 186 ContentDialog, 301, 303
funkcja DependencyObject, 75
przejścia, 145 EditingCommands, 69
wygładzania, 146 EventTrigger, 69
FileIO, 271
FrameworkElement, 133, 194
G Freezable, 216
Geometry, 216
generic type, Patrz: typ parametryczny Graphics, 215
gradient, 115, 116 konwersja, 55
graphical user interface, Patrz: interfejs List, 190
użytkownika graficzny MainWindow, 41
GUI, Patrz: interfejs użytkownika graficzny MediaCommands, 69
MessageBox, 185
Skorowidz 317

MessageDialogBox, 298 ProgressBarBrushConverter, 60


modelu, 100 ProgressBarHighlightConverter, 60
NavigationCommands, 69 testowanie, 83, 95
NotificationDialogBox, 198, 293, 295 wbudowany, 60
ObservedObject, 50 ZoomPercentageConverter, 60
PrivateObject, 91, 92 kształt, 215
RelayCommand, 67, 68, 102, 104, 185, 235
Shape, 215
SolidColorBrush, 20
L
statyczna, 93 lista, 156, 177, 179
StorageFile, 271 sortowanie, Patrz: sortowanie
UIElement, 133, 194 ListBox, Patrz: lista
Windows.Storage.KnownFolders, 271
XDocument, 272
kod M
XAML, 26, 111, 123, 239 manifest, 259
zaplecza widoku, Patrz: code-behind menedżer stanów wizualnych, 312
kolekcja, 163 metoda
modyfikowanie, 184 CanExecute, 61, 65, 102, 103, 188, 240
w aplikacji mobilnej, 271 Convert, 53, 56, 181
w modelu widoku, 172 ConvertBack, 53, 56, 181
zachowań, 74 Execute, 61, 63, 102, 103
konsola Xbox, 307 GetProperty, 91
kontrolka, 111 OnCanExecuteChanged, 240
DatePicker, 186, 189 OnPropertyChanged, 103
definiowanie, 121 OnSuspending, 251, 281
dziedzicząca po Shape, Patrz: kształt ScrollToBottom, 189
Ellipse, 215 SetField, 91, 92
Grid, 38, Patrz też: siatka SetProperty, 91
Line, 215 ShowAsync, 303
ListBox, 177, 184, 289 Sort, 190
Path, 215 XDocument.Save, 271
projektowana przez użytkownika, 121, 234, 239 mock object, Patrz: obiekt atrapa
Rectangle, 215 model, 106
rozmiar, 14 pasywny, 193
Slider, 12 testowanie, 83
styl, Patrz: styl tworzenie, 99
szablon, Patrz: szablon kontrolki widoku, 27, 29, 44, 46, 53, 103, 106, 169, 188,
TextBlock, 176 193, 216
wiązanie, 57 instancja, 37
widoku, 101 kolekcja, Patrz: kolekcja w modelu widoku
WPF, 75 testowanie, 83
konwerter, 53, 54, 55, 57, 234, 276 tworzenie, 31, 33, 34, 35, 102
AlternationConverter, 60 wiązanie widoku, 103, 104
BooleanToVisibilityConverter, 60, 278 multibinding, 56, 57, 279, 303
BoolToBrushConverter, 237
BoolToVisibilityConverter, 178
BorderGapMaskConverter, 60 O
ColorToSolidColorBrushConverter, 96
DataGridLengthConverter, 60 obiekt
definiowanie, 179 atrapa, 92, 94
JournalEntryListConverter, 60 Windows.Storage.ApplicationData.
JournalEntryUnifiedViewConverter, 60 Current.LocalFolder, 271
MenuScrollingVisibilityConverter, 60
318 MVVM i XAML w Visual Studio 2015

Windows.Storage.ApplicationData. System, 100


Current.LocalSettings, 249 System.Windows, 186
Windows.Storage.ApplicationData. System.Windows.Data, 53, 237
Current.RoamingSettings, 249 System.Windows.Input, 186
wstrzykiwanie, 93 System.Windows.Media, 237
okno Windows.UI.Xaml.Data, 237
dialogowe, 193, 194, 196 x, 15
łańcuch, 209 przycisk, 111, 311
MessageBox, 199 aktywny, 118
w aplikacji Windows Phone, 291 definiowanie, 121
wyboru pliku, 205 tekst, 118
wyświetlenie warunkowe, 203 wygląd, 111, 119
zawartość, 210, 301 zagnieżdżanie, 155, 160
pasek tytułu, 223
przesuwanie, 223
przezroczystość, 222
R
operator reguła DRY, 123
., 40 routed event, Patrz: zdarzenie trasowane
?, 40
dostępu, 40
S
P siatka, 38, 123
smartfona emulator, Patrz: emulator smartfona
pasek aplikacji, Patrz: aplikacja pasek pędzel, 215
pędzel, 115, 215 sortowanie, 190
LinearGradientBrush, 115, 116, 125 splash screen, Patrz: ekran powitalny
RadialGradientBrush, 116 stos StackPanel, 134
SolidColorBrush, 237 styl, 127, 151
plik lokalizacja, 127, 130, 131
App.config, 22 Surface Hub, 307
App xaml.cs, 249, 251, 313 suwak, 16, 53
domyślny aplikacji, 11 szablon
JSON, 168 danych, 175
XML, 167, 168 kontrolki, 149, 150, 151
pojemnik RelativePanel, 311
pole opcji, 69, 156
polecenie, 61 Ś
CommandAfter, 202 ścieżka, 225
CommandBefore, 198, 202 środowisko
Create IntelliTests, 88 Blend, 148
Show, 198 projektowe Expression Blend, 16, 118
uruchamianie, 62, 66
projekt
aplikacji uniwersalnej, Patrz: aplikacja T
uniwersalna
domyślny, 265 tabletu emulator, Patrz: emulator tabletu
współdzielony, 234, 235, 237, 265, 276 test
projektowanie domenowe, Patrz: DDD dostęp do pól testowanej klasy, 90
przestrzeń nazw IntelliTest, 88
domyślna, 15 jednostkowy, 83, 95, 97
local, 13, 15 tworzenie, 85, 87, 88
mc, 16 uruchamianie, 88
Microsoft.VisualStudio.TestTools.UnitTesting, 90 Visual Studio 2013, 84, 86, 87
s, 186 konwertera, 95
Skorowidz 319

wielokrotny, 89 model, Patrz: model widoku


wyjątku, 96 tworzenie, 100
testowanie funkcjonalne, 9 warstwa, Patrz: warstwa widoku
tile, Patrz: kafelek Windows 10, 307
transformacja Windows 8.1, 233, 234, 242, 255
animowana, Patrz: animacja Windows Forms, 9
CompositeTransform, 140 Windows Phone, 265, 291
kompozycji, 134, 135, 137, 215 Windows Phone 8.1, 234, 265
MatrixTransform, 140 Windows Phone Runtime, 231
obrotu, 220 Windows Phone Store, 259
renderowania, 135, 137 Windows Presentation Foundation, Patrz: WPF
złożona, 140 Windows Runtime, 231, 239, 282
tunelowanie, 159 Windows Store, 259
tunneling, Patrz: tunelowanie WinRT, Patrz: Windows Runtime
typ wirtualizacja Hyper-V, 233
ApplicationDataContainer, 249 własność, 20
byte, 53 Angle, 143
Comparison, 190 Background, 115
DateTime, 216 Caption, 194
DependencyProperty, 79 Center, 111
double, 53, 89 ColumnDefinitions, 123
int, 89 Content, 111, 210
konwersja, 53, 55 Current, 249
MessageDialogBoxButton, 298 DataContext, 194
parametryczny, 93 DateTime.Now, 188, 216
SuspendingDeferral, 252 DialogBypassButton, 204
DialogResult, 212
doczepiona, 79, 311
U Fill, 39, 215
Universal Windows Platform, Patrz: UWP Foreground, 112, 115
urządzenie IoT, 307 Height, 111
user control, Patrz: kontrolka projektowana przez IsDialogBypassed, 203
użytkownika IsEnabled, 178
UWP, 307, 309 LayoutTransform, 133, 134
Opacity, 222
OriginalSource, 159
V Page.BottomAppBar, 288
Visual Studio, 255 RenderTransform, 133, 215
Visual Studio 2010, 84 RowDefinitions, 123
Visual Studio 2013, 173, 231, 233 SelectedIndex, 188, 289
Visual Studio 2015, 86, 87, 103, 173, 231, 233, Source, 159
307, 309 Stroke, 215
StrokeThickness, 215
Visibility, 178
W Width, 111
WindowContent, 210
warstwa, 193 WPF, 10
dostępu do danych, Patrz: DAL wyjątek
logiki biznesowej, Patrz: BLL ArgumentOutOfRangeException, 100
modelu, 25, 26, 29 NotImplementedException, 96
widoku, Patrz: widok NullReferrenceException, 40
dla Windows Phone 8.1, 265 testowanie, 96
wiązanie danych, Patrz: dane wiązanie wyzwalacz, 129
widok, 26, 106, 186, 234
320 MVVM i XAML w Visual Studio 2015

wzorzec Paint, 215


MVC, 25 PreviewKeyDown, 159
MVP, 25 PreviewMouseDown, 159
MVVM, 25, 83, 99, 103, 106, 188, 193, 231, przekształcanie w polecenie, 69
233, 234 RoutedEventArgs, 159
trasowane, 153
kontrola przepływu, 156
Z przerwanie sekwencji, 158
zachowanie, 73 Window.Closed, 61
definiowanie, 73 zegar, 216, 217, 227
kolekcja, 74 analogowy, 218
zdarzenie, 16 tarcza, 222
bulgotanie, Patrz: bulgotanie znacznik, Patrz: element
CanExecuteChanged, 61, 235, 283 znak
Click, 189 ., 40
CollectionChanged, 174 ?, 40
kontrolki, 9

You might also like