2013. május 10., péntek

WPF #11 Dependency Property

A Wpf egyik (ha nem a legnagyobb) újítása a .Net rendszerben egy újfajta tulajdonság - a DependencyProperty - bevezetése volt.

Mindenféle ismeret nélkül - akaratlanul - számtalan alkalom adódhatott, ahol ezen tulajdonság felhasználásra került. Lehetett az stílus definíció vagy animáció.

Az alábbi gomb definíció két darab DependencyProperty használatával állítja be a gomb szélességét és magasságát.
<Button Width="100" Height="20"/>

Ezt a kódot bárhova beillesztve egy meghatározott méretű gomb kerül kirajzolásra. A gomb megjelenítéséhez azonban szükség van egy sor egyéb tulajdonságra (pl háttérszín), amelyek nem kerültek meghatározásra, a rendszer mégis adott neki egy meghatározott alapértéket...

Az eddig adatkötés kivétel nélkül szimpla .Net tulajdonságok kerültek felhasználásra, így jogosan merül fel a kérdés, mivel tud többet, illetve miben különbözik tőle a DependencyProperty.

Minden .Net tulajdonság egy adott osztály privát tagját éri el, ezzel szemben a DependencyProperty dinamikusan kerül meghatározásra, amikor a konkrét értéke lekérdezésre kerül. (Ezt talán a legegyszerűbb egy Key/Value gyűjteményhez hasonlítani, ahol a kulcs alapján lehet az értéket lekérdezni).

Minden DependencyProperty csak egyszer kerül eltárolásra, és a keretrendszer mindössze a változásokat tárolja külön. Ezzel óriási memória foglalási problémát old fel. Egyetlen Button is számos tulajdonsággal rendelkezik amelyeket ha mind tárolni kellene akkor hamar betelne a memória.

Minden XAML-ben elhelyezett elem tulajdonsága stílusok segítségével is meghatározható. ( ajánlott)
Stíluson belül csak DependencyProperty-re lehet hivatkozni.

Szintén fontos megjegyezni, hogy csak DependencyProperty-t lehet animációban célként megadni.

Az egymásba ágyazott elemek - általában - öröklik a szülő objektum beállított DependencyProperty értékét. Pl egy Grid megadott fontSize értékét felveszi a benne elhelyezett TextBlock.

Minden DependencyProperty automatikusan rendelkezik számos callBack függvénnyel, amelyek az értékének megváltozásakor azonnal értesítést küldenek. Ezen tuljadonsága tökéletesen kihasználható adatkötésre.

Zárójelben jegyzem meg, hogy egy egyszerű adatkötéshez inkább érdemes .Net tulajdonságot használni, mert egyetlen DependecyProperty létrehozása, felparaméterezése jóval több munkát igényel, mint egy szimpla tulajdonság és az INotifyPropertyChanged interface használata.
 
Mivel egy DependencyProperty értékének beállítására számos lehetőség van, ezt a .Net keretrendszer az alábbi prioritási listát használja a végső érték meghatározására.

1. Animációban meghatározott érték
2. Adatkötés
3. Locálisan megadott érték
4. Áltatunk írt stílusban meghatározott trigger hatására
5. Általunk írt Templateben meghatározott trigger hatására
6. Általunk írt stílusban meghatározott érték
7. A Wpf által alapból beállított stílusban meghatározott trigger hatására
8. A Wpf által alapból beállított stílusban meghatározott érték
9. Örökölt érték
10. A DependencyProperty alapértéke.

A 10-es pontot látva egyértleművé válik, hogy az elöbb kirajzolásra került gomb a Width és a Height tulajdonságán kívűl honnan veszi az összes többi tulajdonságának az alapértékét.
 
Az eddigiektől eltérően az alábbiakban egy kész program minden forráskódja lesz a kiindulópont.
A program egy újfajta Grid elmet hoz létre (GridWithHueProperty).
Ezen Grid hátterét a Hue érték megadásával lehet megváltoztatni.

A hue a HSB (hue-saturation-brightness) színmód színkód értéke. Az alábbi képen a jobb oldalon látható színátmenet a színpaletta összes maximális telítettségű és fényerejű színét tartalmazza. Ez a hue. A hue megváltoztatásával könnyen és gyorsan lehet pirosból kéket vagy akármilyen más színt készíteni.


