Szerkesztés

Megosztás a következőn keresztül:


Custom Dependency Properties

This topic describes the reasons that Windows Presentation Foundation (WPF) application developers and component authors might want to create custom dependency property, and describes the implementation steps as well as some implementation options that can improve performance, usability, or versatility of the property.

Prerequisites

This topic assumes that you understand dependency properties from the perspective of a consumer of existing dependency properties on WPF classes, and have read the Dependency Properties Overview topic. In order to follow the examples in this topic, you should also understand XAML and know how to write WPF applications.

What Is a Dependency Property?

You can enable what would otherwise be a common language runtime (CLR) property to support styling, data binding, inheritance, animations, and default values by implementing it as a dependency property. Dependency properties are properties that are registered with the WPF property system by calling the Register method (or RegisterReadOnly), and that are backed by a DependencyProperty identifier field. Dependency properties can be used only by DependencyObject types, but DependencyObject is quite high in the WPF class hierarchy, so the majority of classes available in WPF can support dependency properties. For more information about dependency properties and some of the terminology and conventions used for describing them in this SDK, see Dependency Properties Overview.

Examples of Dependency Properties

Examples of dependency properties that are implemented on WPF classes include the Background property, the Width property, and the Text property, among many others. Each dependency property exposed by a class has a corresponding public static field of type DependencyProperty exposed on that same class. This is the identifier for the dependency property. The identifier is named using a convention: the name of the dependency property with the string Property appended to it. For example, the corresponding DependencyProperty identifier field for the Background property is BackgroundProperty. The identifier stores the information about the dependency property as it was registered, and the identifier is then used later for other operations involving the dependency property, such as calling SetValue.

As mentioned in the Dependency Properties Overview, all dependency properties in WPF (except most attached properties) are also CLR properties because of the "wrapper" implementation. Therefore, from code, you can get or set dependency properties by calling CLR accessors that define the wrappers in the same manner that you would use other CLR properties. As a consumer of established dependency properties, you do not typically use the DependencyObject methods GetValue and SetValue, which are the connection point to the underlying property system. Rather, the existing implementation of the CLR properties will have already called GetValue and SetValue within the get and set wrapper implementations of the property, using the identifier field appropriately. If you are implementing a custom dependency property yourself, then you will be defining the wrapper in a similar way.

When Should You Implement a Dependency Property?

When you implement a property on a class, so long as your class derives from DependencyObject, you have the option to back your property with a DependencyProperty identifier and thus to make it a dependency property. Having your property be a dependency property is not always necessary or appropriate, and will depend on your scenario needs. Sometimes, the typical technique of backing your property with a private field is adequate. However, you should implement your property as a dependency property whenever you want your property to support one or more of the following WPF capabilities:

  • You want your property to be settable in a style. For more information, see Styling and Templating.

  • You want your property to support data binding. For more information about data binding dependency properties, see Bind the Properties of Two Controls.

  • You want your property to be settable with a dynamic resource reference. For more information, see XAML Resources.

  • You want to inherit a property value automatically from a parent element in the element tree. In this case, register with the RegisterAttached method, even if you also create a property wrapper for CLR access. For more information, see Property Value Inheritance.

  • You want your property to be animatable. For more information, see Animation Overview.

  • You want the property system to report when the previous value of the property has been changed by actions taken by the property system, the environment, or the user, or by reading and using styles. By using property metadata, your property can specify a callback method that will be invoked each time the property system determines that your property value was definitively changed. A related concept is property value coercion. For more information, see Dependency Property Callbacks and Validation.

  • You want to use established metadata conventions that are also used by WPF processes, such as reporting whether changing a property value should require the layout system to recompose the visuals for an element. Or you want to be able to use metadata overrides so that derived classes can change metadata-based characteristics such as the default value.

  • You want properties of a custom control to receive Visual Studio WPF Designer support, such as Properties window editing. For more information, see Control Authoring Overview.

When you examine these scenarios, you should also consider whether you can achieve your scenario by overriding the metadata of an existing dependency property, rather than implementing a completely new property. Whether a metadata override is practical depends on your scenario and how closely that scenario resembles the implementation in existing WPF dependency properties and classes. For more information about overriding metadata on existing properties, see Dependency Property Metadata.

Checklist for Defining a Dependency Property

Defining a dependency property consists of four distinct concepts. These concepts are not necessarily strict procedural steps, because some of these end up being combined as single lines of code in the implementation:

  • (Optional) Create property metadata for the dependency property.

  • Register the property name with the property system, specifying an owner type and the type of the property value. Also specify the property metadata, if used.

  • Define a DependencyProperty identifier as a public static readonly field on the owner type.

  • Define a CLR "wrapper" property whose name matches the name of the dependency property. Implement the CLR "wrapper" property's get and set accessors to connect with the dependency property that backs it.

Registering the Property with the Property System

In order for your property to be a dependency property, you must register that property into a table maintained by the property system, and give it a unique identifier that is used as the qualifier for later property system operations. These operations might be internal operations, or your own code calling property system APIs. To register the property, you call the Register method within the body of your class (inside the class, but outside of any member definitions). The identifier field is also provided by the Register method call, as the return value. The reason that the Register call is done outside of other member definitions is because you use this return value to assign and create a public static readonly field of type DependencyProperty as part of your class. This field becomes the identifier for your dependency property.

public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))

Dependency Property Name Conventions

There are established naming conventions regarding dependency properties that you must follow in all but exceptional circumstances.

The dependency property itself will have a basic name, "AquariumGraphic" as in this example, which is given as the first parameter of Register. That name must be unique within each registering type. Dependency properties inherited through base types are considered to be already part of the registering type; names of inherited properties cannot be registered again. However, there is a technique for adding a class as owner of a dependency property even when that dependency property is not inherited; for details, see Dependency Property Metadata.

When you create the identifier field, name this field by the name of the property as you registered it, plus the suffix Property. This field is your identifier for the dependency property, and it will be used later as an input for the SetValue and GetValue calls you will make in the wrappers, by any other code access to the property by your own code, by any external code access you allow, by the property system, and potentially by XAML processors.

Note

Defining the dependency property in the class body is the typical implementation, but it is also possible to define a dependency property in the class static constructor. This approach might make sense if you need more than one line of code to initialize the dependency property.

Implementing the "Wrapper"

Your wrapper implementation should call GetValue in the get implementation, and SetValue in the set implementation (the original registration call and field are shown here too for clarity).

In all but exceptional circumstances, your wrapper implementations should perform only the GetValue and SetValue actions, respectively. The reason for this is discussed in the topic XAML Loading and Dependency Properties.

All existing public dependency properties that are provided on the WPF classes use this simple wrapper implementation model; most of the complexity of how dependency properties work is either inherently a behavior of the property system, or is implemented through other concepts such as coercion or property change callbacks through property metadata.


public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
public Uri AquariumGraphic
{
  get { return (Uri)GetValue(AquariumGraphicProperty); }
  set { SetValue(AquariumGraphicProperty, value); }
}

Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))
Public Property AquariumGraphic() As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set(ByVal value As Uri)
        SetValue(AquariumGraphicProperty, value)
    End Set
End Property

Again, by convention, the name of the wrapper property must be the same as the name chosen and given as first parameter of the Register call that registered the property. If your property does not follow the convention, this does not necessarily disable all possible uses, but you will encounter several notable issues:

  • Certain aspects of styles and templates will not work.

  • Most tools and designers must rely on the naming conventions to properly serialize XAML, or to provide designer environment assistance at a per-property level.

  • The current implementation of the WPF XAML loader bypasses the wrappers entirely, and relies on the naming convention when processing attribute values. For more information, see XAML Loading and Dependency Properties.

Property Metadata for a New Dependency Property

When you register a dependency property, the registration through the property system creates a metadata object that stores property characteristics. Many of these characteristics have defaults that are set if the property is registered with the simple signatures of Register. Other signatures of Register allow you to specify the metadata that you want as you register the property. The most common metadata given for dependency properties is to give them a default value that is applied on new instances that use the property.

If you are creating a dependency property that exists on a derived class of FrameworkElement, you can use the more specialized metadata class FrameworkPropertyMetadata rather than the base PropertyMetadata class. The constructor for the FrameworkPropertyMetadata class has several signatures where you can specify various metadata characteristics in combination. If you want to specify the default value only, use the signature that takes a single parameter of type Object. Pass that object parameter as a type-specific default value for your property (the default value provided must be the type you provided as the propertyType parameter in the Register call).

For FrameworkPropertyMetadata, you can also specify metadata option flags for your property. These flags are converted into discrete properties on the property metadata after registration and are used to communicate certain conditionals to other processes such as the layout engine.

Setting Appropriate Metadata Flags

  • If your property (or changes in its value) affects the user interface (UI), and in particular affects how the layout system should size or render your element in a page, set one or more of the following flags: AffectsMeasure, AffectsArrange, AffectsRender.

    • AffectsMeasure indicates that a change to this property requires a change to UI rendering where the containing object might require more or less space within the parent. For example, a "Width" property should have this flag set.

    • AffectsArrange indicates that a change to this property requires a change to UI rendering that typically does not require a change in the dedicated space, but does indicate that the positioning within the space has changed. For example, an "Alignment" property should have this flag set.

    • AffectsRender indicates that some other change has occurred that will not affect layout and measure, but does require another render. An example would be a property that changes a color of an existing element, such as "Background".

    • These flags are often used as a protocol in metadata for your own override implementations of property system or layout callbacks. For instance, you might have an OnPropertyChanged callback that will call InvalidateArrange if any property of the instance reports a value change and has AffectsArrange as true in its metadata.

  • Some properties may affect the rendering characteristics of the containing parent element, in ways above and beyond the changes in required size mentioned above. An example is the MinOrphanLines property used in the flow document model, where changes to that property can change the overall rendering of the flow document that contains the paragraph. Use AffectsParentArrange or AffectsParentMeasure to identify similar cases in your own properties.

  • By default, dependency properties support data binding. You can deliberately disable data binding, for cases where there is no realistic scenario for data binding, or where performance in data binding for a large object is recognized as a problem.

  • By default, data binding Mode for dependency properties defaults to OneWay. You can always change the binding to be TwoWay per binding instance; for details, see Specify the Direction of the Binding. But as the dependency property author, you can choose to make the property use TwoWay binding mode by default. An example of an existing dependency property is MenuItem.IsSubmenuOpen; the scenario for this property is that the IsSubmenuOpen setting logic and the compositing of MenuItem interact with the default theme style. The IsSubmenuOpen property logic uses data binding natively to maintain the state of the property in accordance to other state properties and method calls. Another example property that binds TwoWay by default is TextBox.Text.

  • You can also enable property inheritance in a custom dependency property by setting the Inherits flag. Property inheritance is useful for a scenario where parent elements and child elements have a property in common, and it makes sense for the child elements to have that particular property value set to the same value as the parent set it. An example inheritable property is DataContext, which is used for binding operations to enable the important master-detail scenario for data presentation. By making DataContext inheritable, any child elements inherit that data context also. Because of property value inheritance, you can specify a data context at the page or application root, and do not need to respecify it for bindings in all possible child elements. DataContext is also a good example to illustrate that inheritance overrides the default value, but it can always be set locally on any particular child element; for details, see Use the Master-Detail Pattern with Hierarchical Data. Property value inheritance does have a possible performance cost, and thus should be used sparingly; for details, see Property Value Inheritance.

  • Set the Journal flag to indicate if your dependency property should be detected or used by navigation journaling services. An example is the SelectedIndex property; any item selected in a selection control should be persisted when the journaling history is navigated.

Read-Only Dependency Properties

You can define a dependency property that is read-only. However, the scenarios for why you might define your property as read-only are somewhat different, as is the procedure for registering them with the property system and exposing the identifier. For more information, see Read-Only Dependency Properties.

Collection-Type Dependency Properties

Collection-type dependency properties have some additional implementation issues to consider. For details, see Collection-Type Dependency Properties.

Dependency Property Security Considerations

Dependency properties should be declared as public properties. Dependency property identifier fields should be declared as public static fields. Even if you attempt to declare other access levels (such as protected), a dependency property can always be accessed through the identifier in combination with the property system APIs. Even a protected identifier field is potentially accessible because of metadata reporting or value determination APIs that are part of the property system, such as LocalValueEnumerator. For more information, see Dependency Property Security.

Dependency Properties and Class Constructors

There is a general principle in managed code programming (often enforced by code analysis tools such as FxCop) that class constructors should not call virtual methods. This is because constructors can be called as base initialization of a derived class constructor, and entering the virtual method through the constructor might occur at an incomplete initialization state of the object instance being constructed. When you derive from any class that already derives from DependencyObject, you should be aware that the property system itself calls and exposes virtual methods internally. These virtual methods are part of the WPF property system services. Overriding the methods enables derived classes to participate in value determination. To avoid potential issues with runtime initialization, you should not set dependency property values within constructors of classes, unless you follow a very specific constructor pattern. For details, see Safe Constructor Patterns for DependencyObjects.

See also