Dependency Properties

We've already seen some examples of Dependency Properties in previous chapters, but now let's take a more thorough look. We have a large number of options that we can use when declaring these properties, with some more commonly used than others. Let's investigate the standard declaration first, by defining an Hours property of type int in a class named DurationPicker:

public static readonly DependencyProperty HoursProperty =
  DependencyProperty.Register(nameof(Hours), typeof(int), 
  typeof(DurationPicker)); 
 
public int Hours 
{  
  get { return (int)GetValue(HoursProperty); } 
  set { SetValue(HoursProperty, value); }  
} 

As with all Dependency Properties, we start by declaring the property as static and readonly because, we only want a single, immutable instance of it. This also enables us to access it without an instance of our class.

Unlike normal CLR properties, we do not store the values of our Dependency Properties in private fields that back the properties. Instead, default values are stored directly within the metadata of each DependencyProperty object and altered values are stored in a separate array in the DependencyObject instance that the Dependency Property value was set on.

Let's clarify this a little further and remember that all of the built-in controls extend the DependencyObject class. This means that the altered values of the TextProperty Dependency Property for example, which was declared in the TextBox class, are stored in the actual TextBox instance that the property value was changed on. This is the main reason why  bindings can only be set on a Dependency Property of a Dependency Object.

An array of values exists in each DependencyObject instance and contains the values of all of its declared Dependency Properties that have been explicitly set on it. This is a very important point. This means that by default, with no changed values, the array is empty and therefore, the memory footprint is very small.

This is converse to a CLR class, where each property has a memory footprint, whether it is set or not. The result of this arrangement is that it saves a huge amount of memory, because only Dependency Property values that have been explicitly set will be stored in the array of values, while default values are read directly from the Dependency Property objects instead.

The fact that this array of changed values exists in the DependencyObject class explains why we need to call its GetValue and SetValue methods to access and set the values of our Dependency Properties. Our HoursProperty here is merely the identifier, known as the Dependency Property Identifier, whose GlobalIndex property value is used to access the relevant value from that array.

Note that the values in this array are of type object, so that it can work with any object type. This explains why we need to cast the return value of the GetValue method from object to the appropriate type in the getter of our CLR wrapper property. Let's now examine what happens internally when we declare a Dependency Property.

In the DependencyProperty class, there is a private static Hashtable named PropertyFromName, which holds references to every registered Dependency Property in the application and is shared among all instances of the class. To declare each property and create our key to the Hashtable, we use the Register method of the DependencyProperty class.

This method has a number of overloads, but all of them require the following information; the name and type of the property and the type of the declaring class, or owner type as Microsoft prefer to call it. Let's look into this process in a bit more depth.

When we register a Dependency Property using one of the Register methods, the provided metadata is first validated and replaced with default values, if required. Then a private RegisterCommon method is called and inside it, a class named FromNameKey is used to generate the unique key from the name and owner type of the Dependency Property to create. It does this by creating a unique hash code, by combining the results from calling the object.GetHashCode method on both the name and owner type passed to it.

After the FromNameKey object has been created, the PropertyFromName collection is checked for this key and an ArgumentException is thrown if one already exists within it. If it is unique, then the default metadata and default value are validated and set from input parameters, or automatically generated if missing.

After this step, the actual DependencyProperty instance is created using the new keyword and a private constructor. This internal instance is then added to the PropertyFromName Hashtable, using the FromNameKey object as the unique key, and then returned to the caller of the Register method, to be stored locally in the public static readonly Dependency Property Identifier.

Note that the overloaded Register methods both have an additional input parameter of type PropertyMetadata and we'll investigate this in the next section. For now, let's focus on the last overload, which also enables us to attach a ValidateValueCallback handler to our property.

As the name suggests, this is solely used for validation purposes and we cannot alter the data bound value in this method. Instead, we are simply required to return true or false to specify the validity of the current value. Let's see how we can attach this handler to our property and what its method signature is:

public static readonly DependencyProperty HoursProperty =
  DependencyProperty.Register(nameof(Hours), typeof(int), 
  typeof(DurationPicker), new PropertyMetadata(12), ValidateHours));
 
private static bool ValidateHours(object value) 
{ 
  int intValue = (int)value; 
  return intValue > 0 && intValue < 25; 
} 

Note that the ValidateValueCallback delegate does not provide us with any reference to our class and so, we cannot access its other properties from this static context. In order to compare the current value with other property values, or to ensure that certain conditions are met, we can use another overload of the PropertyMetadata input parameter of the DependencyProperty.Register method and we'll see this shortly. But let's now return to focus on the PropertyMetadata input parameter.