Monday, May 4, 2009

DependencyProperty and DependencyObject

Up until now, you would create properties in a class using this syntax:

public
class
MyObject
{
    private
string MyPropertyValue;

    public
string MyProperty
    {
        set
        {
            MyPropertyValue = value;
        }
        get
        {
            return MyPropertyValue;
        }
    }
}

This is fine but then you have a lot of code to add to handle additional features such as PropertyChanged event, data binding, default value, validation.

DependencyObject and DependencyProperty provide a simpler way to implement properties and provide directly off the shelf:

  • DataBinding
  • Events
  • Validation
  • Default Value

As well as elements more specific to Windows Foundation and .NET 3.0:

  • Animation
  • Style
  • Attached properties
  • Expressions

So let's see how to use these new classes.
First we create a class similar to the one above:

public
class
Lift : DependencyObject
{
    public
static
DependencyProperty FloorProperty =
        DependencyProperty.Register("Floor", typeof(int), typeof(Lift));

    public
int Floor
    {
        get { return (int)GetValue(FloorProperty); }
        set { SetValue(FloorProperty, value); }
    }
}

I have a class Lift with a property called Floor. Already I can implement many of the WPF feature. The Lift class is ready for DataBinding, the Floor property will accept expressions to be set and can be used as part of another expression.
Let's look at this simple XAML code:

<Window
x:Class="TestWPF1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:AppCode="clr-namespace:TestWPF1"
    Title="TestWPF1"
Height="300"
Width="300"
    >
    <
Window.Resources>
        <
AppCode:Lift
x:Key="Lift1"
Floor="1" />
    </
Window.Resources>
    <
StackPanel
Orientation="Horizontal">
        <
TextBlock
Text="{Binding Path=Floor}" />
        <
StackPanel.DataContext>
            <
Binding
Source="{StaticResource Lift1}" />
        </
StackPanel.DataContext>
    </
StackPanel>
</
Window>

I created an instance of Lift in XAML and databound it to the StackPanel.

Still I rely on the user to set the Floor value to a correct value. Why not assign a default value:

    public
static
DependencyProperty FloorProperty =
        DependencyProperty.Register("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0));

By setting the default value in the metadata, I can now create an instance of the Lift class without specifying the value for Floor:

    <AppCode:Lift
x:Key="Lift1" />

Now I'd like to display the kind of goods for sale on the floor of that building. I add a second property to my class called "Goods".

    public
static
DependencyProperty GoodsProperty =
        DependencyProperty.Register("Goods", typeof(string), typeof(Lift));

    public
string Goods
    {
        get { return (string)GetValue(GoodsProperty); }
        set { SetValue(GoodsProperty, value); }
    }

And I can use it directly in XAML:

    <AppCode:Lift
x:Key="Lift1"
Floor="1"
Goods="Perfumes" />
    …
    <
TextBlock
Text="{Binding Path=Floor}" />
    <
TextBlock
Text=": " />
    <
TextBlock
Text="{Binding Path=Goods}" />

But I'd like set the goods value directly in my class according to the floor set. I can do that by handling the PropertyChanged event on the Floor property. I just expand a bit the property definition to get the Changed event:


 

        public
static
DependencyProperty FloorProperty =
            DependencyProperty.Register("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged));

        private
static
void OnFloorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs arg)
        {
            Lift l = (Lift)obj;
            switch (l.Floor)
            {
                case 0:
                    l.Goods = "Books";
                    break;
                case 1:
                    l.Goods = "Perfumes";
                    break;
                case 2:
                    l.Goods = "Toys";
                    break;
                case 3:
                    l.Goods = "Kitchenware";
                    break;
            }
        }

So in XAML, it looks like:


 

    <AppCode:Lift
x:Key="Lift1"
Floor="1" />
    …
    <
TextBlock
Text="{Binding Path=Floor}" />
    <
TextBlock
Text=": " />
    <
TextBlock
Text="{Binding Path=Goods}" />

I no longer have to supply the Goods value, it's set when the Floor value is modified.


 

Ok so we've looked at Databinding, Property Changed, Default value, now we need validation. My building doesn't have an infinite number of floors so I'd like to restrict the Floor value to between 0 and 3.


 

I can add validation by handling the Validate event:


 

    public
static
DependencyProperty FloorProperty =
        DependencyProperty.Register(("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged)),new
ValidateValueCallback(OnFloorValidate));

        private
static
bool OnFloorValidate(object obj)
        {
            return obj is
int && (int)obj >= 0 && (int)obj <= 3;
        }

This not only validates that the value is between 0 and 3 but also checks that the value is actually an integer. An exception is thrown if you try to set the property to an incorrect value.


 

The final class looks like:


 

    class
Lift : DependencyObject
    {
        public
static
DependencyProperty FloorProperty =
            DependencyProperty.Register(("Floor", typeof(int),typeof(Lift),new
PropertyMetadata(0,new
PropertyChangedCallback(OnFloorChanged)),new
ValidateValueCallback(OnFloorValidate));

        private
static
void OnFloorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs arg)
        {
            Lift l = (Lift)obj;
            switch (l.Floor)
            {
                case 0:
                    l.Goods = "Books";
                    break;
                case 1:
                    l.Goods = "Perfumes";
                    break;
                case 2:
                    l.Goods = "Toys";
                    break;
                case 3:
                    l.Goods = "Kitchenware";
                    break;
            }
        }

        private
static
bool OnFloorValidate(object obj)
        {
            return obj is
int && (int)obj >= 0 && (int)obj <= 3;
        }

        public
int Floor
        {
            get { return (int)GetValue(FloorProperty); }
            set { SetValue(FloorProperty, value); }
        }

        public
static
DependencyProperty GoodsProperty =
        DependencyProperty.Register("Goods", typeof(string), typeof(Lift));

        public
string Goods
        {
            get { return (string)GetValue(GoodsProperty); }
            set { SetValue(GoodsProperty, value); }
        }
    }

And the XAML:


 

<Window
x:Class="TestWPF1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:AppCode="clr-namespace:TestWPF1"
    Title="TestWPF1"
Height="300"
Width="300"
    >
    <
Window.Resources>
        <
AppCode:Lift
x:Key="Lift1"
Floor="1" />
        <
AppCode:Lift
x:Key="Lift2"
Floor="3" />
    </
Window.Resources>
    <
StackPanel
Orientation="Vertical">
        <
StackPanel
Orientation="Horizontal">
            <
TextBlock
Text="{Binding Path=Floor}" />
            <
TextBlock
Text=", " />
            <
TextBlock
Text="{Binding Path=Goods}" />
            <
StackPanel.DataContext>
                <
Binding
Source="{StaticResource Lift1}" />
            </
StackPanel.DataContext>
        </
StackPanel>
        <
StackPanel
Orientation="Horizontal">
            <
TextBlock
Text="{Binding Path=Floor}" />
            <
TextBlock
Text=", " />
            <
TextBlock
Text="{Binding Path=Goods}" />
            <
StackPanel.DataContext>
                <
Binding
Source="{StaticResource Lift2}" />
            </
StackPanel.DataContext>
        </
StackPanel>
    </
StackPanel>
</
Window>

0 comments:

Post a Comment