A Grid elem háttérszínének megadásához RGB színösszetevőkre van szükség. Szerencsére a HSB könnyen RGB-re alakítható.

Class: ColorMode
using System;
using System.Windows.Media;

namespace Wpf11_DependencyProperty
{
    public class ColorMode
    {
        public static Color HsbToRgb(double hue, double saturation, double brightness)
        {
            byte r = 0, g = 0, b = 0;
            if (Math.Abs(saturation - 0) < Double.Epsilon)
            {
                r = g = b = (byte)(brightness * 255.0f + 0.5f);
            }
            else
            {
                var h = (hue - Math.Floor(hue)) * 6.0f;
                var f = h - Math.Floor(h);
                var p = brightness * (1.0f - saturation);
                var q = brightness * (1.0f - saturation * f);
                var t = brightness * (1.0f - (saturation * (1.0f - f)));

                switch ((byte)h)
                {
                    case 0:
                        r = (byte)(brightness * 255.0f + 0.5f);
                        g = (byte)(t * 255.0f + 0.5f);
                        b = (byte)(p * 255.0f + 0.5f);
                        break;
                    case 1:
                        r = (byte)(q * 255.0f + 0.5f);
                        g = (byte)(brightness * 255.0f + 0.5f);
                        b = (byte)(p * 255.0f + 0.5f);
                        break;
                    case 2:
                        r = (byte)(p * 255.0f + 0.5f);
                        g = (byte)(brightness * 255.0f + 0.5f);
                        b = (byte)(t * 255.0f + 0.5f);
                        break;
                    case 3:
                        r = (byte)(p * 255.0f + 0.5f);
                        g = (byte)(q * 255.0f + 0.5f);
                        b = (byte)(brightness * 255.0f + 0.5f);
                        break;
                    case 4:
                        r = (byte)(t * 255.0f + 0.5f);
                        g = (byte)(p * 255.0f + 0.5f);
                        b = (byte)(brightness * 255.0f + 0.5f);
                        break;
                    case 5:
                        r = (byte)(brightness * 255.0f + 0.5f);
                        g = (byte)(p * 255.0f + 0.5f);
                        b = (byte)(q * 255.0f + 0.5f);
                        break;
                }
            }

            return Color.FromRgb(r, g, b);
        }
    }
}


Class: GridWithHueProperty
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Wpf11_DependencyProperty
{
    public class GridWithHueProperty : Grid
    {
        public static readonly DependencyProperty HueProperty =
               DependencyProperty.Register("Hue", typeof(Double),
                                           typeof(GridWithHueProperty),
                                           new FrameworkPropertyMetadata(0.1,
                                               OnHuePropertyChanged,
                                               OnCoerceHueProperty),
                                               OnValidateHueProperty);

        // ********************************************************************

        public Double Hue
        {
            get { return (Double) GetValue(HueProperty); }
            set { SetValue(HueProperty, value); }
        }

        // ********************************************************************

        private static void OnHuePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            var sPanel = source as GridWithHueProperty;
            var newValue = (Double)e.NewValue;

            if (sPanel != null)
            {
                sPanel.Background = new SolidColorBrush(ColorMode.HsbToRgb(newValue / 360, 1, 1));
            }

        }

        // ********************************************************************

        private static object OnCoerceHueProperty(DependencyObject d, object basevalue)
        {
            return (double)basevalue % 360;
        }

        // ********************************************************************

        private static bool OnValidateHueProperty(object value)
        {
            return (double)value >= 0;
        }
    }
}


XAML
<Window x:Class="Wpf11_DependencyProperty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Wpf11_DependencyProperty"
        Title="MainWindow" Height="350" Width="525">
    

    <StackPanel Orientation="Vertical">
        <local:GridWithHueProperty Height="50" Hue="0" x:Name="MyGrid"/>
        
        <StackPanel Orientation="Horizontal">
            <Button Content="150" Width="100" Click="ChangeTo150"/>
            <Button Content="-1" Width="100" Click="ChangeToNegative"/>
            
            <Button Content="Start Anim" Width="100">
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation From="0" To="360" Duration="0:0:10" Storyboard.TargetName="MyGrid" Storyboard.TargetProperty="Hue"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            
            
        </StackPanel>
        
        <TextBlock Text="Error message:"/>
        <TextBox Height="100" Name="ErrorTbox"/>
        
        <TextBlock Text="Info message:"/>
        <TextBox Height="30" Name="InfoTbox"/>
    </StackPanel>
    
