Partilhar via


Propriedades de dependência personalizadas

Aqui, explicamos como definir e implementar suas próprias propriedades de dependência para um aplicativo do Tempo de Execução do Windows usando C++, C# ou Visual Basic. Listamos os motivos pelos quais os desenvolvedores de aplicativos e autores de componentes podem querer criar propriedades de dependência personalizadas. Descrevemos as etapas de implementação de uma propriedade de dependência personalizada, bem como algumas práticas recomendadas que podem melhorar o desempenho, a usabilidade ou a versatilidade da propriedade de dependência.

Pré-requisitos

Presumimos que você tenha lido a visão geral das propriedades de dependência e que entenda as propriedades de dependência da perspectiva de um consumidor de propriedades de dependência existentes. Para seguir os exemplos neste tópico, você também deve entender XAML e saber como escrever um aplicativo Windows Runtime básico em C++, C# ou Visual Basic.

O que é uma propriedade de dependência?

Para dar suporte a estilo, associação de dados, animações e valores padrão para uma propriedade, ela deve ser implementada como uma propriedade de dependência. Os valores de propriedade de dependência não são armazenados como campos na classe, eles são armazenados pela estrutura xaml e são referenciados usando uma chave, que é recuperada quando a propriedade é registrada no sistema de propriedades do Tempo de Execução do Windows chamando o método DependencyProperty.Register. As propriedades de dependência podem ser usadas somente por tipos derivados de DependencyObject. Mas DependencyObject é bastante alto na hierarquia de classes, portanto, a maioria das classes destinadas à interface do usuário e ao suporte à apresentação pode dar suporte a propriedades de dependência. Para obter mais informações sobre propriedades de dependência e algumas das terminologias e convenções usadas para descrevê-las nesta documentação, consulte Visão geral das propriedades de dependência.

Exemplos de propriedades de dependência no Tempo de Execução do Windows são: Control.Background, FrameworkElement.Width e TextBox.Text, entre muitos outros.

A convenção é que cada propriedade de dependência exposta por uma classe tem uma propriedade somente leitura estática pública correspondente do tipo DependencyProperty que é exposta nessa mesma classe que fornece o identificador para a propriedade de dependência. O nome do identificador segue esta convenção: o nome da propriedade de dependência, com a cadeia de caracteres "Property" adicionada ao final do nome. Por exemplo, o identificador DependencyProperty correspondente para a propriedade Control.Background é Control.BackgroundProperty. O identificador armazena as informações sobre a propriedade de dependência conforme ela foi registrada e pode ser usado para outras operações que envolvem a propriedade de dependência, como chamar SetValue.

Wrappers de propriedade

As propriedades de dependência normalmente têm uma implementação de wrapper. Sem o wrapper, a única maneira de obter ou definir as propriedades seria usar os métodos utilitários de propriedade de dependência GetValue e SetValue e passar o identificador para eles como um parâmetro. Este é um uso bastante antinatural para algo que é ostensivamente uma propriedade. Mas com o wrapper, seu código e qualquer outro código que faça referência à propriedade de dependência podem usar uma sintaxe de propriedade de objeto direta que é natural para a linguagem que você está usando.

Se você implementar uma propriedade de dependência personalizada por conta própria e quiser que ela seja pública e fácil de chamar, defina os wrappers de propriedade também. Os wrappers de propriedade também são úteis para relatar informações básicas sobre a propriedade de dependência para processos de reflexão ou análise estática. Especificamente, o wrapper é onde você coloca atributos como ContentPropertyAttribute.

Quando implementar uma propriedade como uma propriedade de dependência

Sempre que você implementa uma propriedade pública de leitura/gravação em uma classe, desde que sua classe derive de DependencyObject, você tem a opção de fazer sua propriedade funcionar como uma propriedade de dependência. Às vezes, a técnica típica de apoiar sua propriedade com um campo privado é adequada. Definir sua propriedade personalizada como uma propriedade de dependência nem sempre é necessário ou apropriado. A escolha dependerá dos cenários que você pretende que sua propriedade suporte.

