Skip to content

UPF Dependency Properties

Cole Campbell edited this page Mar 3, 2018 · 2 revisions

Dependency properties are a concept in the Ultraviolet Presentation Foundation which supplement C#'s built-in properties. A dependency property's value depends (hence its name) on various external factors, rather than being provided by a backing field on the type that defines the property.

Declaring a Dependency Property

To create a new dependency property, it must be registered with the dependency property system using the static Register() method on the DependencyProperty class. This method returns a new DependencyProperty instance which serves to uniquely identify the dependency property.

public class MyElement : FrameworkElement
{
    public static readonly DependencyProperty FooProperty = DependencyProperty.Register("Foo", typeof(Int32), typeof(MyElement),
        new PropertyMetadata<Int32>(123, PropertyMetadataOptions.AffectsMeasure, HandleFooChanged));
}

There's a lot to unpack here:

  1. The first argument is the name of the dependency property. In C# 6 and beyond, it is probably a good idea to use nameof(Foo) here, where Foo is a standard property which exposes the dependency property (see below).
  2. The typeof(Int32) argument specifies the dependency property's type.
  3. The typeof(MyElement) argument specifies the dependency property's owner type. This is the type which is declaring the property, in this case a custom element type called MyElement.
  4. The PropertyMetadata<T> object is used to specify the dependency property metadata, which consists of the following elements:
    1. The property's default value, in this case 123; if set to null, then default(T) will be used, where T is the dependency property's type.
    2. The property's metadata options. In this case, we're specifying that changing the value of Foo on an element will invalidate that element's measurement state.
    3. The property's change handler, which is called when the property value is changed for a particular element.
    4. The property's value coercion callback, not shown in this example. This is a delegate which can be used to coerce the dependency property's value into a particular state; for example, the RangeBase element uses this technique to force its Value property to remain within the range specified by its Minimum and Maximum properties.

Once you have an instance of the DependencyProperty class, you can use that identifier to expose the dependency property on a particular object by declaring a traditional property:

public Int32 Foo
{
    get { return GetValue<Int32>(FooProperty); }
    set { SetValue<Int32>(FooProperty, value); }
}

Dependency properties can only be set on dependency objects; these are anything derived from the DependencyObject abstract class, which includes all of UPF's interface elements. This class defines the API for interacting with an object's dependency property values, including the GetValue<T>() and SetValue<T>() methods.

Dependency properties can be referenced in UVML using the name specified in the call to Register().

<MyElement Foo="999"/>

When styling a dependency property via UVSS, it is important to note that it must be referenced by its styling name. Unless a styling name is explicitly defined through the call to Register(), it is automatically generated from the property's real name; a property named "FooBarBaz" will have an automatically generated styling name of foo-bar-baz.

Adding Owners

A dependency property can have multiple owner types. To add an owner type to an existing dependency property, use the AddOwner() method:

public class AnotherElement : FrameworkElement
{
    public static readonly DependencyProperty FooProperty = MyElement.FooProperty.AddOwner(typeof(AnotherElement),
        new PropertyMetadata<Int32>(456, PropertyMetadataOptions.AffectsArrage, HandleFooChanged));
}

This will add the AnotherElement type to the list of owner types for FooProperty.

Each owner type can specify its own property metadata; here, we've overridden the default value and specified a new change handler. New property metadata is merged with inherited metadata where it makes sense to do so, and otherwise overridden. Note that metadata is "inherited" first from any types which are ancestors of the new owner type, and finally from the dependency property's "true owner," i.e. the type which originally declared it.

  • If a default value is specified, it overrides any existing default value; otherwise it is inherited.
  • The property's options are merged with inherited options.
  • The property's change handler, if specified, is combined with inherited handlers.
  • The property's coercion callback overrides any existing coercion callbacks.

Attached Properties

An attached property is a special type of dependency property which can be set on a type other than one of its owner types. As an example, the Grid layout container defines Row and Column properties which are intended to be set not on the grid itself, but on the grid's children, which can be any arbitrary type of interface element.

To define an attached property, use the RegisterAttached() method on DependencyProperty.

public class MyElement : FrameworkElement
{
    public static readonly DependencyProperty BarProperty = DependencyProperty.RegisterAttached("Bar", typeof(Int32), typeof(MyElement),
        new PropertyMetadata<Int32>(789, PropertyMetadata.AffectsMeasure));
}

The parameters of RegisterAttached() are identical to those used by Register().

Attached properties are usually exposed through static methods on the declaring type, rather than properties on the target type. These methods take the target object as their first parameters.

public class MyElement : FrameworkElement
{
    public static void SetBar(DependencyObject element, Int32 value)
    {
        element.SetValue<Int32>(BarProperty, value);
    }

    public static Int32 GetBar(DependencyObject element)
    {
        return element.GetValue<Int32>(BarProperty);
    }
}

In UVML, an attached property is referenced by prepending the name of the owner type to the property name.

<Button MyElement.Bar="999"/>

This works similarly in UVSS, except that the property's styling name should be used instead:

button
{
    myelement.bar { 999 }
}

Read-only Properties

A dependency property can be declared read-only using the RegisterReadOnly() method. This is useful for creating dependency properties which should not be modifiable by the "outside world," such as those which directly represent the internal state of an element.

public class MyElement : FrameworkElement
{
    private static readonly DependencyPropertyKey QuxPropertyKey = DependencyProperty.RegisterReadOnly("Qux", typeof(Int32), typeof(MyElement),
        new PropertyMetadata<Int32>(0));

    public static readonly DependencyProperty QuxProperty = QuxPropertyKey.DependencyProperty;

    public static Int32 Qux
    {
        get { return GetValue<Int32>(QuxProperty); }
        private set { SetValue<Int32>(QuxPropertyKey, value); }
    }
}

Note that this returns an instance of the DependencyPropertyKey class, rather than the DependencyProperty class, and that this object is declared private. The declaring class can still use this key value to set the dependency property value using SetValue<T>(), but attempting to set the value in any other way—such as through UVML or UVSS—will result in an exception being thrown.

Value Precedence

As noted in the introduction, dependency properties can draw their values from a number of different sources. When multiple value sources are applicable, they have the following priority, from highest to lowest:

  1. Animations
  2. Data bindings
  3. Local values (i.e. those set directly using the SetValue<T>() method)
  4. Triggers
  5. Styles
  6. Default values

Digest Cycle

The Ultraviolet Presentation Foundation uses a digest cycle to track changes to dependency properties which are animated or data bound. Every frame, each such dependency property is digested—its current value is compared to its value in the previous frame, and if that value has changed, the appropriate change handlers are invoked.

As an optimization, dependency properties which have a simple binding to another dependency property do not participate in the digest cycle. A simple binding is defined as any binding which directly references a dependency property without needing to dereference any other objects. Thus, the binding expression {{Foo}} is a simple binding, but {{A.B.C.Foo}} is not.

Clone this wiki locally