DEV Community

yohan
yohan

Posted on Edited on Originally published at cnblogs.com

Deep Dive into WPF — Dependency Property (Part 2)

Picking up from the previous article, we introduced the principles behind dependency properties and implemented a simple DependencyProperty (DP). This article focuses on how to use DPs and what to watch out for.

Review

DP class diagram

Dependency properties are used by DependencyObject. So what does a typical usage scenario look like?

Using DependencyProperty

A simple usage looks like this:

 1: public class SimpleDO : DependencyObject
 2: {
 3:     public static readonly DependencyProperty IsActiveProperty =
 4:         DependencyProperty.Register("IsActive", typeof(bool), typeof(SimpleDO),
 5:             new PropertyMetadata((bool)false));
 6:
 7:     public bool IsActive
 8:     {
 9:         get { return (bool)GetValue(IsActiveProperty); }
10:         set { SetValue(IsActiveProperty, value); }
11:     }
12: }

SimpleDO sDo = new SimpleDO();
sDo.IsActive = true;
Enter fullscreen mode Exit fullscreen mode

Here we register the DP with DependencyProperty.Register. The Register function has many overloads; the fullest form looks like this:

public static DependencyProperty Register(string name, Type propertyType, Type ownerType,
                                         PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
Enter fullscreen mode Exit fullscreen mode

The first four parameters were covered in the previous article. They mainly determine the DP's key in the global map, the property's type, and the internal property metadata. The last parameter is a delegate used to validate the value.

Setting validation aside for a moment, let's look at PropertyMetadata. As mentioned earlier, PropertyMetadata can be subclassed, and a subclass can call OverrideMetadata to override it. The strategies that the WPF property system supports for dependency properties are encapsulated in the metadata. What kinds of PropertyMetadata are there?

The common ones are mainly FrameworkPropertyMetadata, UIPropertyMetadata, and PropertyMetadata. Their inheritance relationship is F → U → P. Taking the most complex one, what does FrameworkPropertyMetadata provide?

FrameworkPropertyMetadata

FrameworkPropertyMetadata's constructors have many overloads; a representative full constructor looks like this:

public FrameworkPropertyMetadata( object defaultValue,
                                  FrameworkPropertyMetadataOptions flags,
                                  PropertyChangedCallback propertyChangedCallback,
                                  CoerceValueCallback coerceValueCallback,
                                  bool isAnimationProhibited,
                                  UpdateSourceTrigger defaultUpdateSourceTrigger);
Enter fullscreen mode Exit fullscreen mode

The first parameter is the default value, and the last two are whether animation is allowed and the update strategy used during binding—I won't go into those in detail. Focus on the third and fourth parameters: the two callbacks. Together with the ValidateValueCallback mentioned earlier, these three callbacks represent Validate, PropertyChanged (change notification), and Coerce. Of course, as metadata, FrameworkPropertyMetadata merely stores strategy information; the WPF property system provides the functionality based on that information and invokes the supplied delegates at the appropriate times.

So how does the WPF property system determine a property's value?

The Rules for Resolving a DependencyProperty

Borrowing a well-known diagram, let's walk through the basic steps the WPF property system uses when operating on a dependency property:

Dependency property value resolution order

  • Step 1: Determine the Base Value. The same property can be assigned in many places. For example, a control's Background might be set both in a Style and in the control's constructor. The Base Value is the highest-priority value among those assignments.
  • Step 2: Expression evaluation. If the value obtained in step 1 is an expression value (Expression)—for example, a binding—the WPF property system needs to convert it into a concrete value.
  • Step 3: Animation. If the property is currently being animated, the value produced by the animation takes precedence over the value obtained earlier. This is the well-known "animation wins" rule in WPF.
  • Step 4: Coercion. If we supplied a CoerceValueCallback in the FrameworkPropertyMetadata, the WPF property system will call back into our delegate to force-assign the data. During property assignment, Coerce has the highest priority—even higher than animation.
  • Step 5: Validation. If a ValidateValueCallback was passed at Register time, WPF will finally call our delegate to validate the data. If the data is invalid, an exception is thrown to signal that.

So how should these features be used?

A Simple Example

A small example to illustrate the process:

public class SimpleDO : DependencyObject
{
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(double), typeof(SimpleDO),
            new FrameworkPropertyMetadata((double)0.0,
                FrameworkPropertyMetadataOptions.None,
                new PropertyChangedCallback(OnValueChanged),
                new CoerceValueCallback(CoerceValue)),
            new ValidateValueCallback(IsValidateValue));

    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine("ValueChanged new value is {0}", e.NewValue);
    }

    private static object CoerceValue(DependencyObject d, object value)
    {
        Console.WriteLine("CoerceValue value is {0}", value);
        return value;
    }

    private static bool IsValidateValue(object value)
    {
        Console.WriteLine("ValidateValue value is {0}", value);
        return true;
    }
}