</Window>


XAML mögöttes kód
using System;
using System.ComponentModel;
using System.Windows;

namespace Wpf11_DependencyProperty
{

    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            var dp = DependencyPropertyDescriptor.FromProperty(GridWithHueProperty.HueProperty, typeof(GridWithHueProperty));
            dp.AddValueChanged(MyGrid, Handler);
        }

        private void Handler(object sender, EventArgs eventArgs)
        {
            var msp = sender as GridWithHueProperty;
            var helper = DependencyPropertyHelper.GetValueSource(msp, GridWithHueProperty.HueProperty);
            InfoTbox.Text = msp.Hue.ToString();
        }


        private void ChangeTo150(object sender, RoutedEventArgs e)
        {
            MyGrid.Hue = 150;
        }

        private void ChangeToNegative(object sender, RoutedEventArgs e)
        {
            try
            {
                MyGrid.Hue = -1;
            }
            catch (Exception exception)
            {
                ErrorTbox.Text = exception.ToString();
            }
        }
    }
}

ColorMode osztály

Amely egyetlen statikus függvénnyel rendelkezik és a megadott HSB értéket RGB-re alakítja.
A bejövő paraméterek 0 és 1 közötti értéket vehetnek fel.

A hue értéke egy 360 fokos körcikk 360-ad része. 
A stauration és a brightness egy 100% skála 0-1 közé konvertált értéke. 

A program csak maximális színekkel foglalkozik, ezért a saturation és a brightness paraméterek mindig 1 értéket kapnak.


GridWithHueProperty osztály.

Amely magát a DependencyProperty-t tartalmazza.
Ez az újonnan létrehozott Grid típus, amely a Grid-ből kerül származtatásra
Minden DependenyProperty publikus, static és  Property végződésű.

public static readonly DependencyProperty HueProperty =
               DependencyProperty.Register("Hue", typeof(Double),
                                           typeof(GridWithHueProperty),
                                           new FrameworkPropertyMetadata(0.1,
                                               OnHuePropertyChanged,
                                               OnCoerceHueProperty),
                                               OnValidateHueProperty);

A tulajdonságot a DependencyProperty.Register statikus metódus használatával kell regisztrálni.
Az első paraméter a tulajdonság neve (immáron Property végződés nélkül).
A második a tulajdonság típusa.
A harmadik paraméter használatával MetaAdatok adhatóak meg, mely segítségével jelenleg az alapérték (0.1), valamint 3 callBack függvény kerül beállításra.

Mind a register mind a FrameworkPropertyMetadata túlterhelt függvények.

Ezt követi egy szokásos .Net tulajdonság, amely egy szimpla warpper, a Hue tulajdonságon keresztül biztosít hozzáférést a DependencyProperty-hez mind az XAML-ben mind a kódban.

public Double Hue
        {
            get { return (Double) GetValue(HueProperty); }
            set { SetValue(HueProperty, value); }
        }
 
A metadatként megadott 3 callBack függvény szolgáltat felületet a tulajdonság változásának észlelésére illetve az ilyenkor szükséges kód lefuttatására.

Az OnHuePropertyChanged metódus, mint ahogy a neve is mutatja az érték megváltozásakor kerül meghívásra. A három függvény közül ez fut le legutoljára (ha eljut idáig a program)
Itt kerül kiszámításra a háttér színe a megadott hue érték alapján.

var sPanel = source as GridWithHueProperty;
var newValue = (Double)e.NewValue;
if (sPanel != null)
{
    sPanel.Background = new SolidColorBrush(ColorMode.HsbToRgb(newValue / 360, 1, 1));
}

Az OnCoerceHueProperty metódus a beérkező paramétert helyezi a megfelelő értéktartományba.
Ezt legegyszerűbben egy Slider elem segítségével lehet demonstrálni, ahol az elemnek van egy minimum és egy maximum értéke. A bejövő értéket itt lehet ellenőrizni és szükség esetén min/max közé helyezni.
Jelen esetben az érték csak 0 és 360 között lehet.

private static object OnCoerceHueProperty(DependencyObject d, object basevalue)
{
    return (double)basevalue % 360;
}

Az OnValidateHueProperty a beérkező elem helyességét vizsgálja. 
Amennyiben false értékkel tér vissza, úgy futás közben kivétel keletkezik.
Jelen esetben csak pozitív érték a megengedett, negatív esetén hibát generál.