Você pode considerar a implementação de sua propriedade como uma propriedade de dependência quando quiser que ela dê suporte a um ou mais desses recursos do Tempo de Execução do Windows ou de aplicativos do Tempo de Execução do Windows:

  • Definindo a propriedade por meio de um Style
  • Atuando como propriedade de destino válida para vinculação de dados com {Binding}
  • Suporte a valores animados por meio de um Storyboard
  • Relatar quando o valor da propriedade foi alterado por:
    • Ações tomadas pelo próprio sistema de propriedade
    • O ambiente dos
    • Ações do usuário
    • Estilos de leitura e escrita

Lista de verificação para definir uma propriedade de dependência

A definição de uma propriedade de dependência pode ser pensada como um conjunto de conceitos. Esses conceitos não são necessariamente etapas processuais, pois vários conceitos podem ser abordados em uma única linha de código na implementação. Esta lista fornece apenas uma visão geral rápida. Explicaremos cada conceito com mais detalhes posteriormente neste tópico e mostraremos o código de exemplo em várias linguagens.

  • Registre o nome da propriedade no sistema de propriedades (chame Register), especificando um tipo de proprietário e o tipo do valor da propriedade.
    • Há um parâmetro necessário para Register que espera metadados de propriedade. Especifique null para isso ou, se quiser um comportamento alterado de propriedade ou um valor padrão baseado em metadados que possa ser restaurado chamando ClearValue, especifique uma instância de PropertyMetadata.
  • Defina um identificador DependencyProperty como um membro de propriedade public static readonly no tipo owner.
  • Defina uma propriedade wrapper, seguindo o modelo de acessador de propriedade usado na linguagem que você está implementando. O nome da propriedade wrapper deve corresponder à cadeia de caracteres de nome que você usou em Register. Implemente os acessadores get e set para conectar o wrapper com a propriedade de dependência que ele encapsula, chamando GetValue e SetValue e passando o identificador de sua própria propriedade como um parâmetro.
  • (Opcional) Coloque atributos como ContentPropertyAttribute no wrapper.

Observação

Se você estiver definindo uma propriedade anexada personalizada, geralmente omite o wrapper. Em vez disso, você escreve um estilo diferente de acessador que um processador XAML pode usar. Consulte Propriedades anexadas personalizadas

Cadastrando a propriedade

Para que sua propriedade seja uma propriedade de dependência, você deve registrá-la em um repositório de propriedades mantido pelo sistema de propriedades do Tempo de Execução do Windows. Para registrar a propriedade, chame o método Register.

Para linguagens Microsoft .NET (C# e Microsoft Visual Basic), você chama Register dentro do corpo da sua classe (dentro da classe, mas fora de qualquer definição de membro). O identificador é fornecido pela chamada do método Register , como o valor retornado. A chamada Register normalmente é feita como um construtor estático ou como parte da inicialização de uma propriedade pública estática somente leitura do tipo DependencyProperty como parte de sua classe. Essa propriedade expõe o identificador da propriedade de dependência. Aqui estão exemplos da chamada Register.

Observação

Registrar a propriedade de dependência como parte da definição da propriedade do identificador é a implementação típica, mas você também pode registrar uma propriedade de dependência no construtor estático da classe. Essa abordagem pode fazer sentido se você precisar de mais de uma linha de código para inicializar a propriedade de dependência.

Para C++/CX, você tem opções de como dividir a implementação entre o cabeçalho e o arquivo de código. A divisão típica é declarar o próprio identificador como propriedade estática pública no cabeçalho, com uma implementação get , mas sem conjunto. A implementação get refere-se a um campo privado, que é uma instância DependencyProperty não inicializada. Você também pode declarar os wrappers e as implementações get e set do wrapper. Nesse caso, o cabeçalho inclui alguma implementação mínima. Se o wrapper precisar de atribuição do Tempo de Execução do Windows, atribua no cabeçalho também. Coloque a chamada Register no arquivo de código, dentro de uma função auxiliar que só é executada quando o aplicativo é inicializado pela primeira vez. Use o valor retornado de Register para preencher os identificadores estáticos, mas não inicializados, que você declarou no cabeçalho, que você definiu inicialmente como nullptr no escopo raiz do arquivo de implementação.

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  nameof(Label),
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null)
);
Public Shared ReadOnly LabelProperty As DependencyProperty = 
    DependencyProperty.Register("Label", 
      GetType(String), 
      GetType(ImageWithLabelControl), 
      New PropertyMetadata(Nothing))