SimpleDO sDo = new SimpleDO();
sDo.Value = 1;
Enter fullscreen mode Exit fullscreen mode

The corresponding output:

ValidateValue value is 0
ValidateValue value is 0
ValidateValue value is 1
CoerceValue value is 1
ValueChanged new value is 1
Enter fullscreen mode Exit fullscreen mode

After the property changes, the PropertyChangeCallback is eventually invoked. The order of Validate and Coerce here looks a bit off—it doesn't strictly follow the Coerce → Validate sequence discussed earlier. WPF optimizes property assignment: when a property is modified, Validate is called first to check whether the incoming value is valid; if it isn't, the subsequent steps are skipped for performance. We can also see that after CoerceValue, ValidateValue is not called again—PropertyChanged is invoked directly. That's because the value was already validated earlier; if Coerce didn't change the value, there's no need to validate again. If Coerce does change the value, ValidateValue will be called again at that point. The "Validate is the last step" rule means that throughout the entire Value assignment process, the final value is guaranteed to have been validated.

Of course, if we apply an animation to Value (SimpleDO would need to inherit from UIElement), for example:

DoubleAnimation animation = new DoubleAnimation(1, 20,
     new Duration(TimeSpan.FromMilliseconds(5000)), FillBehavior.Stop);
sDo.BeginAnimation(SimpleDO.ValueProperty, animation);
Enter fullscreen mode Exit fullscreen mode

Then during the animation, calling sDo.Value = 30 has no effect—though the value is still stored in the LocalValue. When the animation ends, FillBehavior decides whether to keep the animation's final value or return to the LocalValue. With FillBehavior.Stop, Value is 30 after the animation ends; with HoldEnd, the animation's last value of 20 is held indefinitely after it ends.

We'll come back to LocalValue (the local value) shortly. First, let's revisit this example. We passed in three delegates (PropertyChange, Coerce, and Validate). The purpose of PropertyChangeCallback is obvious—it's invoked when the property value changes. But what are Coerce and Validate for?

Coerce and Validate

DependencyObject provides two functions to support invoking Coerce and Validate:

public void CoerceValue(DependencyProperty dp);
public void InvalidateProperty(DependencyProperty dp);
Enter fullscreen mode Exit fullscreen mode

The first function is more commonly used. Take Slider: it has three interacting properties—Value, Minimum, and Maximum. These properties interact under a default rule of Minimum ≤ Value ≤ Maximum. When one of them changes, how do the other two respond and adjust? WPF uses CoerceValue here, and the implementation is simple: when registering Maximum, add a CoerceValueCallback; in the CoerceMaximum function, if Maximum is less than Minimum, set Maximum equal to Minimum. Likewise, a CoerceValueCallback is added to Value to perform the corresponding coercion. Then, when Minimum's ChangedValueCallback fires, call CoerceValue on Maximum and Value.

In one sentence: when one of several interdependent dependency properties changes, call CoerceValue on the dependency properties it affects from within its PropertyChangeCallback. Those dependency properties must of course implement CoerceValueCallback, where they enforce the correctness of the interdependencies.

