Dependency properties are a completely new
implementation of properties—one that has a significant amount of added
value. You need dependency properties to plug into core WPF features
such as animation, data binding, and styles.
Most of the properties that are exposed by WPF
elements are dependency properties. In all the examples you've seen up
to this point, you've been using dependency properties without realizing
it. That's because dependency properties are designed to be consumed in
the same way as normal properties.
However, dependency properties are not
normal properties. It's comforting to think of a dependency property as
a normal property (defined in the typical .NET fashion) with a set of
WPF features added on. Conceptually, dependency features behave this
way, but that's not how they're implemented behind the scenes. The
simple reason why is performance. If the designers of WPF simply added
extra features on top of the .NET property system, they'd need to create
a complex, bulky layer for your code to travel through. Ordinary
properties could not support all the features of dependency properties
without this extra overhead.
Dependency properties are a WPF-specific creation.
However, the dependency properties in the WPF libraries are always
wrapped by ordinary .NET property procedures. This makes them usable in
the normal way, even with code that has no understanding of the WPF
dependency property system. It seems odd to think of an older technology
wrapping a newer one, but that's how WPF is able to change a
fundamental ingredient such as properties without disrupting the rest of
the .NET world.
1. Defining a Dependency Property
You'll spend much more time using dependency
properties than creating them. However, there are still many reasons
that you'll need to create your own dependency properties. Obviously,
they're a key ingredient if you're designing a custom WPF element.
However, they're also required in some cases if you want to add data
binding, animation, or another WPF feature to a portion of code that
wouldn't otherwise support it. Creating a dependency property isn't
difficult, but the syntax takes a little getting used to. It's
thoroughly different from creating an ordinary .NET property.
NOTE
You can add dependency properties only to
dependency objects—classes that derive from DependencyObject.
Fortunately, most of the key pieces of WPF infrastructure derive
indirectly from DependencyObject, with the most obvious example being
elements.
The first step is to define an object that represents
your property. This is an instance of the DependencyProperty class. The
information about your property needs to be available all the time, and
possibly even shared among classes (as is common with WPF elements).
For that reason, your DependencyProperty object must be defined as a
static field in the associated class.
For example, the FrameworkElement class defines a
Margin property that all elements share. Unsurprisingly, Margin is a
dependency property. That means it's defined in the FrameworkElement
class like this:
public class FrameworkElement: UIElement, ...
{
public static readonly DependencyProperty MarginProperty;
...
}
By convention, the field that defines a dependency property has the name of the ordinary property, plus the word Property
at the end. That way, you can separate the dependency property
definition from the name of the actual property. The field is defined
with the readonly keyword, which means it can be set only in the static
constructor for the FrameworkElement, which is the task you'll undertake
next.
2. Registering a Dependency Property
Defining the DependencyProperty object is just the
first step. For it to become usable, you need to register your
dependency property with WPF. This step needs to be completed before any
code uses the property, so it must be performed in a static constructor
for the associated class.
WPF ensures that DependencyProperty objects can't be
instantiated directly, because the DependencyProperty class has no
public constructor. Instead, a DependencyObject instance can be created
only using the static DependencyProperty.Register() method. WPF also
ensures that DependencyProperty objects can't be changed after they're
created, because all DependencyProperty members are read-only. Instead,
their values must be supplied as arguments to the Register() method.
The following code shows an example of how a
DependencyProperty must be created. Here, the FrameworkElement class
uses a static constructor to initialize the MarginProperty:
static FrameworkElement()
{
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(
new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness), typeof(FrameworkElement), metadata,
new ValidateValueCallback(FrameworkElement.IsMarginValid));
...
}
There are two steps involved in registering a
dependency property. First, you create a FrameworkPropertyMetadata
object that indicates what services you want to use with your dependency
property (such as support for data binding, animation, and journaling).
Next, you register the property by calling the static
DependencyProperty.Register() method. At this point, you are responsible
for supplying a few key ingredients:
The property name (Margin in this example)
The data type used by the property (the Thickness structure in this example)
The type that owns this property (the FrameworkElement class in this example)
Optionally, a FrameworkPropertyMetadata object with additional property settings
Optionally, a callback that performs validation for the property
The first three details are all straightforward. The
FrameworkPropertyMetadata object and the validation callback are more
interesting.
You use the FrameworkPropertyMetadata to configure
additional features for your dependency property. Most of the properties
of the FrameworkPropertyMetadata class are simple Boolean flags that
you set to flip on a feature. (The default value for each Boolean flag
is false.) A few are callbacks that point to custom methods that you
create to perform a specific task.
One—FrameworkPropertyMetadata.DefaultValue—sets the default value that
WPF will apply when the property is first initialized. Table 1 lists all the FrameworkPropertyMetadata properties.
Table 1. Properties of the FrameworkPropertyMetadata Class
Name | Description |
---|
AffectsArrange, AffectsMeasure, AffectsParentArrange, and AffectsParentMeasure | If
true, the dependency property may affect how adjacent elements (or the
parent element) are placed during the measure pass and the arrange pass
of a layout operation. For example, the Margin dependency property sets
AffectsMeasure to true, signaling that if the margin of an element
changes, the layout container needs to repeat the measure step to
determine the new placement of elements. |
AffectsRender | If
true, the dependency property may affect something about the way an
element is drawn, requiring that the element be repainted. |
BindsTwoWayByDefault | If
true, this dependency property will use two-way data binding instead of
one-way data binding by default. However, you can specify the binding
behavior you want explicitly when you create the binding. |
Inherits | If
true, the dependency property value propagates through the element tree
and can be inherited by nested elements. For example, Font is an
inheritable dependency property—if you set it on a higher-level element,
it's inherited by nested elements, unless they explicitly override it
with their own font settings. |
IsAnimationProhibited | If true, the dependency property can't be used in an animation. |
IsNotDataBindable | If true, the dependency property can't be set with a binding expression. |
Journal | If true, this dependency property will be persisted to the journal (the history of visited pages) in a page-based application. |
SubPropertiesDoNotAffectRender | If true, WPF will not rerender an object if one of its subproperties (the property of a property) changes. |
DefaultUpdateSourceTrigger | This
sets the default value for the Binding.UpdateSourceTrigger property
when this property is used in a binding expression. The
UpdateSourceTrigger determines when a databound value applies its
changes. You can set the UpdateSourceTrigger property manually when you
create the binding. |
DefaultValue | This sets the default value for the dependency property. |
CoerceValueCallback | This provides a callback that attempts to "correct" a property value before it's validated. |
PropertyChangedCallback | This provides a callback that is called when a property value is changed. |
3. Adding a Property Wrapper
The final step to creating a dependency property is
to wrap it in a traditional .NET property. However, whereas typical
property procedures retrieve or set the value of a private field, the
property procedures for a WPF property use the GetValue() and SetValue()
methods that are defined in the base DependencyObject class. Here's an
example:
public Thickness Margin
{
set { SetValue(MarginProperty, value); }
get { return (Thickness)GetValue(MarginProperty); }
}
When you create the property wrapper, you should
include nothing more than a call to SetValue() and a call to GetValue(),
as in the previous example. You should not
add any extra code to validate values, raise events, and so on. That's
because other features in WPF may bypass the property wrapper and call
SetValue() and GetValue() directly. (One example is when a compiled XAML
file is parsed at runtime.) Both SetValue() and GetValue() are public.
NOTE
The property wrapper isn't the right place to
validate data or raise an event. However, WPF does provide a place for
this code; the trick is to use dependency property callbacks. Validation
should be performed through the
DependencyProperty.ValidateValueCallback shown previously, while events
can be raised from the FrameworkPropertyMetadata.PropertyChangedCallback
shown in the next section.
You now have a fully functioning dependency property,
which you can set just like any other .NET property using the property
wrapper:
myElement.Margin = new Thickness(5);
There's one extra detail. Dependency properties
follow strict rules of precedence to determine their current value. Even
if you don't set a dependency property directly, it may already have a
value—perhaps one that's applied by a binding, style, or animation, or
one that's inherited through the element tree. (You'll learn more about
these rules of precedence in the next section, "How WPF Uses Dependency
Properties.") However, as soon as you set the value directly, it
overrides all these other influences.
At some point later, you may want to remove your
local value setting and let the property value be determined as though
you never set it. Obviously, you can't accomplish this by setting a new
value. Instead, you need to use another method that's inherited from
DependencyObject: the ClearValue() method. Here's how it works:
myElement.ClearValue(FrameworkElement.MarginProperty);
4. How WPF Uses Dependency Properties
Dependency
properties are required for a range of WPF features. However, all of
these features work through two key behaviors that every dependency
property supports—change notification and dynamic value resolution.
Contrary to what you might expect, dependency properties do not
automatically fireevents to let you know when a property value changes.
Instead, they trigger a protected method named
OnPropertyChangedCallback(). This method passes the information along to
two WPF services (data binding and triggers). It also calls the
PropertyChangedCallback, if one is defined.
NOTE
If you're dealing with a control that you've
created, you can use the property callback mechanism to react to
property changes and even raise an event. Many common controls use this
technique for properties that correspond to user-supplied information.
For example, the TextBox provides a TextChanged event, and the ScrollBar
provides a ValueChanged event. A control can implement functionality
like this using the PropertyChangedCallback, but this functionality
isn't exposed from dependency properties in a general way for
performance reasons.
The second feature that's key to the way dependency
properties work is dynamic value resolution. This means when you
retrieve the value from a dependency property, WPF takes several factors
into consideration.
This behavior gives dependency properties their name—in essence, a dependency property depends
on multiple property providers, each with its own level of precedence.
When you retrieve a value from a property value, the WPF property system
goes through a series of steps to arrive at the final value. First, it
determines the base value for the property by considering the following
factors, arranged from lowest to highest precedence:
The default value (as set by the FrameworkPropertyMetadata object)
The
inherited value (if the FrameworkPropertyMetadata.Inherits flag is set
and a value has been applied to an element somewhere up the containment
hierarchy)
The value from a theme style
The value from a project style
The local value (in other words, a value you've set directly on this object using code or XAML)
As this list shows, you override the entire hierarchy
by applying a value directly. If you don't, the value is determined by
the next applicable item up on the list.
NOTE
One of the advantages of this system is that it's
very economical. If the value of a property has not been set locally,
WPF will retrieve its value from a style, another element, or the
default. In this case, no memory is required to store the value. You can
quickly see the savings if you add a few buttons to a form. Each button
has dozens of properties, which, if they are set through one of these
mechanisms, use no memory at all.
WPF follows the previous list to determine the base value
of a dependency property. However, the base value is not necessarily
the final value that you'll retrieve from a property. That's because WPF
considers several other providers that can change a property's value.
Here's the four-step process WPF follows to determine a property value:
Determine the base value (as described previously).
If
the property is set using an expression, evaluate that expression.
Currently, WPF supports two types of expression: data binding and resources .
If this property is the target of animation, apply that animation.
Run the CoerceValueCallback to "correct" the value.
Essentially, dependency properties are hardwired into
a small set of WPF services. If it weren't for this infrastructure,
these features would add unnecessary complexity and significant
overhead.
In future versions of WPF, the dependency property
pipeline could be extended to include additional services. When you
design custom elements , you'll probably use dependency properties for most (if not all) of their public properties.
|
|
5. Shared Dependency Properties
Some classes share the same dependency property, even
though they have separate class hierarchies. For example, both
TextBlock.FontFamily and Control.FontFamily point to the same static
dependency property, which is actually defined in the TextElement class
and TextElement.FontFamilyProperty. The static constructor of
TextElement registers the property, but the static constructors of
TextBlock and Control simply reuse it by calling the
DependencyProperty.AddOwner() method:
TextBlock.FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
You can use the same technique when you create your
own custom classes (assuming the property is not already provided in the
class you're inheriting from, in which case you get it for free). You
can also use an overload of the AddOwner() method that allows you to
supply a validation callback and a new FrameworkPropertyMetadata that
will apply only to this new use of the dependency property.
Reusing dependency properties can lead to some
strange side effects in WPF, most notably with styles. For example, if
you use a style to set the TextBlock.FontFamily property automatically,
your style will also affect the Control.FontFamily property because
behind the scenes both classes use the same dependency property.
6. Attached Dependency Properties
An attached property is a dependency property, and it's managed by the
WPF property system. The difference is that an attached property applies
to a class other than the one where it's defined.
The Grid class defines the attached properties Row and
Column, which you set on the contained elements to indicate where they
should be positioned. Similarly, the DockPanel defines the attached
property Dock, and the Canvas defines the attached properties Left,
Right, Top, and Bottom.
To define an attached property, you use the
RegisterAttached() method instead of Register(). Here's an example that
registers the Grid.Row property:
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(
0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged));
Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int),
typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative));
As with an ordinary dependency property, you can supply a FrameworkPropertyMetadata object and a ValidateValueCallback.
When creating an attached property, you don't define
the .NET property wrapper. That's because attached properties can be set
on any dependency object. For example,
the Grid.Row property may be set on a Grid object (if you have one Grid
nested inside another) or on some other element. In fact, the Grid.Row
property can be set on an element even if that element isn't in a
Grid—and even if there isn't a single Grid object in your element tree.
Instead of using a .NET property wrapper, attached
properties require a pair of static methods that can be called to set
and get the property value. These methods use the familiar SetValue()
and GetValue() methods (inherited from the DependencyObject class). The
static methods should be named SetPropertyName() and GetPropertyName().
Here are the static methods that implement the Grid.Row attached property:
public static int GetRow(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException(...);
}
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value)
{
if (element == null)
{
throw new ArgumentNullException(...);
}
element.SetValue(Grid.RowProperty, value);
}
Here's an example that positions an element in the first row of a Grid using code:
Grid.SetRow(txtElement, 0);
Alternatively, you can call the SetValue() or GetValue() method directly and bypass the static methods:
txtElement.SetValue(Grid.RowProperty, 0);
The SetValue() method also provides one
brain-twisting oddity. Although XAML doesn't allow it, you can use an
overloaded version of the SetValue() method in code to attach a value
for any dependency property, even if that property isn't defined as an attached property. For example, the following code is perfectly legitimate:
ComboBox comboBox = new ComboBox();
...
comboBox.SetValue(PasswordBox.PasswordCharProperty, "*");
Here, a value for the PasswordBox.PasswordChar
property is set for a ComboBox object, even though
PasswordBox.PasswordCharProperty is registered as an ordinary dependency
property, not an attached property. This action won't change the way
the ComboBox works—after all, the code inside the ComboBox won't look
for the value of a property that it doesn't know exists—but you could
act upon the PasswordChar value in your own code.
Although rarely used, this quirk provides
some more insight into the way the WPF property systemworks, and it
demonstrates its remarkable extensibility. It also shows that even
though attached properties are registered with a different method than
normal dependency properties, in the eyes of WPF there's no real
distinction. The only difference is what the XAML parser allows. Unless
you register your property as an attached property, you won't be able to
set it in on other elements in your markup.