Bagikan melalui


Collection-Type Dependency Properties

Microsoft Silverlight will reach end of support after October 2021. Learn more.

This topic provides guidance and suggested patterns for how to implement a dependency property where the type of the property is a collection type.

Prerequisites

This topic assumes that you understand dependency properties from the perspective of a consumer of existing dependency properties on Silverlight classes, and that you have read Dependency Properties Overview and Custom Dependency Objects and Dependency Properties.

Scenarios for Collection-Type Dependency Properties

Collection-type dependency properties are relatively rare in the Silverlight core libraries. In most cases, it is adequate to define the collection such that the item type of the collection is strongly typed to be some kind of DependencyObject, but that the collection property itself is implemented as a conventional CLR property. This is because collections are not necessarily suitable to a number of the typical scenarios where dependency properties are involved:

  • You do not typically animate a collection.

  • You do not typically need to support prepopulating the items in a custom collection property by using a style or a template.

  • Binding to collections is a major scenario, but a collection does not need to be held by a dependency property in order to be a binding source. The Silverlight binding engine has extensive logic that can use standard CLR collections for sources. For binding targets, it is more typical to use existing ItemsControl and DataTemplate principles for items support, or to use view-model patterns. For more information on binding to and from collections, see Data Binding.

  • Notifications for collection changes are better addressed through dedicated CLR interface conventions INotifyPropertyChanged and/or INotifyCollectionChanged, or by deriving the collection type from ObservableCollection<T>.

Nevertheless, scenarios for collection-type dependency properties do exist. The remainder of this topic provides some guidance on how to implement a collection-type dependency property.

Implementing a Collection-Type Dependency Property

For a dependency property in general, the implementation pattern is to 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, if the type that is contained in the collection is itself a DependencyObject derived class, the pattern becomes more complex.

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. Otherwise, you will create an unintentional singleton class.

Consider the following example. The following section of the example shows the definition for an Aquarium class. The class defines the collection-type dependency property AquariumObjects, which uses the generic List<T> type with a FrameworkElement type constraint. In the Register(String, Type, Type, PropertyMetadata) call for the dependency property, the metadata establishes the default value to be a new generic List<T>.

Public Class Aquarium 
    Inherits DependencyObject 
    Private Shared ReadOnly AquariumContentsProperty As DependencyProperty = DependencyProperty.Register("AquariumContents", GetType(List(Of FrameworkElement)), GetType(Aquarium), New PropertyMetadata(New List(Of FrameworkElement)())) 
    Public Property AquariumContents() As List(Of FrameworkElement) 
        Get 
            Return DirectCast(GetValue(AquariumContentsProperty), List(Of FrameworkElement)) 
        End Get 
        Set(ByVal value As List(Of FrameworkElement)) 
            SetValue(AquariumContentsProperty, DirectCast(value, List(Of FrameworkElement))) 
        End Set 
    End Property 
    Public Sub New() 
        MyBase.New() 
    End Sub 
End Class
public class Aquarium : DependencyObject
{
    private static readonly DependencyProperty AquariumContentsProperty =
        DependencyProperty.Register(
          "AquariumContents",
          typeof(List<FrameworkElement>),
          typeof(Aquarium),
          new PropertyMetadata(new List<FrameworkElement>())
        );
    public List<FrameworkElement> AquariumContents
    {
        get { return (List<FrameworkElement>)GetValue(AquariumContentsProperty); }
        set { SetValue(AquariumContentsProperty, (List<FrameworkElement>)value); }
    }
    public Aquarium()
        : base() {}
}

However, if you leave the code as shown, 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: instead of each collection having a count of one, each collection has a count of two!

Dim myAq1 As New Aquarium()
Dim myAq2 As New Aquarium()
Dim f1 As New Fish()
Dim f2 As New Fish()
myAq1.AquariumContents.Add(f1)
myAq2.AquariumContents.Add(f2)
Aquarium myAq1 = new Aquarium();
Aquarium myAq2 = new Aquarium();
Fish f1 = new Fish();
Fish f2 = new Fish();
myAq1.AquariumContents.Add(f1);
myAq2.AquariumContents.Add(f2);

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.

    Public Sub New() 
        MyBase.New() 
        SetValue(AquariumContentsProperty, New List(Of FrameworkElement)()) 
    End Sub 
End Class 
public Aquarium() : base()
{
    SetValue(AquariumContentsProperty, new List<FrameworkElement>()); 
}

Collections and Notifications

Defining the collection as a dependency property does not automatically provide change notification for the items in the collection by virtue of the property system invoking the PropertyChanged callback. If you want notifications for collections or collection items, for example for a data binding scenario, you should implement the interfaces INotifyPropertyChanged and/or INotifyCollectionChanged. For more information, see the "Change Notification" section of Data Binding.