// ImageWithLabelControl.idl
namespace ImageWithLabelControlApp
{
    runtimeclass ImageWithLabelControl : Windows.UI.Xaml.Controls.Control
    {
        ImageWithLabelControl();
        static Windows.UI.Xaml.DependencyProperty LabelProperty{ get; };
        String Label;
    }
}

// ImageWithLabelControl.h
...
struct ImageWithLabelControl : ImageWithLabelControlT<ImageWithLabelControl>
{
...
public:
    static Windows::UI::Xaml::DependencyProperty LabelProperty()
    {
        return m_labelProperty;
    }

private:
    static Windows::UI::Xaml::DependencyProperty m_labelProperty;
...
};

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr }
);
...
//.h file
//using namespace Windows::UI::Xaml::Controls;
//using namespace Windows::UI::Xaml::Interop;
//using namespace Windows::UI::Xaml;
//using namespace Platform;

public ref class ImageWithLabelControl sealed : public Control
{
private:
    static DependencyProperty^ _LabelProperty;
...
public:
    static void RegisterDependencyProperties();
    static property DependencyProperty^ LabelProperty
    {
        DependencyProperty^ get() {return _LabelProperty;}
    }
...
};

//.cpp file
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml.Interop;

DependencyProperty^ ImageWithLabelControl::_LabelProperty = nullptr;

// This function is called from the App constructor in App.xaml.cpp
// to register the properties
void ImageWithLabelControl::RegisterDependencyProperties()
{ 
    if (_LabelProperty == nullptr)
    { 
        _LabelProperty = DependencyProperty::Register(
          "Label", Platform::String::typeid, ImageWithLabelControl::typeid, nullptr);
    } 
}

Observação

Para o código C++/CX, o motivo pelo qual você tem um campo privado e uma propriedade pública somente leitura que exibe a DependencyProperty é para que outros chamadores que usam sua propriedade de dependência também possam usar APIs de utilitário do sistema de propriedades que exigem que o identificador seja público. Se você mantiver o identificador privado, as pessoas não poderão usar essas APIs de utilitário. Exemplos dessa API e cenários incluem GetValue ou SetValue por escolha, ClearValue, GetAnimationBaseValue, SetBinding e Setter.Property. Você não pode usar um campo público para isso, pois as regras de metadados do Tempo de Execução do Windows não permitem campos públicos.

Convenções de nome de propriedade de dependência

Há convenções de nomenclatura para propriedades de dependência; segui-los em todas as circunstâncias, exceto em circunstâncias excepcionais. A propriedade de dependência em si tem um nome básico ("Label" no exemplo anterior) que é fornecido como o primeiro parâmetro de Register. O nome deve ser exclusivo em cada tipo de registro e o requisito de exclusividade também se aplica a todos os membros herdados. As propriedades de dependência herdadas por meio de tipos base já são consideradas parte do tipo de registro; nomes de propriedades herdadas não podem ser registrados novamente.

Aviso

Embora o nome fornecido aqui possa ser qualquer identificador de cadeia de caracteres válido na programação para a linguagem de sua escolha, você geralmente deseja definir sua propriedade de dependência em XAML também. Para ser definido em XAML, o nome da propriedade escolhido deve ser um nome XAML válido. Para obter mais informações, consulte Visão geral do XAML.

Ao criar a propriedade do identificador, combine o nome da propriedade como você a registrou com o sufixo "Property" ("LabelProperty", por exemplo). Essa propriedade é o identificador da propriedade de dependência e é usada como uma entrada para as chamadas SetValue e GetValue que você faz em seus próprios wrappers de propriedade. Ele também é usado pelo sistema de propriedades e outros processadores XAML, como {x:Bind}

