DEV Community

yohan
yohan

Posted on Edited on Originally published at cnblogs.com

Deep Dive into WPF — Dependency Property (Part 1)

Windows Presentation Foundation (WPF) provides a set of services that can be used to extend the functionality of common language runtime (CLR) properties; these services are collectively known as the WPF property system. Properties supported by the WPF property system are called dependency properties.

That is MSDN's description of the dependency property (DependencyProperty). It mainly covers two points: WPF provides services that extend CLR properties, and properties supported by that service are called dependency properties.

The description alone leaves you foggy. To understand a concept, you first need to know the background that produced it and why it must exist. So what problem does introducing the dependency property solve in WPF?

Starting from Properties

Properties are familiar territory. They encapsulate a class's fields, represent the class's state, compile down to get_ / set_ methods, and can be used by classes, structs, and so on. A common property looks like this:

 1: public class NormalObject
 2: {
 3:     private string _unUsedField;
 4:
 5:     private string _name;
 6:     public string Name
 7:     {
 8:         get
 9:         {
10:             return _name;
11:         }
12:         set
13:         {
14:             _name = value;
15:         }
16:     }
17: }
Enter fullscreen mode Exit fullscreen mode

In the object-oriented world, properties are everywhere. A Button, for instance, defines roughly 70–80 properties to describe its state. So where do properties fall short?

Of course, "shortfalls" are relative to a concrete context. Take Button. Its inheritance chain is:

Button → ButtonBase → ContentControl → Control → FrameworkElement → UIElement → Visual → DependencyObject → …

Every inheritance level carries down the parent class's private fields. That inheritance is meaningful, of course—but for Button, most properties are never modified and still hold the defaults defined by their parent classes. Typically, over an entire Button object's lifetime only a small subset of properties is ever changed; most keep their initial values forever. Every field needs to occupy some amount of memory—several bytes or more depending on the field. Here is where we hope to optimize:

  • Object bloat from inheritance. Every inheritance level inherits the parent's fields, so objects low in the inheritance tree inevitably grow large.
  • Most fields are never modified and keep their construction-time defaults. Can those fields be stripped out of the object to reduce its size?

A Prototype of the Dependency Property

Given the requirements above, the dependency property emerged. A simple prototype looks like this:

DependencyProperty:

 1: public class DependencyProperty
 2: {
 3:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
 4:     internal string Name;
 5:     internal object Value;
 6:     internal object HashCode;
 7:
 8:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
 9:     {
10:         this.Name = name;
11:         this.Value = defaultValue;
12:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
13:     }
14:
15:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
16:     {
17:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
18:         RegisteredDps.Add(dp.HashCode, dp);
19:         return dp;
20:     }
21: }
22:
Enter fullscreen mode Exit fullscreen mode

DependencyObject:

 1: public class DependencyObject
 2: {
 3:     private string _unUsedField;
 4:
 5:     public static readonly DependencyProperty NameProperty =
 6:         DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
 7:
 8:     public object GetValue(DependencyProperty dp)
 9:     {
10:         return DependencyProperty.RegisteredDps[dp.HashCode].Value;
11:     }
12:
13:     public void SetValue(DependencyProperty dp, object value)
14:     {
15:         DependencyProperty.RegisteredDps[dp.HashCode].Value = value;
16:     }
17:
18:     public string Name
19:     {
20:         get
21:         {
22:             return (string)GetValue(NameProperty);
23:         }
24:         set
25:         {
26:             SetValue(NameProperty, value);
27:         }
28:     }
29: }
30:
Enter fullscreen mode Exit fullscreen mode

Here we first define the dependency property DependencyProperty, which stores the fields we wanted to extract earlier. The DP maintains a global map that holds all registered DPs, and exposes a Register method for registering new ones. To keep the map keys unique, registration XORs the hash codes of the supplied name and the owner type to form the key. The most important parameter is the last one, which sets the DP's default value.

Next we define DependencyObject to use the DP. We register a new DP (NameProperty) via DependencyProperty.Register, then provide GetValue and SetValue to operate on it. Finally, like NormalObject earlier, we define a Name property. The difference is that the actual value is not stored in a field on DependencyObject; it lives in the NameProperty DP, and gets/set operations go through GetValue and SetValue.

