Personalización de las propiedades de dependencia (WPF .NET)

Los desarrolladores de aplicaciones y los autores de componentes de Windows Presentation Foundation (WPF) pueden crear propiedades de dependencia personalizadas para ampliar la funcionalidad de sus propiedades. A diferencia de una propiedad de Common Language Runtime (CLR), una propiedad de dependencia agrega compatibilidad con estilos, enlaces de datos, herencia, animaciones y valores predeterminados. Background, Width y Text son ejemplos de propiedades de dependencia existentes en clases WPF. En este artículo se describe cómo implementar propiedades de dependencia personalizadas y se presentan opciones para mejorar el rendimiento, la facilidad de uso y la versatilidad.

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 estar familiarizado con el lenguaje XAML (XAML) y saber cómo escribir aplicaciones WPF.

Identificador de propiedad de dependencia

Las propiedades de dependencia son propiedades que se registran con el sistema de propiedades de WPF a través de Register o llamadas a RegisterReadOnly. El método Register devuelve una instancia DependencyProperty que contiene el nombre registrado y las características de una propiedad de dependencia. Asignará la instancia DependencyProperty a un campo estático de solo lectura, conocido como identificador de propiedad de dependencia, que por convención se denomina <property name>Property. Por ejemplo, el campo de identificador de la propiedad Background siempre es BackgroundProperty.

El identificador de propiedad de dependencia se usa como campo de respaldo para obtener o establecer valores de propiedad, en lugar del patrón estándar de respaldo de una propiedad con un campo privado. El identificador no se usa únicamente por el sistema de propiedades; los procesadores XAML también pueden usarlo, y el código (y posiblemente el código externo) puede acceder a las propiedades de dependencia a través de sus identificadores.

Las propiedades de dependencia solo se pueden aplicar a clases derivadas de tipos DependencyObject. La mayoría de las clases de WPF admiten propiedades de dependencia, ya que DependencyObject está cerca de la raíz de la jerarquía de clases de WPF. Para obtener más información sobre las propiedades de dependencia y la terminología y las convenciones que se usan para describirlas, consulte Introducción a las propiedades de dependencia.

Contenedores de propiedades de dependencia

Las propiedades de dependencia de WPF que no son propiedades adjuntas se exponen mediante un contenedor CLR que implementa los descriptores de acceso get y set. Mediante el uso de un contenedor de propiedades, los consumidores de propiedades de dependencia pueden obtener o establecer valores de propiedad de dependencia, como lo harían con cualquier otra propiedad CLR. Los descriptores de acceso get y set interactúan con el sistema de propiedades subyacente a través de DependencyObject.GetValue y llamadas a DependencyObject.SetValue, pasando el identificador de propiedad de dependencia como un parámetro. Normalmente los consumidores de propiedades de dependencia no llaman a GetValue ni a SetValue directamente, pero si va a implementar una propiedad de dependencia personalizada, usará esos métodos en el contenedor.

Cuándo implementar una propiedad de dependencia

Al implementar una propiedad en una clase que se deriva de DependencyObject, se convierte en una propiedad de dependencia mediante la copia de seguridad de la propiedad con un identificador DependencyProperty. Depende de su escenario el que crear una propiedad de dependencia sea o no beneficioso. Aunque la copia de seguridad de la propiedad con un campo privado es adecuada para algunos escenarios, considere la posibilidad de implementar una propiedad de dependencia si desea que la propiedad admita una o varias de las siguientes funcionalidades de WPF:

  • Propiedades que se pueden establecer dentro de un estilo. Para más información, vea Estilos y plantillas.

  • Propiedades que admiten el enlace de datos. Para obtener más información sobre las propiedades de dependencia de enlace de datos, consulte Enlazar las propiedades de dos controles.

  • Propiedades que se pueden establecer a través de referencias de recursos dinámicos. Para obtener más información, consulte Recursos XAML.

  • Propiedades que heredan su valor automáticamente de un elemento primario en el árbol de elementos. Para ello, deberá registrarse mediante RegisterAttached, incluso si también crea un contenedor de propiedades para el acceso CLR. Para obtener más información, consulte Herencia de valores de propiedad.

  • Propiedades que se pueden animar. Para obtener más información, vea Información general sobre animaciones.

  • Notificación por parte del sistema de propiedades de WPF cuando cambia un valor de propiedad. Los cambios pueden deberse a acciones del sistema de propiedades, el entorno, el usuario o los estilos. La propiedad puede especificar un método de devolución de llamada a metadatos de propiedad que será invocado cada vez que el sistema de propiedades determine que el valor de la propiedad ha cambiado. Un concepto relacionado es la coerción del valor de propiedad. Para obtener más información, consulte Devoluciones de llamada y validación de las propiedades de dependencia.

  • Acceso a metadatos de propiedad de dependencia, que leen los procesos de WPF. Por ejemplo, puede usar metadatos de propiedad para:

    • Especificar si un valor de propiedad de dependencia modificado debe hacer que el sistema de diseño recomponga objetos visuales para un elemento.

    • Establecer el valor predeterminado de una propiedad de dependencia mediante la invalidación de los metadatos en las clases derivadas.

  • Compatibilidad con el diseñador de WPF de Visual Studio, como la edición de las propiedades de un control personalizado en la ventana Propiedades. Para obtener más información, consulte Información general sobre la creación de controles.

