poniedziałek, 19 lipca 2010

Dlaczego warto używać Dependency Properties oraz Routed Events podczas tworzenia aplikacji w WPF

WPF rozwinął idę właściwości i zdarzeń. Standardowe właściwości i zdarzenia zostały przepisane całkowicie od początku dzięki czemu wprowadzono nową funkcjonalność jak i zachowano bardzo dużą wydajność rozwiązań. Przyjrzyjmy się tym dwóm mechanizmom bliżej.


Dependency Propeteries

Zwykłe właściwości są znane raczej wszystkim programistą C#:
        private int _age;

        public int Age
        {
            get { return _age; }
            set { _age = value; }
        }
Umożliwiają proste przypisanie wartości prywatnym polom klasy.
Aby stworzyć klasę, która zawiera DP (Dependency Property) musimy dziedziczyć po klasie (lub bezpośrednio) dziedziczącej z DependencyObject (większość klas WPF dziedziczy z tej klasy więc nie będzie z tym problemu).
Informacje zapisane w DP muszą być dostępne dla innych klas, z tego też powodu definiujemy właściwość jako statyczną i dodatkowo tylko do odczytu:
public static readonly DependencyProperty FirstNameProperty;
Przyjęła się konwencja, aby DP nazywać według szablonu Nazwa_właściwośćiProperty. Pozwala to na proste oddzielenie zwykłych właściwości od DP.
Jak widzimy DP oznaczone jest jako readonly (tylko do odczytu) i static, tak więc wartość może mu przypisać tylko statyczny konstruktor.
Kolejnym etapem tworzenia DP jest rejestracja. Etap ten wykonujemy w statycznym konstruktorze klasy wykorzystując do tego metodę Register(). Rejestracja przebiega dwuetapowo z tym że pierwszy etap można pominąć:
1. Tworzymy metadane czyli obiekt typu FrameworkPropertyMetadata w którym możemy ustawić takie rzeczy jak np. wartość domyślna, sposób bindowania.
2. Rejestracja DP za pomocą metody Register podając jako parametry:
- nazwę właściwośći
- typ danych właściwości
- typ będący właścicielem właściwości
- opcjonalnie: metadane
- opcjonalnie: wywołanie zwrotne (Callback) walidacji
        static MyClass()
        {
            //1 etap
            FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata("None", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);
            //2 etap
            FirstNameProperty = DependencyProperty.Register("FirstName", typeof(string), typeof(MyClass), metadata, new ValidateValueCallback(MyClass.IsFirstNameValid));
        }

Aby można było łatwo korzystać z DP definiujemy wrapper w postaci zwykłej właściwości w którym wykorzystamy metody SetValue(DP, value) oraz GetValue(DP) :
        public string FirstName
        {
            set
            {
                SetValue(FirstNameProperty, value);
            }
            get
            {
                return (string)GetValue(FirstNameProperty);
            }
        }

W jaki sposób WPF wykorzystuje DP?
Większość właściwości, którym przypisujemy wartości za pomocą XAMLa czy też z kodu, jest właśnie typu DP. Podczas pobierania wartości z DP dochodzi do swoistej "reakcji łańcuchowej" w celu uzyskania jej. Hierarchię można przedstawić następująco:
1. Wartość domyślna
2. Odziedziczona wartość
3. Wartość zapisana w stylu
4. Lokalna wartość
Warto się zastanowić dlaczego wymyślono ten mechanizm. Otóż uzyskano dzięki temu zmniejszenie zasobów potrzebnych na tworzenie właściwości tam, gdzie nie są one potrzebne.

Współdzielenie DP
DP mają tą zaletę, że można je udostępnić w innych klasach bez potrzeby dziedziczenia po klasie w której są zdefiniowane. Wykorzystujemy w tym celu metodę AddOwner() przyjmującą jako parametr właściciela DP:
    public class SecondClass : DependencyObject
    {
        public static readonly DependencyProperty FirstNameProperty = MyClass.FirstNameProperty.AddOwner(typeof(SecondClass));
        public string FirstName
        {
            get
            {
                return (string)GetValue(FirstNameProperty);
            }
            set
            {
                SetValue(FirstNameProperty, value);
            }
        }
        static SecondClass()
        {
        }
    }

Attached DP
Jest to specjalny rodzaj DP, który nie odnosi się do klasy w której został zdefiniowany. Najlepszym przykładem jest Grid który posiada ADP (Attached DP) Row i Column. Kładąc kontrokę na Gridzie możemy przypisać numer wiersza i kolumny w której się zndzie:
<Button Grid.Row="2" Grid.Column="2" Width="100" Height="25" Content="Button" />
Deklaracja tego rodzaju DP odbywa się jak poprzednio z wyjątkiem dwóch ostatnich kroków. Zamiast metody Register(), wykorzystujemy metodę RegisterAttached() i nie tworzymy wrapera (ponieważ właściwości i tak nie wykorzystamy w bieżącej klasie). Zamiast wrapera tworzymy SetPropertyName oraz GetPropertyName. Przykład implementacji i ustawienia wartości ADP w XAMLu:
    class SimplePanel : StackPanel
    {
        private static readonly DependencyProperty _text;

        static SimplePanel()
        {
            _text = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(SimplePanel));
        }

        public static string GetText(UIElement element)
        {
            if (element != null)
            {
                return (string)element.GetValue(SimplePanel._text);
            }
            else
            {
                return null;
            }
        }

        public static void SetText(UIElement element, string value)
        {
            if (element != null)
            {
                element.SetValue(SimplePanel._text, value);
            }
        }
    }
<Window x:Class="WpfApplication3.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication3"
       Title="MainWindow" Height="350" Width="525">
    <Grid>
        <c:SimplePanel>
            <Button Width="100" Height="25" c:SimplePanel.Text="Ala ma kota"></Button>
        </c:SimplePanel>
    </Grid>