As an example, many details are ignored for brevity. Let's test whether it solves the problem.

Create one NormalObject and one DependencyObject, open SOS in VS, and inspect:

.load sos
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
!DumpHeap -stat -type NormalObject
total 1 objects
Statistics:
MT    Count    TotalSize Class Name
009e30d0        1           16 DPDemonstration.NormalObject
Total 1 objects
!DumpHeap -stat -type DependencyObject
total 1 objects
Statistics:
MT    Count    TotalSize Class Name
009e31a0        1           12 DPDemonstration.DependencyObject
Total 1 objects
Enter fullscreen mode Exit fullscreen mode

Each object here was given an _unUsedField. The .NET GC requires a minimum object size of 12 bytes; smaller objects are automatically padded up to that. A plain object occupies 8 bytes—Syncblk (4 bytes) plus TypeHandle (4 bytes)—and for the demo we added an _unUsedField (4 bytes) to reach the minimum.

Here, DependencyObject saves 4 bytes of storage for _name compared with NormalObject.

Taking It Further

This is only the first step of a long march. The idea solves the problem we wanted, but the implementation is still unacceptable. In this version all DependencyObject instances share one DP—understandable—but after you modify one object's property, every object's property is effectively modified too. That's absurd.

So once an object's property is modified, the value must be maintained on the object itself. Let's revise DependencyObject and introduce an Effective concept.

The improved DependencyObject adds _effectiveValues:

 1: public class DependencyObject
 2: {
 3:     private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
 4:
 5:     public static readonly DependencyProperty NameProperty =
 6:         DependencyProperty.Register("Name", typeof(string), typeof(DependencyObject), string.Empty);
 7:
 8:     public object GetValue(DependencyProperty dp)
 9:     {
10:         EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
11:         if (effectiveValue.PropertyIndex != 0)
12:         {
13:             return effectiveValue.Value;
14:         }
15:         else
16:         {
17:             return DependencyProperty.RegisteredDps[dp.HashCode].Value;
18:         }
19:     }
20:
21:     public void SetValue(DependencyProperty dp, object value)
22:     {
23:         EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
24:         if (effectiveValue.PropertyIndex != 0)
25:         {
26:             effectiveValue.Value = value;
27:         }
28:         else
29:         {
30:             effectiveValue = new EffectiveValueEntry() { PropertyIndex = dp.Index, Value = value };
31:             _effectiveValues.Add(effectiveValue);
32:         }
33:     }
34:
35:     public string Name
36:     {
37:         get
38:         {
39:             return (string)GetValue(NameProperty);
40:         }
41:         set
42:         {
43:             SetValue(NameProperty, value);
44:         }
45:     }
46: }
Enter fullscreen mode Exit fullscreen mode

The newly introduced EffectiveValueEntry:

 1: internal struct EffectiveValueEntry
 2: {
 3:     internal int PropertyIndex { get; set; }
 4:
 5:     internal object Value { get; set; }
 6: }
Enter fullscreen mode Exit fullscreen mode

The improved DependencyProperty adds PropertyIndex:

 1: public class DependencyProperty
 2: {
 3:     private static int globalIndex = 0;
 4:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
 5:     internal string Name;
 6:     internal object Value;
 7:     internal int Index;
 8:     internal object HashCode;
 9:
10:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
11:     {
12:         this.Name = name;
13:         this.Value = defaultValue;
14:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
15:     }
16:
17:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
18:     {
19:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
20:         globalIndex++;
21:         dp.Index = globalIndex;
22:         RegisteredDps.Add(dp.HashCode, dp);
23:         return dp;
24:     }
25: }
Enter fullscreen mode Exit fullscreen mode

Adding _effectiveValues to DependencyObject means every modified DP is saved in an EffectiveValueEntry. This way we only store modified properties; unmodified ones still read the DP's default value, optimizing property storage.

Further Development

So far, the transformation from plain properties to dependency properties has gone smoothly. But real-world use exposed another problem. With inheritance, subclasses can override the parent's fields—in other words, the default value should be overridable per subclass. How do we handle that? Should a subclass re-register a new DP with a new default value?

That would be an ugly implementation. To support different default values for the same DP, the DP must internally maintain a list keyed by different DependencyObject types, so the correct default can be looked up from the type of the incoming DependencyObject.

The DP needs a self-describing list. Following Microsoft's naming conventions, we add a new type-level property metadata (PropertyMetadata):

 1: public class PropertyMetadata
 2: {
 3:     public Type Type { get; set; }
 4:     public object Value { get; set; }
 5:
 6:     public PropertyMetadata(object defaultValue)
 7:     {
 8:         this.Value = defaultValue;
 9:     }
10: }
Enter fullscreen mode Exit fullscreen mode

Corresponding changes to DependencyProperty:

 1: public class DependencyProperty
 2: {
 3:     private static int globalIndex = 0;
 4:     internal static Dictionary<object, DependencyProperty> RegisteredDps = new Dictionary<object, DependencyProperty>();
 5:     internal string Name;
 6:     internal object Value;
 7:     internal int Index;
 8:     internal object HashCode;
 9:     private List<PropertyMetadata> _metadataMap = new List<PropertyMetadata>();
10:     private PropertyMetadata _defaultMetadata;
11:
12:     private DependencyProperty(string name, Type propertyName, Type ownerType, object defaultValue)
13:     {
14:         this.Name = name;
15:         this.Value = defaultValue;
16:         this.HashCode = name.GetHashCode() ^ ownerType.GetHashCode();
17:
18:         PropertyMetadata metadata = new PropertyMetadata(defaultValue) { Type = ownerType };
19:         _metadataMap.Add(metadata);
20:         _defaultMetadata = metadata;
21:     }
22:
23:     public static DependencyProperty Register(string name, Type propertyType, Type ownerType, object defaultValue)
24:     {
25:         DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultValue);
26:         globalIndex++;
27:         dp.Index = globalIndex;
28:         RegisteredDps.Add(dp.HashCode, dp);
29:         return dp;
30:     }
31:
32:     public void OverrideMetadata(Type forType, PropertyMetadata metadata)
33:     {
34:         metadata.Type = forType;
35:         _metadataMap.Add(metadata);
36:     }
37:
38:     public PropertyMetadata GetMetadata(Type type)
39:     {
40:         PropertyMetadata medatata = _metadataMap.FirstOrDefault((i) => i.Type == type) ??
41:             _metadataMap.FirstOrDefault((i) => type.IsSubclassOf(i.Type));
42:         if (medatata == null)
43:         {
44:             medatata = _defaultMetadata;
45:         }
46:         return medatata;
47:     }
48: }
Enter fullscreen mode Exit fullscreen mode

Modify GetValue in DependencyObject and change _effectiveValues. For brevity, NameProperty and SetValue are omitted:

 1: public class DependencyObject
 2: {
 3:    private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>();
 4:
 5:    public object GetValue(DependencyProperty dp)
 6:    {
 7:        EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index);
 8:        if (effectiveValue.PropertyIndex != 0)
 9:        {
10:            return effectiveValue.Value;
11:        }
12:        else
13:        {
14:            PropertyMetadata metadata;
15:            metadata = DependencyProperty.RegisteredDps[dp.HashCode].GetMetadata(this.GetType());
16:            return metadata.Value;
17:        }
18:    }
19: }
Enter fullscreen mode Exit fullscreen mode

Now we can define a SubDependencyObject and call OverrideMetadata to add new metadata to the DP's _metadataMap:

 1: public class SubDependencyObject : DependencyObject
 2: {
 3:     static SubDependencyObject()
 4:     {
 5:         NameProperty.OverrideMetadata(typeof(SubDependencyObject), new PropertyMetadata("SubName"));
 6:     }
 7: }
Enter fullscreen mode Exit fullscreen mode

Create a DependencyObject and a SubDependencyObject, and you'll find that Name's value has been changed to "SubName". Of course, the real DP's metadata operations are more involved: when a subclass calls OverrideMetadata, a merge occurs that combines the new metadata with the parent's. And GetMetadata must return the metadata for the type itself or its nearest ancestor. To find the nearest ancestor, WPF introduces a DependencyObjectType class, constructed with BaseType = this.base.GetType(); we'll skip that for simplicity here.

How WPF Extends the Dependency Property

In the preceding examples we built a DependencyProperty around the idea of optimizing storage. Having forged such a fine weapon, it would be a shame not to polish it—and WPF extended the DP on this foundation to make it even more powerful.

For ordinary CLR properties, adding logic in the setter is perfectly normal; you can also raise events or change other properties from the setter. What support does a dependency property offer for that?

Naturally, WPF added PropertyChangedCallback and CoerceValueCallback to the DP's PropertyMetadata. These delegates can be passed when constructing the PropertyMetadata; during SetValue, the corresponding PropertyMetadata is retrieved and PropertyChangedCallback is invoked. The PropertyMetadata can be supplied when the DP is built, or when a subclass calls OverrideMetadata—so the same DP can behave differently for different DependencyObjects. WPF extended this in many ways and defined a full set of property assignment rules, including calculation, Coerce (limitation), Validate, and more.

Of course, once you open this up there is a great deal to say, and WPF designed it with care. This is what we mentioned at the opening: WPF provides a set of services used to extend CLR properties.

Multiple Property Values

Development is driven by needs, and the WPF implementation produced yet another requirement:

WPF has native animation support. For a DP property—Button's Width, say—you can animate it from 100 to 200 over one second, and when the animation ends you want the property to return to its original value. Likewise, you can assign a property in a XAML expression, and when the expression becomes invalid you again expect the original value to be restored. This need arises because assignments to the same property can happen in different contexts, and the property must change as the object's state changes. Two requirements follow:

  • The property exposes a single value externally, but can hold multiple values internally, selecting the current one based on state (condition) changes.
  • Those states (conditions) need a defined priority, so the system can decide which value to use based on priority.

Multiple values for one property is a tall order for a CLR property. For a DP it is simple: the DP's value already lives in our EffectiveValueEntry. Previously it held one Value; now we just define multiple values.

 1: internal struct EffectiveValueEntry
 2: {
 3:     private object _value;
 4:
 5:     internal int PropertyIndex { get; set; }
 6:
 7:     internal object Value
 8:     {
 9:         get
10:         {
11:             return _value;
12:         }
13:         set
14:         {
15:             _value = value;
16:         }
17:     }
18:
19:     internal ModifiedValue ModifiedValue
20:     {
21:         get
22:         {
23:             if (this._value != null)
24:             {
25:                 return (this._value as ModifiedValue);
26:             }
27:             return null;
28:         }
29:     }
30: }
Enter fullscreen mode Exit fullscreen mode

The corresponding ModifiedValue:

 1: internal class ModifiedValue
 2: {
 3:     internal object AnimatedValue { get; set; }
 4:     internal object BaseValue { get; set; }
 5:     internal object CoercedValue { get; set; }
 6:     internal object ExpressionValue { get; set; }
 7: }
Enter fullscreen mode Exit fullscreen mode

When a property has never been modified, ModifiedValue is null; once modified, ModifiedValue is populated. EffectiveValueEntry defines many methods such as SetExpressionValue(object value) and SetAnimatedValue(object value) to write the corresponding values into ModifiedValue; it also provides properties like IsAnimated and IsExpression to indicate the current state. The assignment operation is relatively complex. Priorities fall into two major categories: first, the priorities among the properties of ModifiedValue; second, for ExpressionValue, its own priority ordering—Local > Style > Template, and so on. I won't go into the details here.

Advantages of Dependency Properties

Looking back, let's summarize the advantages of dependency properties:

  • Optimizes property storage and reduces unnecessary memory usage.
  • Adds property change notification, limitation (coercion), validation, and more.
  • Can store multiple values; combined with Expression and Animation, it enables more flexible usage patterns.

Summary

With dependency properties, WPF provides a powerful property system that supports data binding, styles, animation, attached properties, and more. This article briefly implemented the evolution from plain properties to dependency properties. The result still differs from WPF's real implementation in places—I hope everyone grasps the main thread and puts it to better use.

Beyond the implementation of dependency properties there are other important parts: attached properties born out of dependency properties, how to use dependency properties to design and implement programs better, and what to watch out for when using them. Well then—next time.

Download the sample program

Top comments (0)