private static bool OnValidateHueProperty(object value)
{
    return (double)value >= 0;
}

XAML

 Az XAML-ben elsőként a szükséges NameSpace deklarálására van szükség.
xmlns:local="clr-namespace:Wpf11_DependencyProperty"

Nálam a program a Wpf11_DependencyProperty namespace alatt található, ide a te programod által használt namespace nevét kell írni.

A <local:GridWithHueProperty Height="50" Hue="0" x:Name="MyGrid"/> elhelyezi az újonnan létrehozott  Grid típust a stackpanelben és a hue értékét 0-ra állítja (piros lesz a háttérszín)

Az elhelyezett 3 gomb funkciói:
A hue értékét -1-re állítja
A hue értékét 150-re állítja

A harmadik gomb egy animációt indít:
<Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation From="0" To="360" Duration="0:0:10" Storyboard.TargetName="MyGrid" Storyboard.TargetProperty="Hue"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Button.Triggers>

Gombnyomás hatására 0-tól(From) 360-ig(To) indít egy animációt amely 10 másodpercig tart (Duration="0:0:10").
A Storyboard.TargetName="MyGrid" az előbb létrehozott Grid nevére mutat
A Storyboard.TargetProperty="Hue" pedig arra a tulajdonságra, amelyet animálni kell.

MainWindow osztály

A MainWindow osztályban két függvény nem több mint a gombokra elhelyezett Click esemény feldolgozása.
Az egyik esetében 150-re állítódik a hue értéke, ami rögtön látható is.
A másik esetében -1 értéket próbál felvenni, ezt viszont az előbb említett OnValidateHueProperty nem engedi, és kivétel történik.


A konstruktorban található még az alábbi két sor
var dp = DependencyPropertyDescriptor.FromProperty(GridWithHueProperty.HueProperty, typeof(GridWithHueProperty));

dp.AddValueChanged(MyGrid, Handler);

Használatával a DependencyProperty osztályon kívül is értesítés szerezhető a tulajdonság értékének megváltozásáról.

Amennyiben az érték változik, úgy meghívásrakerül az alábbi függvény, amely a tulajdonság aktuális értékét kiírja.
private void Handler(object sender, EventArgs eventArgs)
{
    var msp = sender as GridWithHueProperty;
    InfoTbox.Text = msp.Hue.ToString();

3 megjegyzés:

  1. Azt ez az írás nem magyarázza [vagy csak én nem éreztem ki belőle], hogy miért is nem egyszerűen property-t használnak a WPF-ben.

    Én valami olyasmit "érzek", hogy a dependency property belső megvalósításban mintegy stack szerű szerzet. Az aljára kerül egy default érték, és ez mindig ott marad. Ha írunk bele, akkor nem felülírja az értéket (mint egy jólnevelt property), hanem eltárolja az újat, addig, amíg olyan környezetben van, ahol ez szükséges, és amint kilép a "környezetből", akkor eldobja-elfeledi és visszatér az előző értékhez.
    Pl. van egy default, azt felülírtuk (push-oltuk) egy új értékkel, erre ráfut egy trigger (pl. mert az egér az elem fölé kerül) és az is felüldefiniálja az értéket. De ahogy kilépünk az egyes környezetekből (pl. egér elhagyja és a trigger nem állítja át többet) akkor visszatér a dependency property az eggyel korábbi értékéhez.

    Én legalábbis valami ilyesmi "modellt" alkottam róla magamban. ;)

    De örülnék, ha "okosabbak" tisztáznák a képet.

    VálaszTörlés
  2. Változtattam rajta, kiegészítettem az elejét egy kicsit részletesebb leírással.
    A tuljdonság triggerezéséről is írok, amint idpm engedi.

    VálaszTörlés
  3. Szerintem sokat segített az íráson a változtatás. [bár engem leginkább az első néhány tucat sor érdekel jelenleg]
    Köszi.
    Így már nekem is tisztább az "illékony és változékony" érték, hogy ez mitől és hogyan alakul mégis.
    Illetve egyértelművé vált, hogy nem tárol valójában mindent, hanem egy fán visszafelé lépked és keres "létező" (meghatározott) értéket.
    Ha nem talál tovább lépked a "gyökér" felé és tovább keres.

    VálaszTörlés