Propiedades de dependencia de tipo colección (WPF .NET)

En este artículo se proporcionan instrucciones y patrones sugeridos para implementar una propiedad de dependencia que sea un tipo colección.

Importante

La documentación de la guía de escritorio para .NET 7 y .NET 6 está en proceso de elaboración.

Requisitos previos

En el artículo se da por supuesto un conocimiento básico de las propiedades de dependencia y que ha leído Información general sobre las propiedades de dependencia. Para seguir los ejemplos de este artículo, resultará útil que esté familiarizado con lenguaje XAML y sepa cómo escribir aplicaciones WPF.

Implementación de una propiedad de dependencia de tipo colección

En general el patrón de implementación de una propiedad de dependencia es un contenedor de propiedades CLR con el respaldo de un identificador DependencyProperty en lugar de un campo u otra construcción. Puede seguir el mismo patrón al implementar una propiedad de dependencia de tipo colección. El patrón es más complejo si el tipo de elemento de colección es un DependencyObject o un Freezable una clase derivada.

Inicialización de la colección

Cuando se crea una propiedad de dependencia, normalmente se especifica el valor predeterminado mediante metadatos de propiedad de dependencia, en lugar de especificar un valor de propiedad inicial. Sin embargo, si el valor de propiedad es un tipo de referencia, el valor predeterminado debe establecerse en el constructor de la clase que registra la propiedad de dependencia. Los metadatos de la propiedad de dependencia no deben incluir un valor de tipo de referencia predeterminado, porque ese valor se asignará a todas las instancias de la clase, creando una clase singleton.

En el ejemplo siguiente se declara una clase Aquarium que contiene una colección de elementos FrameworkElement en un genérico List<T>. Un valor de colección predeterminado no se incluye en el PropertyMetadata que se pasa al método RegisterReadOnly(String, Type, Type, PropertyMetadata) y, en su lugar, el constructor de clase se usa para establecer el valor de colección predeterminado en un nuevo genérico List.

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, owner type, and property metadata.
    private static readonly DependencyPropertyKey s_aquariumContentsPropertyKey =
        DependencyProperty.RegisterReadOnly(
          name: "AquariumContents",
          propertyType: typeof(List<FrameworkElement>),
          ownerType: typeof(Aquarium),
          typeMetadata: new FrameworkPropertyMetadata()
          //typeMetadata: new FrameworkPropertyMetadata(new List<FrameworkElement>())
        );

    // Set the default collection value in a class constructor.
    public Aquarium() => SetValue(s_aquariumContentsPropertyKey, new List<FrameworkElement>());

    // Declare a public get accessor.
    public List<FrameworkElement> AquariumContents =>
        (List<FrameworkElement>)GetValue(s_aquariumContentsPropertyKey.DependencyProperty);
}

public class Fish : FrameworkElement { }
Public Class Aquarium
    Inherits DependencyObject

    ' Register a dependency property with the specified property name,
    ' property type, owner type, and property metadata.
    Private Shared ReadOnly s_aquariumContentsPropertyKey As DependencyPropertyKey =
        DependencyProperty.RegisterReadOnly(
            name:="AquariumContents",
            propertyType:=GetType(List(Of FrameworkElement)),
            ownerType:=GetType(Aquarium),
            typeMetadata:=New FrameworkPropertyMetadata())
            'typeMetadata:=New FrameworkPropertyMetadata(New List(Of FrameworkElement)))

    ' Set the default collection value in a class constructor.
    Public Sub New()
        SetValue(s_aquariumContentsPropertyKey, New List(Of FrameworkElement)())
    End Sub

    ' Declare a public get accessor.
    Public ReadOnly Property AquariumContents As List(Of FrameworkElement)
        Get
            Return CType(GetValue(s_aquariumContentsPropertyKey.DependencyProperty), List(Of FrameworkElement))
        End Get
    End Property
End Class

Public Class Fish
    Inherits FrameworkElement
End Class

El código de prueba siguiente crea dos instancias independientes de Aquarium y agrega un elemento diferente Fish a cada colección. Si ejecuta el código, verá que cada instancia de Aquarium tiene un único elemento de colección, como cabe esperar.

private void InitializeAquariums(object sender, RoutedEventArgs e)
{
    Aquarium aquarium1 = new();
    Aquarium aquarium2 = new();
    aquarium1.AquariumContents.Add(new Fish());
    aquarium2.AquariumContents.Add(new Fish());
    MessageBox.Show(
        $"aquarium1 contains {aquarium1.AquariumContents.Count} fish\r\n" +
        $"aquarium2 contains {aquarium2.AquariumContents.Count} fish");
}
Private Sub InitializeAquariums(sender As Object, e As RoutedEventArgs)
    Dim aquarium1 As New Aquarium()
    Dim aquarium2 As New Aquarium()
    aquarium1.AquariumContents.Add(New Fish())
    aquarium2.AquariumContents.Add(New Fish())
    MessageBox.Show($"aquarium1 contains {aquarium1.AquariumContents.Count} fish{Environment.NewLine}" +
                    $"aquarium2 contains {aquarium2.AquariumContents.Count} fish")
End Sub