</Window>


Walidacja danych w DP
Tradycyjne właściwości walidowaliśmy w momencie ustawiania nowej wartości (czyli w setterze). W DP stworzono inny mechanizm. Walidacji możemy dokonać na dwa sposoby:
1. ValidateValueCallback - może zaakceptować lub odrzucić nową wartość
2. CoerceValueCallback - posiada możliwość zmiany wartości na dopuszczalną
Samo ustawienie wartości w DP z wykorzystaniem tych mechanizmów przebiega następująco:
1. Wywoływany jest CoerceValueCallback
2. Następnie wywoływany jest ValidateValueCallback. Zwraca true w przypadku gdy wartoś jest akceptowana lub false w przeciwnym.
3. Wywołanie PropertyChangedCallback dzięki któremu możemy powiadomić inne klasy o zmianie wartości właściwości

Zacznijmy od mechanizmu ValidateValueCallback
Mechanizm ten ma chyba tylko jedną wadę: brak dostępu do pól niestatycznych. Samam metoda jest zadeklarowana jako statyczna więc automatycznie traci tę możliwość. Przykładowa implementacja:
        private static bool IsFirstNameValid(object value)
        {
            string s = (string)value;
            foreach (var item in s)
            {
                if (!char.IsLetter(item))
                {
                    return false;
                }
            }
            return true;
        }

CoerceValueCallback
        public static object CoerceAllowLetters(DependencyObject o, object value)
        {
            if (((string)value).Contains("Imie"))
            {
                return "";
            }
            return value;
        }




Routed Events

Jedno z potężniejszych narzędzi programisty WPF. Pozwalają na wywołanie nie tylko na obiekcie którego dotyczą, ale na całym drzewie (zarówno logicznym jak i wizualnym). Przykładem może być Button, którego zawartość to obrazek i tekst. Naciskając na obrazek uruchamiamy event klik dla niego, ale chcemy też aby został w naturalny sposób uruchomiony event dla całego przycisku.

Tworzenie Routed Events:
Deklaracja jest bardzo podobna do DP. Tak samo mamy do czynienia z deklaracją static readonly, oraz rejestracją w statycznym konstruktorze:
        public static readonly RoutedEvent MyEvent;

        static MyClass()
        {
            MyEvent = EventManager.RegisterRoutedEvent("My", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyClass));
        }
Następnie tworzymy tradycyjny wrapper na zdarzenie:
        public event RoutedEventHandler My
        {
            add
            {
                AddHandler(MyEvent, value);
            }
            remove
            {
                RemoveHandler(MyEvent, value);
            }
        }

Sharing Routed Events
Tak jak w przypadku DP tak i tu mamy możliwość współdzielenia właściwości z innymi klasami:
public static readonly RoutedEvent MyEent = MyClass.MyEvent.AddOwner(typeof(SecondClass));

Event Routing:
Dla wyobrażenia: Label a w nim StackPanel zawierający obrazek i tekst. Klikamy w obrazek.
Podczas rejestrowania zdarzenia, jako jeden z parametrów podawaliśmy RoutingStrategy. Ten wyliczeniowy typ danych zawiera trzy możliwe strategie:
1. Direct - standardowe wywołanie zdarzenia na obiekcie w którym nastąpiło ono
2. Bubbling - wywołanie zdarzenia od źródła w kierunku rodziców
3. Tunneling - zdarzenie wywołane jest najpierw na elemencie głównym, a następnie przesuwany jest w kierunku źródła
W przykładzie powyżej, przeszukiwanie będzie miało następującą kolejność:
1. Image.MouseDown
2. StackPanel.MouseDown
3. Label.MouseDown
Jeżeli nie byłoby w tym momencie zdefiniowanej obsługi zdarzenia, wywołanie przemierzałoby kolejnyh rodziców:
4. Grid
5. Window
Zdarzenie po przechwyceniu nie przechodzi już do następnych rodziców. Należy więc łapać je tam gdzie to na prawdę jest potrzebne.

RoutedEventArgs
Klasa ta zawiera szereg właściwości które pozwalają na m.in. :
Source - zidentyfikować obiekt wywołujący zdarzenie
OrginalSource - identyfikuje obiekt który jako pierwszy wywołał zdarzenie
Handled - pozwala na zatrzymanie Bubblingu lub Tunelingu

Attached Events
W przypadku, gdy którać z kontrolek nie wspiera jakiegoś zdarzenia a jest nam ono bardzo potrzebne, możemy w prosty sposób umożliwić taką obsługę. Dla przykładu Grid nie obsługuje eventu Click. W łatwy sposób można to zmienić:
    <Grid ButtonBase.Click="Grid_Click" Name="dfdf">
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="12,23,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="12,52,0,0" Name="button2" VerticalAlignment="Top" Width="75" />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="12,90,0,0" Name="button3" VerticalAlignment="Top" Width="75" />
    </Grid>
        private void Grid_Click(object sender, RoutedEventArgs e)
        {
            button1.Content = "Ala";
            button2.Content = "Ala";
            button3.Content = "Ala";
        }

Na koniec warto odpowiedzieć na pytanie postawione w temacie:
Oba mechanizmy pozwalają m.in. na:
- powiadomienia o zmianie wartości
- wywołania zwrotne
- walidację
- dziedziczenie wartości
- współdzielenie z innymi klasami
- wywoływanie "łańcuchowe"
- udział w animacjach
- udział w stylach i szablonach
- bindowanie do danych
- nadpisywanie wartości domyślnych

Tyle na temat dwóch mechanizm, które warto poznać i rozszerzyć o dodatkowe informacje zawarte na MSDN.

2 komentarze: