Propiedades adjuntas personalizadas

Una propiedad adjunta es un concepto XAML. Las propiedades adjuntas suelen definirse como una forma especializada de propiedad de dependencia. En este tema se explica cómo implementar una propiedad adjunta como propiedad de dependencia y cómo definir la convención de descriptor de acceso necesaria para que su propiedad adjunta pueda utilizarse en XAML.

Requisitos previos

Suponemos que entiende las propiedades de dependencia desde la perspectiva de un consumidor de propiedades de dependencia existentes y que ha leído la descripción general de las propiedades de dependencia. También debe haber leído Información general sobre las propiedades adjuntas. Para seguir los ejemplos de este tema, también debe comprender XAML y saber cómo escribir una aplicación básica de Windows Runtime con C++, C# o Visual Basic.

Escenarios para propiedades adjuntas

Puede crear una propiedad adjunta cuando existe una razón para tener una mecanismo de definición de propiedades disponible para clases distintas de la clase que se define. Los escenarios más comunes para esto son la compatibilidad con el diseño y los servicios. Algunos ejemplos de propiedades de diseño existentes son Canvas.ZIndex y Canvas.Top. En un escenario de diseño, los elementos que existen como elementos secundarios de elementos que controlan el diseño pueden expresar requisitos de diseño a sus elementos principales de forma individual, estableciendo cada uno de ellos un valor de propiedad que el principal define como propiedad adjunta. Un ejemplo del escenario de soporte técnico de servicios en la API de Windows Runtime es el conjunto de propiedades adjuntas de ScrollViewer, como ScrollViewer.IsZoomChainingEnabled.

Advertencia

Una limitación existente de la implementación XAML de Windows Runtime es que no puede animar su propiedad adjunta personalizada.

Registro de una propiedad adjunta personalizada

Si está definiendo la propiedad adjunta estrictamente para su uso en otros tipos, la clase en la que se registra la propiedad no tiene que derivar de DependencyObject. Pero es necesario que el parámetro de destino para los descriptores de acceso use DependencyObject si sigue el modelo típico de tener la propiedad adjunta también es una propiedad de dependencia, para que pueda usar el almacén de propiedades de respaldo.

Defina la propiedad adjunta como una propiedad de dependencia declarando una propiedad de solo lecturaestáticapública de tipo DependencyProperty. Esta propiedad se define mediante el valor devuelto del método RegisterAttached. El nombre de la propiedad debe coincidir con el nombre de propiedad adjunto que especifique como parámetro RegisterAttachedname, con la cadena "Property" agregada al final. Esta es la convención establecida para asignar un nombre a los identificadores de las propiedades de dependencia en relación con las propiedades que representan.

El área principal en la que la definición de una propiedad adjunta personalizada difiere de una propiedad de dependencia personalizada es la forma en que se definen los descriptores de acceso o los contenedores. En lugar de usar la técnica contenedora descrita en Propiedades de dependencia personalizadas, también debe proporcionar métodos estáticos GetPropertyName y SetPropertyName como descriptores de acceso para la propiedad adjunta. El analizador XAML usa principalmente los descriptores de acceso, aunque cualquier otro autor de llamada también puede usarlos para establecer valores en escenarios que no son XAML.

Importante

Si no define correctamente los descriptores de acceso, el procesador XAML no puede acceder a su propiedad adjunta y cualquier persona que intente usarla probablemente obtendrá un error del analizador XAML. Además, las herramientas de diseño y codificación suelen basarse en las convenciones "*Property" para asignar nombres a identificadores cuando encuentran una propiedad de dependencia personalizada en un ensamblado al que se hace referencia.

Descriptores de acceso

La signatura del descriptor de acceso GetPropertyName debe ser esta.

public staticvalueTypeGetPropertyName(DependencyObject target)

Para Microsoft Visual Basic, es esto.

Public Shared Function GetPropertyName(ByVal target As DependencyObject) As valueType)

El objeto target puede ser de un tipo más específico en la implementación, pero debe derivar de DependencyObject. El valor devuelto valueType también puede ser de un tipo más específico en su implementación. El tipo de objeto básico es aceptable, pero a menudo querrá que la propiedad adjunta aplique la seguridad del tipo. El uso de escribir en las firmas de captador y establecedor es una técnica recomendada de seguridad de tipos.

La signatura del descriptor de acceso SetPropertyName debe esta.

public static void SetPropertyName(DependencyObject target ,valueType value)

Para Visual Basic, es esto.

Public Shared Sub SetPropertyName(ByVal target As DependencyObject, ByVal value AsvalueType)

