Professional Documents
Culture Documents
VerticalAlignment="Bottom"/>
<Slider x:Name="slider2" Margin="10,0,10,10" Height="22"
VerticalAlignment="Bottom"/>
</Grid>
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.
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
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= ... />
<Slider x:Name="sliderB"
Margin="10,0,10,10" Height="22" VerticalAlignment="Bottom"
Maximum="255"
ValueChanged="sliderR_ValueChanged"/>
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);
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).
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
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;
}
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.
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.
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.
Cała ta prosta klasa widoczna jest na listingu 3.1. Należy pamiętać, aby ustalić jej zakres
dostępności na public.
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.
namespace KoloryWPF.ModelWidoku
{
using Model;
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;
}
}
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.
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();
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.
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.
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.
namespace KoloryWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
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.
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.
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
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 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");
}
}
{
get
{
return kolor.ToColor();
}
}
...
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.
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
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");
}
}
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 EdycjaKoloru3()
{
Kolor.PropertyChanged +=
(object sender, PropertyChangedEventArgs e) =>
{
OnPropertyChanged("Color");
};
}
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>
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.
namespace KoloryWPF.ModelWidoku
{
public abstract class ObservedObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
2
Por. inny pomysł rozwiązania problemu w interfejsie IObservable<>.
Rozdział 4. Wiązanie danych (data binding) 51
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
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.
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.
namespace KoloryWPF.ModelWidoku
{
public class ResetujCommand : ICommand
{
public event EventHandler CanExecuteChanged;
{
return true;
}
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.5. Kod własności należy uzupełnić o przesyłanie referencji do instancji modelu widoku
private ICommand 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.
modelWidoku.R = 0;
modelWidoku.G = 0;
modelWidoku.B = 0;
}
}
}
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}" />
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.
#region Constructor
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
if (execute == null) throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructor
{
if (_canExecute != null) CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null) CommandManager.RequerySuggested -= value;
}
}
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;
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.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>
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(); });
}
}
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.
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.).
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.
namespace KoloryWPF
{
public class ZamknięcieOknaPoNaciśnięciuKlawisza : Behavior<Window>
{
public Key Klawisz { get; set; }
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).
</i:EventTrigger>
</i:Interaction.Triggers>
<i:Interaction.Behaviors>
<local:ZamknięcieOknaPoNaciśnieciuKlawisza Klawisz="Escape" />
</i:Interaction.Behaviors>
...
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
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>
{
get { return (Button)GetValue(PrzyciskProperty); }
set { SetValue(PrzyciskProperty, value); }
}
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.
2
Por. omówienie na stronie https://msdn.microsoft.com/en-us/library/ms749011(v=vs.110).aspx.
80 Część I Wzorzec MVVM
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.
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.
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
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
}
}
}
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);
[TestMethod]
public void TestKonstruktoraIWłasności_LosoweWartości()
{
byte[] losoweWartościSkładowychKoloru = new byte[3 * liczbaPowtórzeń];
rnd.NextBytes(losoweWartościSkładowychKoloru);
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; }
}
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
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.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;
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.
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;
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.
***
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.
namespace AsystentZakupówWPF.Model
{
public class SumowanieKwot
{
public decimal Limit { get; private set; }
public decimal Suma { get; private set; }
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
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.
namespace AsystentZakupówWPF.ModelWidoku
{
using Model;
using System.ComponentModel;
using System.Windows;
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
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;
}
W kodzie XAML stwórzmy instancję konwertera i użyjmy jej, aby do elementu TextBox
dodać wiązanie z przyciskiem (listing 9.6).
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ą
200100 (rysunek 10.1).
Rozdział 10. Budowanie złożonych kontrolek 113
Rysunek 10.2.
Konfigurowanie
pędzla za pomocą
okna własności
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.
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ę.
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>
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.
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.
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.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
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.
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" />
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
Rysunek 12.7.
Przycisk po
wykonaniu dwóch
transformacji
w różnej kolejności
Rozdział 12. Transformacje i animacje 141
<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
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.
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>
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>
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).
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ę.
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>
namespace ZdarzeniaTrasowane
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
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).
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.
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);
}
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.
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
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).
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.
namespace ZadaniaWPF.Model
{
public enum PriorytetZadania : byte { MniejWażne, Ważne, Krytyczne };
{
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; };
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.
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");
}
}
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>.
namespace ZadaniaWPF.Model
{
public class Zadania
{
private List<Zadanie> listaZadań = new List<Zadanie>();
{
return listaZadań.Remove(zadanie);
}
using System.Collections;
namespace ZadaniaWPF.Model
{
public class Zadania : IEnumerable<Zadanie>
{
private List<Zadanie> listaZadań = new List<Zadanie>();
{
return listaZadań.Remove(zadanie);
}
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.
{
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);
}
}
}
}
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
using System.ComponentModel;
using System.Windows.Input;
namespace ZadaniaWPF.ModelWidoku
{
public class Zadanie : INotifyPropertyChanged
{
private Model.Zadanie model;
}
}
Listing 15.7. Polecenia, które należy dodać do klasy Zadanie z modelu widoku
ICommand oznaczJakoZrealizowane;
o =>
{
model.CzyZrealizowane = true;
OnPropertyChanged("CzyZrealizowane",
"CzyZadaniePozostajeNiezrealizowanePoPlanowanymTerminie");
},
o =>
{
return !model.CzyZrealizowane;
});
return oznaczJakoZrealizowane;
}
}
KopiujZadania();
}
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>
Rysunek 15.1.
Widok zbudowany
z kontrolki ListBox
prezentującej
zadania
Rozdział 15. Kolekcje w MVVM i XAML 177
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.
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 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.
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;
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;
}
}
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
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
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
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
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.
using System.Collections;
namespace ZadaniaWPF.Model
{
public class Zadania : IEnumerable<Zadanie>
{
private List<Zadanie> listaZadań = new List<Zadanie>();
...
{
int wynik = -zadanie1.Priorytet.CompareTo(zadanie2.Priorytet);
if (wynik == 0) wynik = zadanie1.PlanowanyTerminRealizacji.
CompareTo(zadanie2.PlanowanyTerminRealizacji);
return wynik;
});
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;
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
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).
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
(c) Jacek Matulewski 2015
WWW:
http://www.fizyka.umk.pl/~jacek" />
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).
{
MessageBox.Show((string)o, Caption, MessageBoxButton.OK,
MessageBoxImage.Information);
};
}
}
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.
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.
{
if (!LastResult.HasValue) return false;
return LastResult.Value == MessageBoxResult.Yes;
}
}
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
};
}
set
{
SetValue(CommandOKProperty, value);
}
}
}
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.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
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);
};
};
}
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.
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());
}
}
}
}
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.
case null:
ExecuteCommand(CommandNull, CommandParameter);
break;
}
}
};
}
{
SetValue(CommandTrueProperty, 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.
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.
namespace ZegarWPF.ModelWidoku
{
public class Zegar : INotifyPropertyChanged
{
private DateTime poprzedniCzas = DateTime.Now;
}
}
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("AktualnyCzas"));
}
public Zegar()
{
Action<object, EventArgs> odświeżanieWidoku = (object sender,
EventArgs e) => { OnPropertyChanged(); };
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
namespace ZegarWPF.ModelWidoku
{
public enum Wskazówka { Godzinowa, Minutowa, Sekundowa };
</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
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.
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
</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}" />
Fill="Black"
Stroke="Black"/>
</Canvas>
</Page>
Rysunek 18.1.
Zegar uruchomiony
w przeglądarce
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)
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
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.
namespace AsystentZakupówUA
{
class BoolToBrushConverter : IValueConverter
{
private Brush brushBlack = new SolidColorBrush(Colors.Black);
private Brush brushRed = new SolidColorBrush(Colors.Red);
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)
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.
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.
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)
if (Model.SumowanieKwot.BieżącaInstanja != null)
await Model.Ustawienia.ZapiszStanModeluAsync
(Model.SumowanieKwot.BieżącaInstanja);
deferral.Complete();
}
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)
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));
}
if (Model.SumowanieKwot.BieżącaInstanja != null)
{
await Model.Ustawienia.ZapiszStanModeluAsync(
Model.SumowanieKwot.BieżącaInstanja);
zmieńWyglądKafelka();
}
deferral.Complete();
}
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.
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.
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.
namespace KoloryUA
{
public class ZamknięcieAplikacjiPoNaciśnieciuKlawisza : DependencyObject,
IBehavior
{
public VirtualKey Klawisz { get; set; }
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ć.
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;
}
}
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)
using System.Windows.Input;
namespace ZadaniaUA.ModelWidoku
{
public class Zadania
{
public const string NazwaPlikuXml = "zadania.xml";
//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();
}
...
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;
}
...
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);
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");
}
}
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
return dateTimeFormatter.Format(dateTime);
}
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>
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();
}
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.
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
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
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;
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.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" />
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).
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)
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 }
...
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 CommandOKOrCloseProperty =
DependencyProperty.Register("CommandOKOrClose", typeof(ICommand),
typeof(MessageDialogBox),
new PropertyMetadata(null));
...
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).
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.
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 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;
};
}
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.
<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>
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)
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)
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
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.
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)
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>
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();
if (Model.SumowanieKwot.BieżącaInstanja != null)
{
await Model.Ustawienia.ZapiszStanModeluAsync(
Model.SumowanieKwot.BieżącaInstanja);
zmieńWyglądKafelka();
}
deferral.Complete();
}
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