Implementando o wrapper

Seu wrapper de propriedade deve chamar GetValue na implementação get e SetValue na implementação set.

Aviso

Em todas as circunstâncias, exceto em circunstâncias excepcionais, suas implementações de wrapper devem executar apenas as operações GetValue e SetValue. Caso contrário, você obterá um comportamento diferente quando sua propriedade for definida por meio de XAML e quando ela for definida por meio de código. Para maior eficiência, o analisador XAML ignora wrappers ao definir propriedades de dependência; e se comunica com o repositório de backup por meio de SetValue.

public String Label
{
    get { return (String)GetValue(LabelProperty); }
    set { SetValue(LabelProperty, value); }
}
Public Property Label() As String
    Get
        Return DirectCast(GetValue(LabelProperty), String) 
    End Get 
    Set(ByVal value As String)
        SetValue(LabelProperty, value)
    End Set
End Property
// ImageWithLabelControl.h
...
winrt::hstring Label()
{
    return winrt::unbox_value<winrt::hstring>(GetValue(m_labelProperty));
}

void Label(winrt::hstring const& value)
{
    SetValue(m_labelProperty, winrt::box_value(value));
}
...
//using namespace Platform;
public:
...
  property String^ Label
  {
    String^ get() {
      return (String^)GetValue(LabelProperty);
    }
    void set(String^ value) {
      SetValue(LabelProperty, value);
    }
  }

Metadados de propriedade para uma propriedade de dependência personalizada

Quando os metadados de propriedade são atribuídos a uma propriedade de dependência, os mesmos metadados são aplicados a essa propriedade para cada instância do tipo property-owner ou suas subclasses. Nos metadados da propriedade, você pode especificar dois comportamentos:

  • Um valor padrão que o sistema de propriedades atribui a todos os casos da propriedade.
  • Um método de retorno de chamada estático que é invocado automaticamente no sistema de propriedades sempre que uma alteração de valor de propriedade é detectada.

Chamando Register com metadados de propriedade

Nos exemplos anteriores de chamada de DependencyProperty.Register, passamos um valor nulo para o parâmetro propertyMetadata. Para permitir que uma propriedade de dependência forneça um valor padrão ou use um retorno de chamada de propriedade alterada, você deve definir uma instância de PropertyMetadata que forneça um ou ambos os recursos.

Normalmente, você fornece um PropertyMetadata como uma instância criada embutida, dentro dos parâmetros para DependencyProperty.Register.

Observação

Se você estiver definindo uma implementação CreateDefaultValueCallback, deverá usar o método utilitário PropertyMetadata.Create em vez de chamar um construtor PropertyMetadata para definir a instância PropertyMetadata.

Este próximo exemplo modifica os exemplos de DependencyProperty.Register mostrados anteriormente referenciando uma instância de PropertyMetadata com um valor PropertyChangedCallback. A implementação do retorno de chamada "OnLabelChanged" será mostrada posteriormente nesta seção.

public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
  nameof(Label),
  typeof(String),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null,new PropertyChangedCallback(OnLabelChanged))
);
Public Shared ReadOnly LabelProperty As DependencyProperty =
    DependencyProperty.Register("Label",
      GetType(String),
      GetType(ImageWithLabelControl),
      New PropertyMetadata(
        Nothing, new PropertyChangedCallback(AddressOf OnLabelChanged)))
// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ nullptr, Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...
DependencyProperty^ ImageWithLabelControl::_LabelProperty =
    DependencyProperty::Register("Label",
    Platform::String::typeid,
    ImageWithLabelControl::typeid,
    ref new PropertyMetadata(nullptr,
      ref new PropertyChangedCallback(&ImageWithLabelControl::OnLabelChanged))
    );

Valor padrão

Você pode especificar um valor padrão para uma propriedade de dependência, de modo que a propriedade sempre retorne um valor padrão específico quando não for definida. Esse valor pode ser diferente do valor padrão inerente para o tipo dessa propriedade.

Se um valor padrão não for especificado, o valor padrão de uma propriedade de dependência será nulo para um tipo de referência ou o padrão do tipo para um tipo de valor ou primitivo de linguagem (por exemplo, 0 para um inteiro ou uma cadeia de caracteres vazia para uma cadeia de caracteres). O principal motivo para estabelecer um valor padrão é que esse valor é restaurado quando você chama ClearValue na propriedade. Estabelecer um valor padrão por propriedade pode ser mais conveniente do que estabelecer valores padrão em construtores, especialmente para tipos de valor. No entanto, para tipos de referência, certifique-se de que o estabelecimento de um valor padrão não crie um padrão singleton não intencional. Para obter mais informações, consulte Práticas recomendadas mais adiante neste tópico

// ImageWithLabelControl.cpp
...
Windows::UI::Xaml::DependencyProperty ImageWithLabelControl::m_labelProperty =
    Windows::UI::Xaml::DependencyProperty::Register(
        L"Label",
        winrt::xaml_typename<winrt::hstring>(),
        winrt::xaml_typename<ImageWithLabelControlApp::ImageWithLabelControl>(),
        Windows::UI::Xaml::PropertyMetadata{ winrt::box_value(L"default label"), Windows::UI::Xaml::PropertyChangedCallback{ &ImageWithLabelControl::OnLabelChanged } }
);
...

Observação

Não se registre com um valor padrão de UnsetValue. Se você fizer isso, isso confundirá os consumidores de imóveis e terá consequências não intencionais dentro do sistema de propriedades.

CreateDefaultValueCallback

Em alguns cenários, você está definindo propriedades de dependência para objetos que são usados em mais de um thread de interface do usuário. Esse pode ser o caso se você estiver definindo um objeto de dados usado por vários aplicativos ou um controle usado em mais de um aplicativo. Você pode habilitar a troca do objeto entre diferentes threads de interface do usuário fornecendo uma implementação CreateDefaultValueCallback em vez de uma instância de valor padrão, que está vinculada ao thread que registrou a propriedade. Basicamente, um CreateDefaultValueCallback define uma fábrica para valores padrão. O valor retornado por CreateDefaultValueCallback está sempre associado ao thread CreateDefaultValueCallback da interface do usuário atual que está usando o objeto.

Para definir metadados que especificam um CreateDefaultValueCallback, você deve chamar PropertyMetadata.Create para retornar uma instância de metadados; os construtores PropertyMetadata não têm uma assinatura que inclua um parâmetro CreateDefaultValueCallback.

O padrão de implementação típico para um CreateDefaultValueCallback é criar uma nova classe DependencyObject, definir o valor de propriedade específico de cada propriedade do DependencyObject para o padrão pretendido e, em seguida, retornar a nova classe como uma referência de Object por meio do valor retornado do método CreateDefaultValueCallback.

Método de retorno de chamada com alteração de propriedade

Você pode definir um método de retorno de chamada de propriedade alterada para definir as interações de sua propriedade com outras propriedades de dependência ou para atualizar uma propriedade interna ou um estado de seu objeto sempre que a propriedade for alterada. Se o retorno de chamada for invocado, o sistema de propriedades determinou que há uma alteração efetiva no valor da propriedade. Como o método de retorno de chamada é estático, o parâmetro d do retorno de chamada é importante porque informa qual instância da classe relatou uma alteração. Uma implementação típica usa a propriedade NewValue dos dados do evento e processa esse valor de alguma maneira, geralmente executando alguma outra alteração no objeto passado como d. Respostas adicionais a uma alteração de propriedade são rejeitar o valor relatado por NewValue, restaurar OldValue ou definir o valor como uma restrição programática aplicada ao NewValue.

O próximo exemplo mostra uma implementação de PropertyChangedCallback . Ele implementa o método que você viu referenciado nos exemplos anteriores de Register, como parte dos argumentos de construção para o PropertyMetadata. O cenário abordado por esse retorno de chamada é que a classe também tem uma propriedade somente leitura calculada chamada "HasLabelValue" (implementação não mostrada). Sempre que a propriedade "Label" é reavaliada, esse método de retorno de chamada é invocado e o retorno de chamada permite que o valor calculado dependente permaneça em sincronização com as alterações na propriedade de dependência.

