Perhaps the most important aspect of the DependencyObject
is its support for hosting dependency properties. While these appear and can be used much like the C# properties we have previously worked with, internally they are managed very differently. Consider when we place a <TextBox>
in a <Grid>
:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Name="textBox" Grid.Column="1" Grid.Row="1"/>
</Grid>
Where do the Column
and Row
properties come from? They aren’t defined on the TextBox
class - you can check the documentation. The answer is they are made available through the dependency property system.
At the heart of this system is a collection of key/value pairs much like the Dictionary
. When the XAML code Grid.Column="1"
is processed, this key and value are added to the TextBox
’s dependency properties collection, and is thereafter accessible by the WPF rendering algorithm.
The DependencyObject
exposes these stored values with the GetValue(DependencyProperty)
and SetValue(DependencyProperty, value)
methods. For example, we can set the Column
property to 2
with:
textBox.SetValue(Grid.ColumnProperty, 2);
We can also create new dependency properties on our own custom classes extending the DependencyObject
(which is also a base class for all WPF controls). Let’s say we are making a custom control for entering number values on a touch screen, which we’ll call NumberBox
. We can extend a UserControl
to create a textbox centered between two buttons, one to increase the value, and one to decrease it:
<UserControl x:Class="CustomDependencyObjectExample.NumberBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CustomDependencyObjectExample"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0">+</Button>
<TextBox Grid.Column="1" />
<Button Grid.Column="2">-</Button>
</Grid>
</UserControl>
Now, let’s assume we want to provide a property Step
of type double
, which is the amount the number should be incremented when the “+” or “-” button is pressed.
The first step is to register the dependency property by creating a DependencyProperty
instance. This will serve as the key to setting and retrieving the dependency property on a dependency object. We register new dependency properties with DependencyProperty.Register(string propertyName, Type propertyType, Type dependencyObjectType)
. The string is the name of the property, the first type is the type of the property, and the second is the class we want to associate this property with. So our Step
property would be registered with:
DependencyProperty.Register(nameof(Step), typeof(double), typeof(NumberBox));
There is an optional fourth property to DependencyProperty.Register()
which is a PropertyMetadata
. This is used to set the default value of the property. We probably should specify a default step, so let’s add a PropertyMetadata
object with a default value of 1:
DependencyProperty.Register(nameof(Step), typeof(double), typeof(NumberBox), new PropertyMetadata(1.0));
The DependencyProperty.Register()
method returns a registered DependencyObject
to serve as a key for accessing our new property. To make sure we can access this key from other classes, we define it as a field that is public
, static
, and readonly
. The naming convention for DependencyProperties
is to name this field by appending “Property” to the name of the property.
Thus, the complete registration, including saving the result to the public static field is:
/// <summary>
/// Identifies the NumberBox.Step XAML attached property
/// </summary>
public static readonly DependencyProperty StepProperty = DependencyProperty.Register(nameof(Step), typeof(double), typeof(NumberBox), new PropertyMetadata(1.0));
We also want to declare a traditional property with the name “Step”. But instead of declaring a backing field, we will use the key/value pair stored in our DependencyObject
using GetValue()
and SetValue()
:
/// <summary>
/// The amount each increment or decrement operation should change the value by
/// </summary>
public double Step
{
get { return (double)GetValue(StepProperty); }
set { SetValue(StepProperty, value); }
}
As dependency property values are stored as an object
, we need to cast the value to a the appropriate type when it is returned.
One of the great benefits of dependency properties is that they can be set using XAML. I.e. we could declare an instance of our <NumberBox>
and set its Step
using an attribute:
<StackPanel>
<NumberBox Step="3.0"/>
</StackPanel>