Validate, on the other hand, mainly checks data validity—for example, whether a double parameter is NaN, and so on.

Relative to other features, Coerce and Validate are not especially common. Among the services the WPF property system provides, FrameworkPropertyMetadataOptions should count as one of the most distinctive and most customizable.

FrameworkPropertyMetadataOptions

An "option" must of course map to some feature. What switches are there?

public enum FrameworkPropertyMetadataOptions
{
      None = 0,
      AffectsMeasure = 1,
      AffectsArrange = 2,
      AffectsParentMeasure = 4,
      AffectsParentArrange = 8,
      AffectsRender = 16,
      Inherits = 32,
      OverridesInheritanceBehavior = 64,
      NotDataBindable = 128,
      BindsTwoWayByDefault = 256,
      Journal = 1024,
      SubPropertiesDoNotAffectRender = 2048,
}
Enter fullscreen mode Exit fullscreen mode

These options fall into two categories. One category is marked with "Affects", indicating what impact the dependency property change will have (it needs to be re-measured, re-drawn, and so on). The other category is everything else, indicating what capability the dependency property gains when that option is selected (default two-way binding, property inheritance, etc.). Let's take a look at property inheritance (Inherits).

Inheritance is familiar: a subclass inherits methods and properties from its parent class. That involves a parent-child relationship—so where is the parent-child relationship for property inheritance?

The inheritability of WPF dependency properties is attached to the object tree—specifically, the logical tree. For example, if a Button is placed inside a Window, the object tree is Window—Button, with Window as the Button's parent. Setting FontSize on the Window also applies to the Button; that is property inheritance. In general, property inheritance propagates down the logical tree unless an object changes the transmission policy. FrameworkElement provides the property:

protected internal InheritanceBehavior InheritanceBehavior { get; set; }
Enter fullscreen mode Exit fullscreen mode

Subclasses can override this property with values such as SkipToAppNow to cut off the inheritance; by default, Frame already breaks the inheritance chain.

Now, back to LocalValue. Together with EffectiveValue, it forms one of the most fascinating stories in dependency properties.

LocalValue and EffectiveValue

Dependency properties are powerful, and WPF never tires of promoting their beauty. Like a magician's endless tricks, there is always a secret underneath. Let's lift the curtain and see what it really is.

In the previous article we implemented an EffectiveValue (EffectiveValueEntry). Although crude, its two main functions were visible. First, it only stores changed values. Second, it stores multiple values internally and selects the current one by priority. As a property, at any moment it should—and only should—expose a single value to the outside. So where do the problems lie? First, spatially: the same dependency property may be assigned in many places—in a constructor, in a Style, inherited from elsewhere, and so on. Second, temporally: those assignments made in different places may change at the same time—for example, while a binding is updating, the property is also being animated. Clear rules are needed to resolve this, which is why two concepts are introduced: BaseValue and LocalValue.

We already said that the first step in processing a dependency property is determining the BaseValue—the base value. "Base" is relative to animation and coercion. While a dependency property is animated or coerced, it displays the animated or coerced value; once those states lapse, it falls back to the base value.

We can call DependencyPropertyHelper's GetValueSource method to obtain information about the current dependency property:

ValueSource source = DependencyPropertyHelper.GetValueSource(sDo, SimpleDO.ValueProperty);
Enter fullscreen mode Exit fullscreen mode

Where ValueSource is:

public struct ValueSource
{
      public BaseValueSource BaseValueSource { get; }
      public bool IsAnimated { get; }
      public bool IsCoerced { get; }
      public bool IsExpression { get; }
}
Enter fullscreen mode Exit fullscreen mode

IsAnimated, IsCoerced, and IsExpression indicate the current state of the dependency property, while BaseValueSource indicates the priority of the current BaseValue. It has:

public enum BaseValueSource
{
      Unknown = 0,
      Default = 1,
      Inherited = 2,
      DefaultStyle = 3,
      DefaultStyleTrigger = 4,
      Style = 5,
      TemplateTrigger = 6,
      StyleTrigger = 7,
      ImplicitStyleReference = 8,
      ParentTemplate = 9,
      ParentTemplateTrigger = 10,
      Local = 11,
}
Enter fullscreen mode Exit fullscreen mode

BaseValueSource priorities increase from low to high, with Local having the highest priority. Local here means a property value explicitly specified in XAML, or assigned manually in code-behind—for example:

<Button x:Name="btn" Width="12"/>
Enter fullscreen mode Exit fullscreen mode

or btn.Width = 12 in code-behind. In other words, once you assign a dependency property in code-behind, any Style value or Trigger for that property loses effect because its priority is not high enough. This situation is very common. Often, when a dependency property misbehaves (a binding doesn't update, a Trigger doesn't react), you can inspect the property's ValueSource to determine whether the DP was set in a way that left its priority too low to respond.

So where does this LocalValue come from—does it mean the Local in BaseValueSource?

Yes. DependencyObject provides ReadLocalValue to read the current LocalValue:

public object ReadLocalValue(DependencyProperty dp);
Enter fullscreen mode Exit fullscreen mode

If you never assigned the dependency property in XAML or in code-behind—even if it was set in a Style—the value read will be DependencyProperty.UnsetValue. If a binding was used in the declaration, the value read will be a BindingExpression; otherwise the current local value is returned.

So what's the difference between LocalValue and EffectiveValue? DependencyObject provides GetValue to obtain the property value—that is the EffectiveValue, the final result of all the magician's tricks—whereas LocalValue is the value set internally. A simple example:

Again using Slider—its Minimum, Value, and Maximum:

 1: Slider slider = new Slider();
 2: slider.Minimum = 0;
 3: slider.Maximum = 10;
 4: slider.Value = 3;
 5:
 6: slider.Minimum = 4; // After set, Value = 4; Value's Local Value = 3;
 7: slider.Minimum = 1; // After set, Value = Value's Local Value = 3;
 8: slider.Minimum = 13; // After set, Value = Maximum = 13;
Enter fullscreen mode Exit fullscreen mode

On line 6, after setting Minimum = 4, Value's Coerce is called. Inside Coerce, because the Value (3) is less than Minimum (4), Value is coerced to 4. But Value's Local value is retained—using ReadLocalValue you can see that Value's LocalValue is still 3. On line 7, after Minimum becomes 1, in Value's Coerce the LocalValue of Value (3) is greater than 1, so the final Value and LocalValue are both 3.

Regarding EffectiveValue and LocalValue, WPF's attitude has always been half-concealed: on one hand, public APIs and comments acknowledge them; on the other hand, they are waved away as internal details. Yet this is the root of many bizarre bugs. I hope everyone can see through the painted skin and master dependency properties from the inside out.

Having covered the capabilities of dependency properties, let's turn back to how to register them, and to the concept of Attached properties that WPF introduces.

Attached Properties

In the earliest example we registered the DP with DependencyProperty.Register. The DP API also exposes DependencyProperty.RegisterAttached for registration. Its parameters are identical to Register—so where does the "Attached" concept come from?

In fact, whenever we use dependency properties we have been attaching all along. We register (construct) a DP, then operate on it from a DependencyObject through GetValue and SetValue—effectively pasting the DP onto the DependencyObject this way, only achieved by wrapping a CLR property. So what about RegisterAttached? Here is the simplest application:

public class AttachedHelper
{
      public static readonly DependencyProperty IsAttachedProperty =
          DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedHelper),
              new FrameworkPropertyMetadata((bool)false));

      public static bool GetIsAttached(DependencyObject d)
      {
              return (bool)d.GetValue(IsAttachedProperty);
      }

      public static void SetIsAttached(DependencyObject d, bool value)
      {
              d.SetValue(IsAttachedProperty, value);
      }
}
Enter fullscreen mode Exit fullscreen mode

Used in XAML:

<local:SimpleDO x:Name="sDo" local:AttachedHelper.IsAttached="True"/>
Enter fullscreen mode Exit fullscreen mode

In this AttachedHelper, the value is not wrapped in a CLR property named IsAttached; instead, SetIsAttached and GetIsAttached access it—though internally they still call SetValue and GetValue. The XAML parser supports this: local:AttachedHelper.IsAttached="True" ultimately calls SetIsAttached.

Register and RegisterAttached differ only in wrapping; internally both use GetValue and SetValue. The "attachment" capability arises from the separated design of DependencyObject and DependencyProperty—not from any great magic in the RegisterAttached function itself. Because it uses static methods, RegisterAttached is more dynamic: we can call SetIsAttached on a live DependencyObject at runtime and stuff the property into that DependencyObject's internal EffectiveValueEntry.

Because of this dynamic attachment capability, dependency properties registered with RegisterAttached are also called attached properties. Attached properties in turn gave rise to many elegant designs, which we'll skip here. Let's look at what to watch out for when using dependency properties.

Cautions When Using Dependency Properties

"Dependency property"—by name it should still be a property. Do the usual CLR property techniques (polymorphism, raising events in the setter, and so on) still apply?

The Jianghu is a dangerous place—be careful.

Take the earlier SimpleDO's ValueProperty. If you call sDo.Value = 2 in code-behind, Value's setter is invoked. But if you assign it in XAML, or bind it to another property, the program still runs and Value looks correct—yet Value's setter is never called. What's going on?

WPF optimizes dependency properties: in binding and similar scenarios it does not call the property's get/set methods, but directly reads or writes the dependency property's EffectiveValue. In other words, for performance it bypasses your wrappers and works inside the DP directly. Naturally, when the rules aren't followed, any polymorphism implemented in the traditional setter style simply fails to run.

WPF's recommended approach for polymorphism is to call a virtual method from PropertyChangedCallback, like so:

private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
     ((SimpleDO)d).OnValueChanged(e);
}

protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e)
{
}
Enter fullscreen mode Exit fullscreen mode

Subclasses achieve polymorphism by overriding OnValueChanged.

Of course, there are many more pitfalls when using dependency properties in practice; I won't enumerate them all. Next, let's look at how dependency properties help us design programs.

New Ways of Thinking with Dependency Properties

Dependency properties offer powerful capabilities: attached properties, property change notification, inheritance, multiple property values, and more. The marketing material is everywhere. Described in the language of martial arts manuals, it goes something like this:

A new martial art has appeared in the Jianghu. It synthesizes the old schools; practiced to the highest level it is uncanny and unpredictable—even an embroidery needle can serve as a weapon, killing without a trace. After a long quest you finally seize the manual. Before you even open it, a chill runs down your spine. Four large characters glare up at you: "Sunflower Manual"…

Indeed, not every object can use dependency properties—only objects that inherit from DependencyObject can. DependencyObject has this ability because it holds an array of EffectiveValueEntry internally, something like a bottomless satchel. This is also a point often forgotten in design.

For example, suppose there are two Panels, and one of them contains many nested Panels and controls. We want to filter by some condition, move the matching controls to the other Panel, and—when a Restore button is clicked—return those controls to their original places. We can define an attached property, say OldParent, walk the logical tree of the first Panel, remove matching controls from their parent, and record their original parent in the attached property OldParent. When restoring, we read OldParent and Add them back.

Applications of this kind simply define an attached property and attach it to objects; the usage is straightforward. The point is only that DependencyObject has this store-and-access capability—don't waste it when designing programs.

Summary

There is a great deal that can be written about dependency properties; every point expanded would yield its own story. Things like RegisterReadOnly and AddOwner have not been detailed here. The separation of a property from the object that uses it is a hallmark of dependency properties; the binding and interaction between the two is where the difficulty lies. I hope everyone can look at dependency properties from the inside out and put them to better use.

Top comments (0)