private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    ImageWithLabelControl iwlc = d as ImageWithLabelControl; //null checks omitted
    String s = e.NewValue as String; //null checks omitted
    if (s == String.Empty)
    {
        iwlc.HasLabelValue = false;
    } else {
        iwlc.HasLabelValue = true;
    }
}
    Private Shared Sub OnLabelChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim iwlc As ImageWithLabelControl = CType(d, ImageWithLabelControl) ' null checks omitted
        Dim s As String = CType(e.NewValue,String) ' null checks omitted
        If s Is String.Empty Then
            iwlc.HasLabelValue = False
        Else
            iwlc.HasLabelValue = True
        End If
    End Sub
void ImageWithLabelControl::OnLabelChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto iwlc{ d.as<ImageWithLabelControlApp::ImageWithLabelControl>() };
    auto s{ winrt::unbox_value<winrt::hstring>(e.NewValue()) };
    iwlc.HasLabelValue(s.size() != 0);
}
static void OnLabelChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    ImageWithLabelControl^ iwlc = (ImageWithLabelControl^)d;
    Platform::String^ s = (Platform::String^)(e->NewValue);
    if (s->IsEmpty()) {
        iwlc->HasLabelValue=false;
    }
}

Comportamento alterado de propriedade para estruturas e enumerações

Se o tipo de um DependencyProperty for uma enumeração ou uma estrutura, o retorno de chamada poderá ser invocado mesmo que os valores internos da estrutura ou o valor da enumeração não tenham sido alterados. Isso é diferente de um sistema primitivo, como uma cadeia de caracteres, em que só é invocado se o valor for alterado. Esse é um efeito colateral das operações de caixa e descaixa nesses valores que são feitas internamente. Se você tiver um método PropertyChangedCallback para uma propriedade em que seu valor é uma enumeração ou estrutura, precisará comparar o OldValue e o NewValue convertendo os valores por conta própria e usando os operadores de comparação sobrecarregados que estão disponíveis para os valores agora convertidos. Ou, se esse operador não estiver disponível (o que pode ser o caso de uma estrutura personalizada), talvez seja necessário comparar os valores individuais. Normalmente, você optaria por não fazer nada se o resultado for que os valores não foram alterados.

private static void OnVisibilityValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    if ((Visibility)e.NewValue != (Visibility)e.OldValue)
    {
        //value really changed, invoke your changed logic here
    } // else this was invoked because of boxing, do nothing
}
Private Shared Sub OnVisibilityValueChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
    If CType(e.NewValue,Visibility) != CType(e.OldValue,Visibility) Then
        '  value really changed, invoke your changed logic here
    End If
    '  else this was invoked because of boxing, do nothing
End Sub
static void OnVisibilityValueChanged(Windows::UI::Xaml::DependencyObject const& d, Windows::UI::Xaml::DependencyPropertyChangedEventArgs const& e)
{
    auto oldVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.OldValue()) };
    auto newVisibility{ winrt::unbox_value<Windows::UI::Xaml::Visibility>(e.NewValue()) };

    if (newVisibility != oldVisibility)
    {
        // The value really changed; invoke your property-changed logic here.
    }
    // Otherwise, OnVisibilityValueChanged was invoked because of boxing; do nothing.
}
static void OnVisibilityValueChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    if ((Visibility)e->NewValue != (Visibility)e->OldValue)
    {
        //value really changed, invoke your changed logic here
    } 
    // else this was invoked because of boxing, do nothing
    }
}

Práticas recomendadas

Lembre-se das seguintes considerações como práticas recomendadas ao definir sua propriedade de dependência personalizada.

DependencyObject e threading

Todas as instâncias de DependencyObject devem ser criadas no thread da interface do usuário associado à janela atual mostrada por um aplicativo do Tempo de Execução do Windows. Embora cada DependencyObject deva ser criado no thread principal da interface do usuário, os objetos podem ser acessados usando uma referência de dispatcher de outros threads, chamando Dispatcher.

