Collection-Type Dependency Properties
This topic provides guidance and suggested patterns for how to implement a dependency property where the type of the property is a collection type.
This topic contains the following sections.
- Implementing a Collection-Type Dependency Property
- Initializing the Collection Beyond the Default Value
- Reporting Binding Value Changes from Collection Properties
- Related Topics
Implementing a Collection-Type Dependency Property
For a dependency property in general, the implementation pattern that you follow is that you define a CLR property wrapper, where that property is backed by a DependencyProperty identifier rather than a field or other construct. You follow this same pattern when you implement a collection-type property. However, a collection-type property introduces some complexity to the pattern whenever the type that is contained within the collection is itself a DependencyObject or Freezable derived class.
Initializing the Collection Beyond the Default Value
When you create a dependency property, you do not specify the property default value as the initial field value. Instead, you specify the default value through the dependency property metadata. If your property is a reference type, the default value specified in dependency property metadata is not a default value per instance; instead it is a default value that applies to all instances of the type. Therefore you must be careful to not use the singular static collection defined by the collection property metadata as the working default value for newly created instances of your type. Instead, you must make sure that you deliberately set the collection value to a unique (instance) collection as part of your class constructor logic.
Consider the following example. The following section of the example shows the definition for a class Aquarium
. The class defines the collection type dependency property AquariumObjects
, which uses the generic List type with a FrameworkElement type constraint. In the Register call for the dependency property, the metadata establishes the default value to be a new generic List.
public class Fish : FrameworkElement { }
public class Aquarium : DependencyObject
{
private static readonly DependencyPropertyKey AquariumContentsPropertyKey =
DependencyProperty.RegisterReadOnly(
"AquariumContents",
typeof(List<FrameworkElement>),
typeof(Aquarium),
new FrameworkPropertyMetadata(new List<FrameworkElement>())
);
public static readonly DependencyProperty AquariumContentsProperty =
AquariumContentsPropertyKey.DependencyProperty;
public List<FrameworkElement> AquariumContents
{
get { return (List<FrameworkElement>)GetValue(AquariumContentsProperty); }
...
}
However, if you just left the code like this, that single list default value is shared for all instances of Aquarium
. If you ran the following test code, which is intended to show how you would instantiate two separate Aquarium
instances and add a single different Fish
to each of them, you would see a surprising result:
Aquarium myAq1 = new Aquarium();
Aquarium myAq2 = new Aquarium();
Fish f1 = new Fish();
Fish f2 = new Fish();
myAq1.AquariumContents.Add(f1);
myAq2.AquariumContents.Add(f2);
MessageBox.Show("aq1 contains " + myAq1.AquariumContents.Count.ToString() + " things");
MessageBox.Show("aq2 contains " + myAq2.AquariumContents.Count.ToString() + " things");
Instead of each collection having a count of one, each collection has a count of two! This is because each Aquarium
added its Fish
to the default value collection, which resulted from a single constructor call in the metadata and is therefore shared between all instances. This situation is almost never what you want.
To correct this problem, you must reset the collection dependency property value to a unique instance, as part of the class constructor call. Because the property is a readonly dependency property, you use the SetValue method to set it, using the DependencyPropertyKey that is only accessible within the class.
public Aquarium() : base()
{
SetValue(AquariumContentsPropertyKey, new List<FrameworkElement>());
}
Now, if you ran that same test code again, you could see more expected results, where each Aquarium
supported its own unique collection.
There would be a slight variation on this pattern if you chose to have your collection property be read-write. In that case, you could call the public set accessor from the constructor to do the initialization, which would stilll be calling the nonkey signature of SetValue within your set wrapper, using a public DependencyProperty identifier.
Reporting Binding Value Changes from Collection Properties
A collection property that is itself a dependency property does not automatically report changes to its subproperties. If you are creating bindings into a collection, this can prevent the binding from reporting changes, thus invalidating some data binding scenarios. However, if you use the collection type FreezableCollection as your collection type, then subproperty changes to contained elements in the collection are properly reported, and binding works as expected.
To enable subproperty binding in a dependency object collection, create the collection property as type FreezableCollection, with a type constraint to any DependencyObject derived class.
See Also
Reference
Concepts
XAML and Custom Classes
Data Binding Overview
Dependency Properties Overview
Custom Dependency Properties
Dependency Property Metadata