En algunos escenarios invalidar los metadatos de una propiedad de dependencia existente es una opción mejor que implementar una nueva propiedad de dependencia. El hecho de que una invalidación de metadatos sea práctica depende del escenario y de la similitud de ese escenario con la implementación de las clases y propiedades de dependencia de WPF existentes. Para obtener más información sobre cómo invalidar los metadatos en las propiedades de dependencia existentes, consulte Metadatos de propiedad de dependencia.

Lista de comprobación para crear una propiedad de dependencia

Siga estos pasos para crear una propiedad de dependencia. Algunos de los pasos se pueden combinar e implementar en una sola línea de código.

  1. (Opcional) Cree metadatos de propiedad de dependencia.

  2. Registre la propiedad de dependencia con el sistema de propiedades, especificando un nombre de propiedad, un tipo de propietario, el tipo de valor de propiedad y, opcionalmente, los metadatos de propiedad.

  3. Defina un identificador DependencyProperty como un campo public static readonly en el tipo de propietario. El nombre del campo de identificador es el nombre de propiedad con el sufijo Property anexado.

  4. Defina una propiedad CLR contenedora con el mismo nombre que el nombre de la propiedad de dependencia. En el contenedor CLR, implemente los descriptores de acceso get y set que se conectan con la propiedad de dependencia que respalda al contenedor.

Registro de la propiedad

Para que la propiedad sea una propiedad de dependencia, debe registrarla en el sistema de propiedades. Para registrar la propiedad, llame al método Register desde dentro del cuerpo de la clase, pero fuera de cualquier definición de miembro. El método Register devuelve un identificador de propiedad de dependencia único que se usará al llamar a la API del sistema de propiedades. La razón por la que la llamada Register se realiza fuera de las definiciones de miembro es porque se asigna el valor devuelto a un campo public static readonly de tipo DependencyProperty. Este campo, que creará en la clase, es el identificador de la propiedad de dependencia. En el ejemplo siguiente el primer argumento de Register denomina la propiedad de dependencia AquariumGraphic.

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

Nota:

La definición de la propiedad de dependencia en el cuerpo de clase es la implementación típica, pero también es posible definir una propiedad de dependencia en el constructor estático de clase. Este enfoque podría tener sentido si necesita más de una línea de código para inicializar la propiedad de dependencia.

Nomenclatura de propiedades de dependencia

La convención de nomenclatura establecida para las propiedades de dependencia es obligatoria para un comportamiento normal del sistema de propiedades. El nombre del campo de identificador que se cree debe ser el nombre registrado de la propiedad con el sufijo Property.

Un nombre de propiedad de dependencia debe ser único dentro de la clase de registro. Las propiedades de dependencia que se heredan a través de un tipo base ya se han registrado y no se pueden registrar mediante un tipo derivado. Sin embargo, puede usar una propiedad de dependencia registrada por un tipo diferente, incluso un tipo del que la clase no hereda, agregando la clase como propietario de la propiedad de dependencia. Para obtener más información sobre cómo agregar una clase como propietario, consulte Metadatos de propiedad de dependencia.

Implementación de un contenedor de propiedades

Por convención el nombre de la propiedad contenedora debe ser el mismo que el primer parámetro de la llamada Register, que es el nombre de la propiedad de dependencia. La implementación del contenedor llamará a GetValue en el descriptor de acceso get y a SetValue en el descriptor de acceso set (para las propiedades de lectura y escritura). En el ejemplo siguiente se muestra un contenedor, después de la declaración de campo de identificador y la llamada de registro. Todas las propiedades de dependencia pública de las clases WPF usan un modelo contenedor similar.

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );

// Declare a read-write property wrapper.
public Uri AquariumGraphic
{
    get => (Uri)GetValue(AquariumGraphicProperty);
    set => SetValue(AquariumGraphicProperty, value);
}
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