Os aspectos de threading de DependencyObject são relevantes porque geralmente significa que somente o código executado no thread da interface do usuário pode alterar ou até mesmo ler o valor de uma propriedade de dependência. Problemas de threading geralmente podem ser evitados no código de interface do usuário típico que faz uso correto de padrões assíncronos e threads de trabalho em segundo plano. Normalmente, você só se depara com problemas de threading relacionados a DependencyObject se estiver definindo seus próprios tipos de DependencyObject e tentar usá-los para fontes de dados ou outros cenários em que um DependencyObject não é necessariamente apropriado.

Evitando singletons não intencionais

Um singleton não intencional pode acontecer se você estiver declarando uma propriedade de dependência que usa um tipo de referência e chamar um construtor para esse tipo de referência como parte do código que estabelece seus PropertyMetadata. O que acontece é que todos os usos da propriedade de dependência compartilham apenas uma instância de PropertyMetadata e, portanto, tentam compartilhar o único tipo de referência que você construiu. Todas as subpropriedades desse tipo de valor que você define por meio de sua propriedade de dependência se propagam para outros objetos de maneiras que você pode não ter pretendido.

Você pode usar construtores de classe para definir valores iniciais para uma propriedade de dependência de tipo de referência se quiser um valor não nulo, mas lembre-se de que isso seria considerado um valor local para fins de visão geral das propriedades de dependência. Pode ser mais apropriado usar um modelo para essa finalidade, se sua classe der suporte a modelos. Outra maneira de evitar um padrão singleton, mas ainda fornecer um padrão útil, é expor uma propriedade estática no tipo de referência que fornece um padrão adequado para os valores dessa classe.

Propriedades de dependência de tipo de coleção

As propriedades de dependência do tipo de coleção apresentam algumas outras questões de implementação a serem consideradas.

As propriedades de dependência de tipo de coleção são relativamente raras na API do Tempo de Execução do Windows. Na maioria dos casos, você pode usar coleções em que os itens são uma subclasse DependencyObject , mas a propriedade de coleção em si é implementada como uma propriedade CLR ou C++ convencional. Isso ocorre porque as coleções não se adequam necessariamente a alguns cenários típicos em que as propriedades de dependência estão envolvidas. Por exemplo:

  • Normalmente, você não anima uma coleção.
  • Normalmente, você não preenche previamente os itens em uma coleção com estilos ou um modelo.
  • Embora a associação a coleções seja um cenário importante, uma coleção não precisa ser uma propriedade de dependência para ser uma fonte de associação. Para destinos de associação, é mais comum usar subclasses de ItemsControl ou DataTemplate para dar suporte a itens de coleção ou usar padrões de modelo de exibição. Para obter mais informações sobre a associação de e para coleções, consulte Associação de dados em detalhes.
  • As notificações de alterações de coleção são melhor tratadas por meio de interfaces como INotifyPropertyChanged ou INotifyCollectionChanged, ou derivando o tipo de coleção de ObservableCollection<T>.

No entanto, existem cenários para propriedades de dependência de tipo de coleção. As próximas três seções fornecem algumas diretrizes sobre como implementar uma propriedade de dependência de tipo de coleção.

Inicializando a coleção

Ao criar uma propriedade de dependência, você pode estabelecer um valor padrão por meio de metadados de propriedade de dependência. Mas tenha cuidado para não usar uma coleção estática singleton como o valor padrão. Em vez disso, você deve definir deliberadamente o valor da coleção como uma coleção exclusiva (instância) como parte da lógica do construtor de classe para a classe proprietária da propriedade da coleção.

// WARNING - DO NOT DO THIS
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
  nameof(Items),
  typeof(IList<object>),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(new List<object>())
);

// DO THIS Instead
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
  nameof(Items),
  typeof(IList<object>),
  typeof(ImageWithLabelControl),
  new PropertyMetadata(null)
);

public ImageWithLabelControl()
{
    // Need to initialize in constructor instead
    Items = new List<object>();
}