Pero si convierte en comentario el constructor de clase y pasa el valor de colección predeterminado PropertyMetadata al método RegisterReadOnly(String, Type, Type, PropertyMetadata), verá que cada instancia de Aquarium obtiene dos elementos de colección. Esto se debe a que ambas instancias de Fish se agregan a la misma lista, que comparten todas las instancias de la clase Aquarium. Por lo tanto, cuando la intención es que cada instancia de objeto tenga su propia lista, el valor predeterminado debe establecerse en el constructor de clase.

Inicialización de una colección de lectura y escritura

En el ejemplo siguiente se declara una propiedad de dependencia de tipo colección de lectura y escritura en la clase Aquarium mediante los métodos de firma sin clave Register(String, Type, Type) y SetValue(DependencyProperty, Object).

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, and owner type. Store the dependency property
    // identifier as a public static readonly member of the class.
    public static readonly DependencyProperty AquariumContentsProperty =
        DependencyProperty.Register(
          name: "AquariumContents",
          propertyType: typeof(List<FrameworkElement>),
          ownerType: typeof(Aquarium)
        );

    // Set the default collection value in a class constructor.
    public Aquarium() => SetValue(AquariumContentsProperty, new List<FrameworkElement>());

    // Declare public get and set accessors.
    public List<FrameworkElement> AquariumContents
    {
        get => (List<FrameworkElement>)GetValue(AquariumContentsProperty);
        set => SetValue(AquariumContentsProperty, value);
    }
}
Public Class Aquarium
    Inherits DependencyObject

    ' Register a dependency property with the specified property name,
    ' property type, and owner type. Store the dependency property
    ' identifier as a static member of the class.
    Public Shared ReadOnly AquariumContentsProperty As DependencyProperty =
        DependencyProperty.Register(
            name:="AquariumContents",
            propertyType:=GetType(List(Of FrameworkElement)),
            ownerType:=GetType(Aquarium))

    ' Set the default collection value in a class constructor.
    Public Sub New()
        SetValue(AquariumContentsProperty, New List(Of FrameworkElement)())
    End Sub

    ' Declare public get and set accessors.
    Public Property AquariumContents As List(Of FrameworkElement)
        Get
            Return CType(GetValue(AquariumContentsProperty), List(Of FrameworkElement))
        End Get
        Set
            SetValue(AquariumContentsProperty, Value)
        End Set
    End Property
End Class

Propiedades de dependencia FreezableCollection

Una propiedad de dependencia de tipo colección no informa automáticamente de los cambios en sus subpropiedades. Como resultado, si va a enlazar a una colección, es posible que el enlace no informe de los cambios, lo que invalida algunos escenarios de enlace de datos. Sin embargo, si usa FreezableCollection<T> para el tipo de propiedad de dependencia, los cambios en las propiedades de los elementos de colección se notificarán correctamente y el enlace funcionará según lo previsto.

Para habilitar el enlace de subpropiedad en una colección de objetos de dependencia, use el tipo colección FreezableCollection con una restricción de tipo de cualquier clase derivada DependencyObject.

En el ejemplo siguiente se declara una clase Aquarium que contiene un FreezableCollection con una restricción de tipo FrameworkElement. Un valor de colección predeterminado no se incluye en PropertyMetadata que se pasa al método RegisterReadOnly(String, Type, Type, PropertyMetadata) y, en su lugar, el constructor de clase se usa para establecer el valor de colección predeterminado en un nuevo FreezableCollection.

public class Aquarium : DependencyObject
{
    // Register a dependency property with the specified property name,
    // property type, and owner type.
    private static readonly DependencyPropertyKey s_aquariumContentsPropertyKey =
        DependencyProperty.RegisterReadOnly(
          name: "AquariumContents",
          propertyType: typeof(FreezableCollection<FrameworkElement>),
          ownerType: typeof(Aquarium),
          typeMetadata: new FrameworkPropertyMetadata()
        );

    // Store the dependency property identifier as a static member of the class.
    public static readonly DependencyProperty AquariumContentsProperty =
        s_aquariumContentsPropertyKey.DependencyProperty;

    // Set the default collection value in a class constructor.
    public Aquarium() => SetValue(s_aquariumContentsPropertyKey, new FreezableCollection<FrameworkElement>());

    // Declare a public get accessor.
    public FreezableCollection<FrameworkElement> AquariumContents =>
        (FreezableCollection<FrameworkElement>)GetValue(AquariumContentsProperty);
}
Public Class Aquarium
    Inherits DependencyObject

    ' Register a dependency property with the specified property name,
    ' property type, and owner type.
    Private Shared ReadOnly s_aquariumContentsPropertyKey As DependencyPropertyKey =
        DependencyProperty.RegisterReadOnly(
            name:="AquariumContents",
            propertyType:=GetType(FreezableCollection(Of FrameworkElement)),
            ownerType:=GetType(Aquarium),
            typeMetadata:=New FrameworkPropertyMetadata())

    ' Set the default collection value in a class constructor.
    Public Sub New()
        SetValue(s_aquariumContentsPropertyKey, New FreezableCollection(Of FrameworkElement)())
    End Sub

    ' Declare a public get accessor.
    Public ReadOnly Property AquariumContents As FreezableCollection(Of FrameworkElement)
        Get
            Return CType(GetValue(s_aquariumContentsPropertyKey.DependencyProperty), FreezableCollection(Of FrameworkElement))
        End Get
    End Property
End Class

Vea también