' Declare a read-write property wrapper.
Public Property AquariumGraphic As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set
        SetValue(AquariumGraphicProperty, Value)
    End Set
End Property

Excepto en raras ocasiones, la implementación del contenedor solo debe contener código GetValue y SetValue. Para ver los motivos detrás de esto, consulte Implicaciones para las propiedades de dependencia personalizadas.

Si la propiedad no sigue las convenciones de nomenclatura establecidas, podría encontrarse con estos problemas:

  • Algunos aspectos de los estilos y plantillas no funcionarán.

  • La mayoría de las herramientas y diseñadores se basan en las convenciones de nomenclatura para serializar correctamente XAML y proporcionar asistencia al entorno del diseñador en un nivel por propiedad.

  • La implementación actual del cargador XAML de WPF omite completamente los contenedores y se basa en la convención de nomenclatura para procesar los valores de atributo. Para obtener más información, consulte Carga de XAML y propiedades de dependencia.

Metadatos de las propiedades de dependencia

Al registrar una propiedad de dependencia, el sistema de propiedades crea un objeto de metadatos para almacenar las características de la propiedad. Las sobrecargas del método Register permiten especificar metadatos de propiedad durante el registro, por ejemplo, Register(String, Type, Type, PropertyMetadata). Un uso común de los metadatos de propiedad es aplicar un valor predeterminado personalizado para las nuevas instancias que usan una propiedad de dependencia. Si no proporciona metadatos de propiedad, el sistema de propiedades asignará valores predeterminados a muchas de las características de la propiedad de dependencia.

Si va a crear una propiedad de dependencia en una clase derivada de FrameworkElement, puede usar la clase de metadatos FrameworkPropertyMetadata, más especializada, en lugar de su clase base PropertyMetadata. Varias firmas de constructor FrameworkPropertyMetadata permiten especificar diferentes combinaciones de características de metadatos. Si solo desea especificar un valor predeterminado, use FrameworkPropertyMetadata(Object) y pase el valor predeterminado al parámetro Object. Asegúrese de que el tipo de valor coincide con el propertyType especificado en la llamada Register.

Algunas sobrecargas de FrameworkPropertyMetadata permiten especificar marcas de configuración de metadatos para la propiedad. El sistema de propiedades convierte estas marcas en propiedades discretas y los valores de marca los usan los procesos de WPF como el motor de diseño.

Establecimiento de marcas de metadatos

Tenga en cuenta lo siguiente al establecer marcas de metadatos:

  • Si el valor de la propiedad (o los cambia en él) afecta a la forma en que el sistema de diseño representa un elemento de la interfaz de usuario, establezca una o varias de las marcas siguientes:

    • AffectsMeasure, que indica que un cambio en el valor de propiedad requiere un cambio en la representación de la interfaz de usuario, específicamente el espacio ocupado por un objeto dentro de su elemento primario. Por ejemplo, establezca esta marca de metadatos para una propiedad Width.

    • AffectsArrange, que indica que un cambio en el valor de propiedad requiere un cambio en la representación de la interfaz de usuario, específicamente la posición de un objeto dentro de su elemento primario. Normalmente el objeto no cambia de tamaño. Por ejemplo, establezca esta marca de metadatos para una propiedad Alignment.

    • AffectsRender, que indica que se ha producido un cambio que no afecta al diseño ni a la medida, pero que todavía requiere otra representación. Por ejemplo, establezca esta marca para una propiedad Background o cualquier otra propiedad que afecte al color de un elemento.

    También puede usar estas marcas como entradas para las implementaciones de invalidación de las devoluciones de llamada del sistema de propiedades (o de diseño). Por ejemplo, puede usar una devolución de llamada OnPropertyChanged para llamar a InvalidateArrange cuando una propiedad de la instancia informa de un cambio de valor y se ha establecido AffectsArrange en metadatos.

  • Algunas propiedades afectan a las características de representación de su elemento primario de otras maneras. Por ejemplo, los cambios en la propiedad MinOrphanLines pueden cambiar la representación general de un documento de flujo. Use AffectsParentArrange o AffectsParentMeasure para señalar acciones primarias en sus propias propiedades.

  • De forma predeterminada, las propiedades de dependencia admiten el enlace de datos. Sin embargo, puede usar IsDataBindingAllowed para deshabilitar el enlace de datos cuando no hay ningún escenario realista para él o donde el rendimiento del enlace de datos es problemático, como en objetos grandes.

  • Aunque el modo de enlace de datos predeterminado para las propiedades de dependencia es OneWay, puede cambiar el modo de enlace de un enlace específico a TwoWay. Para obtener más información, consulte Dirección de enlace. Como autor de una propiedad de dependencia, puede incluso elegir establecer el enlace de dos vías en el modo predeterminado. Un ejemplo de una propiedad de dependencia existente que usa el enlace de datos de dos vías es MenuItem.IsSubmenuOpen, que tiene un estado basado en otras propiedades y llamadas a métodos. El escenario para IsSubmenuOpen es que su lógica de configuración y la composición de MenuItem interactúan con el estilo de tema predeterminado. TextBox.Text es otra propiedad de dependencia de WPF que usa el enlace de dos vías de forma predeterminada.

  • Puede habilitar la herencia de propiedades para la propiedad de dependencia estableciendo la marca Inherits. La herencia de propiedades es útil para escenarios en los que los elementos primarios y secundarios tienen una propiedad en común y en los que tiene sentido que el elemento secundario herede el valor primario de la propiedad común. Un ejemplo de una propiedad heredable es DataContext, que admite operaciones de enlace que usan el escenario maestro-detalle para la presentación de datos. La herencia de valores de propiedad permite especificar un contexto de datos en la raíz de la aplicación o la página, lo que ahorra tener que especificarlo para los enlaces de elementos secundarios. Aunque un valor de propiedad heredado invalida el valor predeterminado, los valores de propiedad se pueden establecer localmente en cualquier elemento secundario. Use la herencia de valores de propiedad con moderación porque tiene un costo de rendimiento. Para obtener más información, consulte Herencia de valores de propiedad.

  • Establezca la marca Journal para indicar que la propiedad de dependencia debería ser detectada o usada por los servicios de registro en diario de navegación. Por ejemplo, la propiedad SelectedIndex establece la marca Journal para recomendar que las aplicaciones mantengan un historial de registro en diario de los elementos seleccionados.

Propiedades de dependencia de solo lectura

Puede definir una propiedad de dependencia de solo lectura. Un escenario típico es una propiedad de dependencia que almacena el estado interno. Por ejemplo, IsMouseOver es de solo lectura porque su estado solo debe determinarse mediante la entrada del mouse. Para obtener más información, consulte Propiedades de dependencia de solo lectura.

Propiedades de dependencia de tipo de colección

Las propiedades de dependencia de tipo de colección tienen problemas de implementación adicionales que se deben tener en cuenta, como el establecer un valor predeterminado para los tipos de referencia y la compatibilidad con el enlace de datos para los elementos de colección. Para obtener más información, consulte Propiedades de dependencia de tipo colección.

Seguridad de las propiedades de dependencia

Normalmente, declarará las propiedades de dependencia como propiedades públicas y los campos de identificador DependencyProperty como campos public static readonly. Si especifica un nivel de acceso más restrictivo, como protected, todavía se puede acceder a una propiedad de dependencia a través de su identificador en combinación con las API del sistema de propiedades. Incluso un campo de identificador protegido es potencialmente accesible a través de informes de metadatos de WPF o API de determinación de valores, como LocalValueEnumerator. Para obtener más información, consulte Seguridad de las propiedades de dependencia.

En el caso de las propiedades de dependencia de solo lectura, el valor devuelto de RegisterReadOnly es DependencyPropertyKey y normalmente DependencyPropertyKey no se convierte en miembro de la clase public. Dado que el sistema de propiedades de WPF no propaga el DependencyPropertyKey fuera del código, una propiedad de dependencia de solo lectura tiene mejor seguridad set que una propiedad de dependencia de lectura y escritura.

Propiedades de dependencia y constructores de clases

Hay un principio general en la programación de código administrado, a menudo aplicado por las herramientas de análisis de código, y es que los constructores de clase no deben llamar a métodos virtuales. Esto se debe a que se puede llamar a constructores base durante la inicialización de un constructor de clase derivada, y un método virtual llamado por un constructor base podría ejecutarse antes de la inicialización completa de la clase derivada. Cuando se deriva de una clase que ya se deriva de DependencyObject, el propio sistema de propiedades llama a, y expone métodos virtuales internamente. Estos métodos virtuales forman parte de los servicios del sistema de propiedades de WPF. La invalidación de los métodos permite que las clases derivadas participen en la determinación del valor. Para evitar posibles problemas con la inicialización en tiempo de ejecución, no debe establecer valores de propiedad de dependencia dentro de constructores de clases, a menos que siga un patrón de constructor específico. Para obtener más información, consulte Patrones de constructor seguros para DependencyObjects.

Vea también