El objeto target puede ser de un tipo más específico en la implementación, pero debe derivar de DependencyObject. El objeto value y su valueType pueden ser de un tipo más específico en la implementación. Recuerde que el valor de este método es la entrada que proviene del procesador XAML cuando encuentra su propiedad adjunta en el marcado. Debe existir una conversión de tipos o un soporte de extensiones de marcado para el tipo que utilice, de modo que pueda crearse el tipo adecuado a partir de un valor de atributo (que, en última instancia, no es más que una cadena). El tipo de objeto básico es aceptable, pero a menudo querrá mayor seguridad de tipos. Para ello, coloque la aplicación de tipos en los descriptores de acceso.

Nota:

También es posible definir una propiedad adjunta en la que el uso previsto es a través de la sintaxis del elemento de propiedad. En ese caso, no necesita la conversión de tipos para los valores, pero sí tiene que asegurarse de que los valores que pretende se pueden construir en XAML. VisualStateManager.VisualStateGroups es un ejemplo de una propiedad adjunta existente que solo admite el uso de elementos de propiedad.

Ejemplo de código

En este ejemplo se muestra el registro de propiedades de dependencia (mediante el método RegisterAttached), así como los descriptores de acceso Get y Set, para una propiedad adjunta personalizada. En el ejemplo, el nombre de la propiedad adjunta es IsMovable. Por consiguiente, los descriptores de acceso deben denominarse GetIsMovable y SetIsMovable. El propietario de la propiedad adjunta es una clase de servicio denominado GameService que no tiene una interfaz de usuario propia; su propósito es proporcionar solo los servicios de propiedad adjunta cuando se usa la propiedad adjunta GameService.IsMovable.

Definir la propiedad adjunta en C++/CX es un poco más compleja. Debe decidir cómo factorizar entre el encabezado y el archivo de código. Además, debe exponer el identificador como una propiedad con solo un descriptor de acceso get, por motivos descritos en Propiedades de dependencia personalizadas. En C++/CX, debe definir esta relación de campo de propiedad explícitamente en lugar de confiar en palabras clave de lectura de .NET y respaldo implícito de propiedades simples. También debe realizar el registro de la propiedad adjunta dentro de una función auxiliar que solo se ejecuta una vez, cuando se inicia la aplicación por primera vez, pero antes de que se carguen las páginas XAML que necesiten la propiedad adjunta. El lugar típico para llamar a las funciones auxiliares de registro de propiedades para cualquier y todas las propiedades adjuntas o dependencias procede del constructor App / Application en el código del archivo app.xaml.

public class GameService : DependencyObject
{
    public static readonly DependencyProperty IsMovableProperty = 
    DependencyProperty.RegisterAttached(
      "IsMovable",
      typeof(Boolean),
      typeof(GameService),
      new PropertyMetadata(false)
    );
    public static void SetIsMovable(UIElement element, Boolean value)
    {
        element.SetValue(IsMovableProperty, value);
    }
    public static Boolean GetIsMovable(UIElement element)
    {
        return (Boolean)element.GetValue(IsMovableProperty);
    }
}
Public Class GameService
    Inherits DependencyObject

    Public Shared ReadOnly IsMovableProperty As DependencyProperty = 
        DependencyProperty.RegisterAttached("IsMovable",  
        GetType(Boolean), 
        GetType(GameService), 
        New PropertyMetadata(False))

    Public Shared Sub SetIsMovable(ByRef element As UIElement, value As Boolean)
        element.SetValue(IsMovableProperty, value)
    End Sub

    Public Shared Function GetIsMovable(ByRef element As UIElement) As Boolean
        GetIsMovable = CBool(element.GetValue(IsMovableProperty))
    End Function
End Class
// GameService.idl
namespace UserAndCustomControls
{
    [default_interface]
    runtimeclass GameService : Windows.UI.Xaml.DependencyObject
    {
        GameService();
        static Windows.UI.Xaml.DependencyProperty IsMovableProperty{ get; };
        static Boolean GetIsMovable(Windows.UI.Xaml.DependencyObject target);
        static void SetIsMovable(Windows.UI.Xaml.DependencyObject target, Boolean value);
    }
}

// GameService.h
...
    static Windows::UI::Xaml::DependencyProperty IsMovableProperty() { return m_IsMovableProperty; }
    static bool GetIsMovable(Windows::UI::Xaml::DependencyObject const& target) { return winrt::unbox_value<bool>(target.GetValue(m_IsMovableProperty)); }
    static void SetIsMovable(Windows::UI::Xaml::DependencyObject const& target, bool value) { target.SetValue(m_IsMovableProperty, winrt::box_value(value)); }

