Kiedyś, dawno temu ktoś mnie poprosił o stworzenie pomocnego wpisu do MVVM Light-a. Kiedyś już miałem styczność z tym frameworkiem i jego odpowiednikami.
Obecnie jednak moja praca krąży tak intensywnie wokół ASP.NET, że nie byłbym w stanie stworzyć takiego wpisu.
Pomyślałem sobie jednak, że mimo, iż Silverlight jest martwą technologią i wszystko teraz będzie iść w mobile/Web, to dla swojej frajdy zrobię kurs MVVM. Silverlight i WPF budzą we mnie pewne pozytywne wspomnienia.
MVVM Light to prosty framework, który pomaga programiście użyć wzorca MVVM. Jak ten wzorzec działa? – po co on jest ? To już inna historia. Zacznijmy od podstawy podstaw.
Zanim padnie słowo MVVM trzeba dobrze omówić mechanizm DataBinding. A przed nim obiekty DependencyProperty i Dependency Object.
Dependency Objects
Po pierwsze, aby mechanizm databinding mógł działać na pewnym poziomie, pewne klasy muszą korzystać z pól System.Windows.DependencyProperty,a same klasy dziedziczyć po System.Windows.DependencyObject.
Każda kontrolka na pewnym poziomie dziedziczy po Dependency Object. Klasa ta jest sercem mechanizmu wiązania w języku XAML.
Dependency Object jego rola
Powodem istnienia klasy Dependency Object jest klasa Dependency Property.
DependencyProperty jest zarejestrowana jako pole statyczne, tylko do odczytu. Musi być ona także publiczna.
Tylko klasa Dependency Object lub klasa dziedzicząca po niej może korzystać z funkcji klasy “Dependency Property”.
Dependency Object zawiera w sobie szereg metod, które pozwalają na interakcję z umieszczonymi polami “Dependency Property”.
Metoda | Cel |
ClearValue | Czyść lokalną wartość umieszczą w Dependency Property |
CoerceValue | Uruchamia “CoerceValueCallback” . |
GetValue | Zwraca obecną wartość umieszczą w Dependency Property jako instancje obiektu. |
InvalidateProperty | Wysyła zapytanie o ponowną walidacje |
ReadLocalValue | Odczytuje lokalną wartość umieszczoną w Dependency Property. Jeśli takiej wartości nie ma zostaje zwrócony DependencyProperty.UnsetValue. Nie jest to null. |
SetValue | Nadpisuje obecną wartość. |
PropertyMetaData
Do rejestrowania obiekt Dependency Property używa metody statycznej
“DependencyProperty.RegisterAttached”.
Do rejestracji obiektu są potrzebne pewne informacje o tym, jak to pole ma działać z pewną właściwością. Informacje te są przechowywane w obiekcie “PropertyMetaData” i ma on następujące właściwości.
Właściwość | Cel |
CoerceValueCallback | Używany do inspekcji lub zmiany wartości Dependency Property, które są zależne od innych właściwości. Wartości są ustawiane w nieokreślonej kolejności. Działa podobnie jak “PropertyChangedCallback” różnica polega jednak na tym, że zdarzenie to zajdzie zawsze, nawet jeśli do wartości przypiszemy tę samą wartość, czyli jej nie zmienimy. |
DefaultValue | Wartość domyślna. Jeśli takiej wartości nie przewidujemy nie używamy słowa kluczowego null tylko statycznego pola DependencyProperty.UnsetValue. |
IsSealed | Zwraca on status możliwość zmiany wartości. Prawda albo fałsz |
PropertyChangedCallback | Zwraca on delegatę, która ma być wykonana gdy wartość Dependency Property ulegnie zmianie. Różnica pomiędzy CoerceValueCallback jest. |
Obiekt PropertyMetadata może być dołączany w trakcie rejestracji właściwości DP. O czym później. Obiekt ten można także dołączyć po rejestracji używając metody OverrideMetadata.
DispatcherObject
Jak zapewne zauważyłeś na rysunkach DependencyObject nie dziedziczy po System.Object. Istnieje jeszcze jeden poziom abstrakcji. Klasy DependencyObject dziedziczą po DispatcherObject.
DispatcherObject jest powiązany z instancją obiektu Dispatcher. Ten obiekt zarządza kolejką zdarzeń powiązanych z jednym wątkiem. Tylko wątek, który utworzył obiekt DispatcherObject ma do niego dostęp.
Wymusza to działanie jednowątkowe na obiektach “DispatcherObject” (czyli w sumie na wszystkich kontrolkach WPF, Silverlight)
W WPF i Silverlight wszystkie interakcje z użytkownikiem odbywają się jednowątkowo. Drugi wątek najczęściej renderuje kontrolki.
Wątek UI ma więc władzę nad wszystkimi kontrolkami, gdyż wszystkie one dziedziczą po DispatcherObject. Nie można do nich uzyskać dostępu z poziomu innego wątku.
Jeśli więc chcemy użyć wielowątkowej aplikacji, która działa z wyglądem użytkownika musimy użyć obiektu “DispatcherObject” jako mediatora. Wiedza ta będzie potrzebna w dalszych wpisach.
Przykład Dependency Properties
Tyle na dzisiaj jeśli chodzi o lekturę. Przejdźmy do przykładów. Co mnie obchodzi jak klasy wyglądają i działają w WPF od tego mam MSDN. Chcę zobaczyć przykład Cezarze.
Do potrzeby Dema utworzyłem pusty projekt WPF.
Utworzyłem w nim klasę “PresentationOfDependencyObject”.
public class PresentationOfDependencyObject : DependencyObject
{
static PresentationOfDependencyObject()
{
NameAndSurNameDependencyProperty = DependencyProperty.Register
("NameAndSurName",
typeof(string), typeof(PresentationOfDependencyObject));
}
public static readonly DependencyProperty NameAndSurNameDependencyProperty;
public string NameAndSurName
{
get { return (string)GetValue(NameAndSurNameDependencyProperty); }
set { SetValue(NameAndSurNameDependencyProperty, value); }
}
}
Istnieje znaczna różnica pomiędzy właściwością, po której można wykonać databinding, a zwykłą właściwością. Opis kodu.
- Pole DependencyProperty musi być zadeklarowane jako public,static i tylko do odczyty “readonly”.
- Pole DependencyPropert jest rejestrowane przy użyciu statycznej metody “DependencyProperty.Register”.
- Statyczny konstruktor w tym wypadku jest bardzo użytecznym i w nim mogę dokonać rejestracji. Można użyć przyrównania do pola DependencyProperty, ale kod przez to będzie mniej czytelny.
- Metoda DependencyProperty.Register przyjmuje nazwę normalnej właściwości, typ tejże właściwości oraz typ klasy przytrzymującej tą właściwość.
- W klasie istnieje normalna właściwość, która upraszcza mechanizmu pola Dependency Property. Zwraca ona wartość pola lub go uzupełnia przy użyciu “get;set;”. Przypomina to działania jawnych automatycznych właściwości.
private string hello; public string Hello { get { return hello; } set { hello = value; } }
Te kroki są pospolite do każdej deklaracji właściwości DependencyProperty. Warto więc je zapamiętać.
Problemem przy tworzeniu tego wpisu jest użycie słów pola, właściwości w kontekście angielskiego słowa DependencyProperty.
Słowo angielskie DependencyProperty zawiera w sobie słowo właściwości.
Sformułowanie angielskie “DependencyProperty” dotyczy jednak instancji normalnej właściwości, która jest rozszerzona przez statyczne pole typu DependencyProperty.
Integracja z XAML
To nie koniec przykładu. Użyjmy wcześniej napisanej klasy w pliku XAML.
<Window x:Class="DependencyPropertyBlog.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Cezary="clr-namespace:DependencyPropertyBlog"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Cezary:PresentationOfDependencyObject x:Key="data" />
</Window.Resources>
<Grid>
</Grid>
</Window>
Niestety, ponieważ moja klasa nie dziedziczny po UIElement, nie mogę jej dodać jako interfejsu użytkownika. Mogę natomiast dodać ją jako element zasobu okna aplikacji. Ważne, by do elementu root dopisać odpowiednią przestrzeń nazw “xmlns:Cezary="clr-namespace:DependencyPropertyBlog". Więcej o tym w tym wpisie
Obecnie w mojej klasie PresentationOfDependencyObject istnieją dwie właściwości.
public class PresentationOfDependencyObject : DependencyObject
{
static PresentationOfDependencyObject()
{
NameAndSurNameDependencyProperty = DependencyProperty.Register
("NameAndSurName",
typeof(string), typeof(PresentationOfDependencyObject));
}
public static readonly DependencyProperty NameAndSurNameDependencyProperty;
public string NameAndSurName
{
get { return (string)GetValue(NameAndSurNameDependencyProperty); }
set { SetValue(NameAndSurNameDependencyProperty, value); }
}
private string hello;
public string Hello
{
get { return hello; }
set { hello = value; }
}
}
Obie właściwości są widoczne przez intellisense XAML. Jeśli masz jakiś problem bądź intellisense podkreśla ci na czerwono zapis XAML, przetwórz jeszcze raz aplikację. [CTR+SHIFT+B]
Jednak tylko właściwość “NameAndSurName” pozwoli na użycie DataBinding”. Łatwo to zauważyć patrząc na okno “Properties” w Visual Studio.
Wiązanie z normalną właściwością
Oto wiec serce mechanizmu DataBinding.
Używając kreatora możemy powiązać właściwość NameAndSurName z właściwością tej samej klasy “Hello”. Tak, jak jest to pokazane na rysunku.
W XAML mamy następujący zapis wiązania.
<Cezary:PresentationOfDependencyObject x:Key="data" Hello="Cezary Walenciuk"
NameAndSurName="{Binding Hello, RelativeSource={RelativeSource Self}}" />
Samo Visual Studio w oknie “Properties” pokazuje, że wiązanie faktycznie działa.
Jeśli więc wiązania działa gdy zmienimy wartość właściwości “Hello” powinnyśmy także zmienić właściwość “NameAndSurName”.
private void Button_Click(object sender, RoutedEventArgs e)
{
object resource = this.Resources["data"];
PresentationOfDependencyObject p = resource as PresentationOfDependencyObject;
if (p != null)
{
System.Diagnostics.Debug.WriteLine(p.NameAndSurName);
System.Diagnostics.Debug.WriteLine(p.Hello);
p.Hello = "You";
MessageBox.Show(p.NameAndSurName);
}
}
Do XAML dodałem szybko przycisk i dodałem do niego zdarzenie. MessageBox powinien wyświetlić nową wartość właściwości “NameAndSurName”. Jest to wartość “You”.
Jak widać mechanizm wiązania nie działa, tak jakbyśmy tego chcieli. Wiązanie DependencyProperty właściwości z normalną właściwością skutkuje takim zachowaniem. Wartość jest przypisana tylko raz. Nie ma mechanizmu śledzenia zmiany normalnej właściwości.
Normalna właściwość nie ma zdarzenia informującego o takim zajściu.
Wiązanie z właściwością DependencyProperty
Stwórzmy więc drugą właściwość “DependencyProperty”. Nazwałem ją FavoriteTech.
public class PresentationOfDependencyObject : DependencyObject
{
static PresentationOfDependencyObject()
{
NameAndSurNameDependencyProperty = DependencyProperty.Register
("NameAndSurName",
typeof(string), typeof(PresentationOfDependencyObject));
FavoriteTechDependencyProperty = DependencyProperty.Register
("FavoriteTech",
typeof(string), typeof(PresentationOfDependencyObject));
}
public static readonly DependencyProperty NameAndSurNameDependencyProperty;
public string NameAndSurName
{
get { return (string)GetValue(NameAndSurNameDependencyProperty); }
set { SetValue(NameAndSurNameDependencyProperty, value); }
}
private string hello;
public string Hello
{
get { return hello; }
set { hello = value; }
}
public static readonly DependencyProperty FavoriteTechDependencyProperty;
public string FavoriteTech
{
get { return (string)GetValue(FavoriteTechDependencyProperty); }
set { SetValue(FavoriteTechDependencyProperty, value); }
}
}
Po ponownie kompilacji programu powinieneś w oknie properties zobaczyć nową dodaną właściwość.
Teraz używając kreatora powiążę właściwość DependencyProperty NameAndSurName z drugą właściwością DependencyProperty FavortiteTech. Używając kreatora jest to banalnie proste.
Mam następujący zapis XAML.
<Window.Resources>
<Cezary:PresentationOfDependencyObject x:Key="data"
NameAndSurName="{Binding FavoriteTech, RelativeSource={RelativeSource Self}}"
FavoriteTech="WPF" />
</Window.Resources>
Modyfikujemy kod kliknięcia przycisku. Zmieniamy teraz właściwość “FavoriteTech” na “You”.
private void Button_Click(object sender, RoutedEventArgs e)
{
object resource = this.Resources["data"];
PresentationOfDependencyObject p = resource as PresentationOfDependencyObject;
if (p != null)
{
System.Diagnostics.Debug.WriteLine(p.NameAndSurName);
System.Diagnostics.Debug.WriteLine(p.FavoriteTech);
p.FavoriteTech = "You";
MessageBox.Show(p.NameAndSurName);
}
}
Wartość właściwości NameAndSurName także się zmieniła.
Wykorzystanie PropertyMetaData
Stworzyliśmy już dwie właściwości rozszerzone, ale żadna z nich nie potrzebowała obiektu PropertyMetaData przy rejestracji.
Metoda “DependencyProperty.Register” ma wiele przeciążeń i dwa z nich używają obiektu PropertyMetaData.
Obiekt ten jest najczęściej stosowany, gdy chcemy dodać do właściwości DP wartość domyślną. Do niego możemy dodać delegaty do metod, które są uruchamiane, gdy zmieniamy właściwości DP. Przykładowo możemy w nich zmienić przypisaną wartość do właściwości. Metody te muszą być statyczne
static PresentationOfDependencyObject()
{
NameAndSurNameDependencyProperty = DependencyProperty.Register
("NameAndSurName",
typeof(string), typeof(PresentationOfDependencyObject));
FavoriteTechDependencyProperty = DependencyProperty.Register("FavoriteTech",
typeof(string), typeof(PresentationOfDependencyObject));
PropertyMetadata p = new PropertyMetadata(1,AgeWasChanged,AgeWasCoerce);
AgeDependencyProperty = DependencyProperty.Register("Age",
typeof(int), typeof(PresentationOfDependencyObject),p);
}
public static String Message;
public static readonly DependencyProperty AgeDependencyProperty;
public int Age
{
get { return (int)GetValue(AgeDependencyProperty); }
set { SetValue(AgeDependencyProperty, value); }
}
public static void AgeWasChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PresentationOfDependencyObject a = d as PresentationOfDependencyObject;
Message += "AgeWasChanged";
Message += "Value = " + a.Age.ToString();
Message += "\n———————–\n";
if (a.Age > 120)
a.Age = 120;
}
public static object AgeWasCoerce(DependencyObject d, object baseValue)
{
int value = (int)baseValue;
Message += "AgeWasCoerce";
Message += "AgeWasChanged";
Message += "Value = " + value.ToString();
Message += "\n———————–\n";
if (value < 0)
return value * -1;
else
return value;
}
Nowa właściwość DP “Age” ma przypisane do siebie dwie metody, które się uruchomią, gdy ulegnie ona zmianie. Jej wartość domyślna to 1.
Dodaj do aplikacji kolejny przycisk i dodaj następujący kod
private void Button_Click(object sender, RoutedEventArgs e)
{
object resource = this.Resources["data"];
PresentationOfDependencyObject p = resource as PresentationOfDependencyObject;
if (p != null)
{
string a = p.Age.ToString();
p.Age = 1;
p.Age = -100;
p.Age = 130;
MessageBox.Show(a + "\n" +PresentationOfDependencyObject.Message);
PresentationOfDependencyObject.Message = "";
}
}
Na początki Age miał wartość jeden.
Zmieniliśmy wartość właściwość AGE na 1 i uruchomiła się tyko metoda AgeWasCoerce.
Zmieniliśmy później wartość Age na –100. Metoda AgeWasCoerce zmieniła tę wartość na dodatnią.
Wartość już zmieniona trafiła do metody AgeWasChanged.
Zmieniłem później właściwość Age na 130. Metoda AgeWasCoerce nie zmieniła wartości. Metoda AgeWasChanged wyświetliła tę wartość i ją zmieniła ustawiając ją ponownie. Wiesz co to oznacza.
Metody AgeWasCoerce i AgeWasChanged zostały uruchomione ponownie.
Podsumowując, jeśli chcesz manipulować wartościami we właściwości DP, lepiej zrobić to używając delegaty “CoerceValueCallBack”.
W następnym wpisie oderwiemy się od Dependency Property i omówię szerzej mechanizm samego wiązania. Mając taką podstawową wiedzę szybko nauczysz się wzorca MVVM.