Um DependencyProperty e o valor padrão de seu PropertyMetadata fazem parte da definição estática do DependencyProperty. Ao fornecer um valor de coleção padrão (ou outra instância) como o valor padrão, ele será compartilhado entre todas as instâncias de sua classe, em vez de cada classe ter sua própria coleção, como normalmente seria desejado.

Notificações de alteração

Definir a coleção como uma propriedade de dependência não fornece automaticamente uma notificação de alteração para os itens na coleção em virtude do sistema de propriedades que invoca o método de retorno de chamada "PropertyChanged". Se você quiser notificações para coleções ou itens de coleção, por exemplo, para um cenário de associação de dados, implemente a interface INotifyPropertyChanged ou INotifyCollectionChanged . Para obter mais informações, consulte Vinculação de dados em detalhes.

Considerações de segurança de propriedade de dependência

Declare propriedades de dependência como propriedades públicas. Declare identificadores de propriedade de dependência como membros somente leitura estáticos públicos. Mesmo que você tente declarar outros níveis de acesso permitidos por uma linguagem (como protected), uma propriedade de dependência sempre poderá ser acessada por meio do identificador em combinação com as APIs do sistema de propriedades. Declarar o identificador de propriedade de dependência como interno ou privado não funcionará, pois o sistema de propriedades não poderá operar corretamente.

As propriedades do wrapper são realmente apenas por conveniência, os mecanismos de segurança aplicados aos wrappers podem ser ignorados chamando GetValue ou SetValue. Portanto, mantenha as propriedades do wrapper públicas; caso contrário, você apenas torna sua propriedade mais difícil para chamadores legítimos usarem sem fornecer nenhum benefício real de segurança.

O Tempo de Execução do Windows não fornece uma maneira de registrar uma propriedade de dependência personalizada como somente leitura.

Propriedades de dependência e construtores de classe

Há um princípio geral de que os construtores de classe não devem chamar métodos virtuais. Isso ocorre porque os construtores podem ser chamados para realizar a inicialização base de um construtor de classe derivada e a entrada do método virtual por meio do construtor pode ocorrer quando a instância do objeto que está sendo construída ainda não foi completamente inicializada. Quando você deriva de qualquer classe que já deriva de DependencyObject, lembre-se de que o próprio sistema de propriedades chama e expõe métodos virtuais internamente como parte de seus serviços. Para evitar possíveis problemas com a inicialização em tempo de execução, não defina valores de propriedade de dependência em construtores de classes.

Registrando as propriedades de dependência para aplicativos C++/CX

A implementação para registrar uma propriedade no C++/CX é mais complicada do que no C#, tanto por causa da separação em cabeçalho e arquivo de implementação quanto porque a inicialização no escopo raiz do arquivo de implementação é uma prática ruim. (As extensões de componente do Visual C++ (C++/CX) colocam o código do inicializador estático do escopo raiz diretamente em DllMain, enquanto os compiladores C# atribuem os inicializadores estáticos às classes e, assim, evitam problemas de bloqueio de carga DllMain .). A prática recomendada aqui é declarar uma função auxiliar que faz todo o registro de propriedade de dependência para uma classe, uma função por classe. Em seguida, para cada classe personalizada que seu aplicativo consome, você precisará fazer referência à função de registro auxiliar exposta por cada classe personalizada que deseja usar. Chame cada função de registro auxiliar uma vez como parte do construtor Application (App::App()), antes de .InitializeComponent Esse construtor só é executado quando o aplicativo é realmente referenciado pela primeira vez, ele não será executado novamente se um aplicativo suspenso for retomado, por exemplo. Além disso, como visto no exemplo de registro C++ anterior, a verificação nullptr em torno de cada chamada Register é importante: é seguro que nenhum chamador da função possa registrar a propriedade duas vezes. Uma segunda chamada de registro provavelmente travaria seu aplicativo sem essa verificação, pois o nome da propriedade seria uma duplicata. Você pode ver esse padrão de implementação no exemplo de controles personalizados e de usuário XAML se examinar o código da versão C++/CX do exemplo.