private:
    static Windows::UI::Xaml::DependencyProperty m_IsMovableProperty;
...

// GameService.cpp
...
Windows::UI::Xaml::DependencyProperty GameService::m_IsMovableProperty =
    Windows::UI::Xaml::DependencyProperty::RegisterAttached(
        L"IsMovable",
        winrt::xaml_typename<bool>(),
        winrt::xaml_typename<UserAndCustomControls::GameService>(),
        Windows::UI::Xaml::PropertyMetadata{ winrt::box_value(false) }
);
...
// GameService.h
#pragma once

#include "pch.h"
//namespace WUX = Windows::UI::Xaml;

namespace UserAndCustomControls {
    public ref class GameService sealed : public WUX::DependencyObject {
    private:
        static WUX::DependencyProperty^ _IsMovableProperty;
    public:
        GameService::GameService();
        void GameService::RegisterDependencyProperties();
        static property WUX::DependencyProperty^ IsMovableProperty
        {
            WUX::DependencyProperty^ get() {
                return _IsMovableProperty;
            }
        };
        static bool GameService::GetIsMovable(WUX::UIElement^ element) {
            return (bool)element->GetValue(_IsMovableProperty);
        };
        static void GameService::SetIsMovable(WUX::UIElement^ element, bool value) {
            element->SetValue(_IsMovableProperty,value);
        }
    };
}

// GameService.cpp
#include "pch.h"
#include "GameService.h"

using namespace UserAndCustomControls;

using namespace Platform;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Data;
using namespace Windows::UI::Xaml::Documents;
using namespace Windows::UI::Xaml::Input;
using namespace Windows::UI::Xaml::Interop;
using namespace Windows::UI::Xaml::Media;

GameService::GameService() {};

GameService::RegisterDependencyProperties() {
    DependencyProperty^ GameService::_IsMovableProperty = DependencyProperty::RegisterAttached(
         "IsMovable", Platform::Boolean::typeid, GameService::typeid, ref new PropertyMetadata(false));
}

Establecimiento de la propiedad adjunta personalizada desde el marcado XAML

Después de definir la propiedad adjunta e incluir sus miembros de soporte técnico como parte de un tipo personalizado, debe hacer que las definiciones estén disponibles para el uso de XAML. Para ello, debe asignar un espacio de nombres XAML que haga referencia al espacio de nombres de código que contiene la clase pertinente. En los casos en los que haya definido la propiedad adjunta como parte de una biblioteca, debe incluir esa biblioteca como parte del paquete de la aplicación para la aplicación.

Normalmente, una asignación de espacio de nombres XML para XAML se coloca en el elemento raíz de una página XAML. Por ejemplo, para la clase denominada GameService en el espacio de nombres UserAndCustomControls que contiene las definiciones de propiedades adjuntas que se muestran en fragmentos de código anteriores, la asignación podría tener este aspecto.

<UserControl
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:uc="using:UserAndCustomControls"
  ... >

Con la asignación, puede establecer la propiedad adjunta GameService.IsMovable en cualquier elemento que coincida con la definición de destino, incluido un tipo existente que define Windows Runtime.

<Image uc:GameService.IsMovable="True" .../>

Si va a establecer la propiedad en un elemento que también está dentro del mismo espacio de nombres XML asignado, debe incluir el prefijo en el nombre de la propiedad adjunta. Esto se debe a que el prefijo califica el tipo de propietario. No se puede suponer que el atributo de la propiedad adjunta está dentro del mismo espacio de nombres XML que el elemento donde se incluye el atributo, aunque, mediante reglas XML normales, los atributos pueden heredar el espacio de nombres de los elementos. Por ejemplo, si establece GameService.IsMovable en un tipo personalizado de ImageWithLabelControl (no se muestra la definición) e incluso si ambos se definieron en el mismo espacio de nombres de código asignado al mismo prefijo, el XAML seguirá siendo este.

<uc:ImageWithLabelControl uc:GameService.IsMovable="True" .../>

Nota:

Si está escribiendo una interfaz de usuario XAML con C++/CX, debe incluir el encabezado para el tipo personalizado que define la propiedad adjunta, siempre que una página XAML use ese tipo. Cada página XAML tiene un encabezado de código subyacente asociado (.xaml.h). Aquí es donde debe incluir (mediante #include) el encabezado para la definición del tipo de propietario de la propiedad adjunta.

Establecimiento de la propiedad adjunta personalizada de forma imperativa

También puede acceder a una propiedad adjunta personalizada desde código imperativo. El código siguiente muestra cómo.

<Image x:Name="gameServiceImage"/>
// MainPage.h
...
#include "GameService.h"
...

// MainPage.cpp
...
MainPage::MainPage()
{
    InitializeComponent();

    GameService::SetIsMovable(gameServiceImage(), true);
}
...

Tipo de valor de una propiedad adjunta personalizada

El tipo que se usa como el tipo de valor de una propiedad adjunta personalizada afecta al uso, la definición o tanto el uso como la definición. El tipo de valor de la propiedad adjunta se declara en varios lugares: en las firmas de los métodos de descriptor de acceso Get y Set, y también como el parámetro propertyType de la llamada RegisterAttached.

El tipo de valor más común para las propiedades adjuntas (personalizado o de otro modo) es una cadena simple. Esto se debe a que las propiedades adjuntas suelen ser diseñadas para el uso de atributos XAML y el uso de una cadena como tipo de valor mantiene las propiedades ligeras. Otros primitivos que tienen conversión nativa a métodos de cadena, como enteros, dobles o un valor de enumeración, también son comunes como tipos de valor para las propiedades adjuntas. Puede usar otros tipos de valor (los que no admiten la conversión de cadenas nativas) como valor de propiedad adjunta. Sin embargo, esto implica tomar una decisión sobre el uso o la implementación:

  • Puede dejar la propiedad adjunta tal cual, pero la propiedad adjunta solo puede admitir el uso donde la propiedad adjunta es un elemento de propiedad y el valor se declara como un elemento de objeto. En este caso, el tipo de propiedad tiene que admitir el uso de XAML como un elemento de objeto. Para las clases de referencia de Windows Runtime existentes, compruebe la sintaxis XAML para asegurarse de que el tipo admite el uso de elementos de objeto XAML.
  • Puede dejar la propiedad adjunta tal como está, pero usarla solo en un uso de atributos a través de una técnica de referencia XAML, como Binding o StaticResource que se puede expresar como una cadena.

Más información sobre el ejemplo Canvas.Left

En ejemplos anteriores de usos de propiedades adjuntas, mostramos diferentes formas de establecer la propiedad adjunta Canvas.Left. ¿Pero qué cambia sobre cómo interactúa un Canvas con el objeto y cuándo ocurre? Examinaremos aún más este ejemplo concreto, ya que si implementa una propiedad adjunta, es interesante ver lo que otra clase de propietario de propiedad adjunta típica pretende hacer con sus valores de propiedad adjuntas si los encuentra en otros objetos.

La función principal de un Canvas es ser un contenedor de diseño con posición absoluta en la interfaz de usuario. Los elementos secundarios de un Canvas se almacenan en una propiedad definida de clase base Children. De todos los paneles Canvas es el único que usa posicionamiento absoluto. Habría sobredimensionado el modelo de objetos del tipo UIElement común para agregar propiedades que solo podrían preocuparse por Canvas y aquellos casos de UIElement concretos en los que son elementos secundarios de un UIElement. Definir las propiedades del control de diseño de un Canvas para que se adjunte las propiedades que cualquier UIElement pueda usar mantiene el modelo de objetos más limpio.

Para ser un panel práctico, Canvas tiene un comportamiento que invalida los métodos Measure y Arrange de nivel de marco. Aquí es donde Canvas comprueba realmente si hay valores de propiedad adjuntos en sus elementos secundarios. Parte de los patrones Measure y Arrange es un bucle que recorre en iteración cualquier contenido y un panel tiene la propiedad Children que hace que sea explícito lo que se supone que se debe considerar el elemento secundario de un panel. Por lo tanto, el comportamiento de diseño de Canvas recorre en iteración estos elementos secundarios y realiza llamadas estáticas Canvas.GetLeft y Canvas.GetTop en cada elemento secundario para ver si esas propiedades adjuntas contienen un valor no predeterminado (el valor predeterminado es 0). Estos valores se usan para colocar absolutamente cada elemento secundario en el espacio de diseño disponible de Canvas según los valores específicos proporcionados por cada elemento secundario y confirmados mediante Arrange.

La salida tendrá un aspecto similar a este pseudocódigo.

protected override Size ArrangeOverride(Size finalSize)
{
    foreach (UIElement child in Children)
    {
        double x = (double) Canvas.GetLeft(child);
        double y = (double) Canvas.GetTop(child);
        child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
    }
    return base.ArrangeOverride(finalSize); 
    // real Canvas has more sophisticated sizing
}

Nota:

Para obtener más información sobre cómo funcionan los paneles, consulte Introducción a los paneles personalizados XAML.