Compartilhar via


Atributo ObservableProperty

O tipo ObservableProperty é um atributo que permite gerar propriedades observáveis de campos anotados. Sua finalidade é reduzir consideravelmente a quantidade de clichês necessários para definir propriedades observáveis.

Observação

Para funcionar, os campos anotados precisam estar em uma classe parcial com a infraestrutura INotifyPropertyChanged necessária. Se o tipo estiver aninhado, todos os tipos da árvore de sintaxe de declaração também deverão ser anotados como parciais. Não fazer isso resultará em erros de compilação, pois o gerador não poderá gerar uma declaração parcial diferente desse tipo com a propriedade observável solicitada.

APIs de plataforma:ObservableProperty, NotifyPropertyChangedFor, NotifyCanExecuteChangedFor, NotifyDataErrorInfo, NotifyPropertyChangedRecipients, ICommand, IRelayCommand, ObservableValidator, PropertyChangedMessage<T>, IMessenger

Como ele funciona

O atributo ObservableProperty pode ser usado para anotar um campo em um tipo parcial, assim como:

[ObservableProperty]
private string? name;

E gerará uma propriedade observável como esta:

public string? Name
{
    get => name;
    set => SetProperty(ref name, value);
}

Ele também fará isso com uma implementação otimizada, portanto, o resultado final será ainda mais rápido.

Observação

O nome da propriedade gerada será criado com base no nome do campo. O gerador pressupõe que o campo se chama lowerCamel, _lowerCamel ou m_lowerCamel e transformará isso em UpperCamel para seguir as convenção de nomenclatura do .NET adequadas. A propriedade resultante sempre terá acessadores públicos, mas o campo pode ser declarado com qualquer visibilidade (private é recomendado).

Executar o código após alterações

Na verdade, o código gerado é um pouco mais complexo que isso, e o motivo disso é que ele também expõe alguns métodos que você pode implementar para conectar-se à lógica de notificação e executar lógica adicional quando a propriedade está prestes a ser atualizada e logo após sua atualização, se necessário. Ou seja, o código gerado é semelhante a este:

public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            string? oldValue = name;
            OnNameChanging(value);
            OnNameChanging(oldValue, value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnNameChanged(oldValue, value);
            OnPropertyChanged();
        }
    }
}

partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);

partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? oldValue, string? newValue);

Isso permite que você implemente qualquer um desses métodos para injetar código adicional. Os dois primeiros são úteis sempre que você deseja executar alguma lógica que só precisa referenciar o novo valor para o qual a propriedade foi definida. Os outros dois são úteis sempre que você tem uma lógica mais complexa que também precisa atualizar algum estado no valor antigo e no novo que está sendo definido.

Por exemplo, aqui está um exemplo de como as duas primeiras sobrecargas podem ser usadas:

[ObservableProperty]
private string? name;

partial void OnNameChanging(string? value)
{
    Console.WriteLine($"Name is about to change to {value}");
}

partial void OnNameChanged(string? value)
{
    Console.WriteLine($"Name has changed to {value}");
}

E aqui está um exemplo de como as outras duas sobrecargas podem ser usadas:

[ObservableProperty]
private ChildViewModel? selectedItem;

partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
    if (oldValue is not null)
    {
        oldValue.IsSelected = true;
    }

    if (newValue is not null)
    {
        newValue.IsSelected = true;
    }
}

Você é livre para implementar qualquer número de métodos entre os que estão disponíveis ou nenhum deles. Se eles não forem implementados (ou se apenas um for), o compilador removerá todas as chamadas, portanto, não haverá nenhum impacto no desempenho para casos em que essa funcionalidade adicional não for necessária.

Observação

Os métodos gerados são métodos parciais sem implementação, o que significa que, se você optar por não implementá-los, não poderá especificar uma acessibilidade explícita para eles. Ou seja, as implementações desses métodos também devem ser declaradas como métodos partial e sempre terão acessibilidade privada implicitamente. Tentar adicionar uma acessibilidade explícita (por exemplo, adicionar public ou private) resultará em um erro, pois isso não é permitido em C#.

Notificar propriedades dependentes

Imagine que você tem uma propriedade FullName para a qual deseja gerar uma notificação sempre que Name é alterada. Você pode fazer isso usando o atributo NotifyPropertyChangedFor, da seguinte maneira:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;

Isso resultará em uma propriedade gerada equivalente a esta:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            OnPropertyChanged("FullName");
        }
    }
}

Notificar comandos dependentes

Imagine que você tem um comando cujo estado de execução depende do valor dessa propriedade. Ou seja, sempre que a propriedade é alterada, o estado de execução do comando deverá ser invalidado e computado novamente. Em outras palavras, ICommand.CanExecuteChanged deve ser gerado novamente. Você pode fazer isso usando o atributo NotifyCanExecuteChangedFor:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;

Isso resultará em uma propriedade gerada equivalente a esta:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            MyCommand.NotifyCanExecuteChanged();
        }
    }
}

Para que isso funcione, o comando de destino deve ser alguma propriedade IRelayCommand.

Solicitar validação de propriedade

Se a propriedade for declarada em um tipo que herda de ObservableValidator, também é possível anotá-la com atributos de validação e solicitar que o setter gerado dispare a validação dessa propriedade. Isso pode ser realizado com o atributo NotifyDataErrorInfo:

[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;

Isso resultará na geração da seguinte propriedade:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            ValidateProperty(value, "Value2");
        }
    }
}

Essa chamada ValidateProperty gerada validará a propriedade e atualizará o estado do objeto ObservableValidator, para que os componentes da interface do usuário possam reagir a ele e exibir os erros de validação adequadamente.

Observação

Propositalmente, somente atributos de campo herdados de ValidationAttribute serão encaminhados para a propriedade gerada. Isso é feito especificamente para dar suporte a cenários de validação de dados. Todos os outros atributos de campo serão ignorados, portanto, atualmente, não é possível adicionar atributos personalizados em um campo e fazer com que eles também sejam aplicados à propriedade gerada. Se isso for necessário (por exemplo, para controlar a serialização), considere usar uma propriedade manual tradicional.

Enviar mensagens de notificação

Se a propriedade for declarada em um tipo que herda de ObservableRecipient, você poderá usar o atributo NotifyPropertyChangedRecipients para instruir o gerador a inserir o código para enviar uma mensagem de propriedade alterada para a alteração de propriedade. Isso permitirá que os destinatários registrados respondam dinamicamente à alteração. Ou seja, considera este código:

[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;

Isso resultará na geração da seguinte propriedade:

public string? Name
{
    get => name;
    set
    {
        string? oldValue = name;

        if (SetProperty(ref name, value))
        {
            Broadcast(oldValue, value);
        }
    }
}

Essa chamada Broadcast gerada enviará um novo PropertyChangedMessage<T> usando a instância IMessenger em uso no viewmodel atual para todos os assinantes registrados.

Adicionar atributos personalizados

Em alguns casos, pode ser útil também ter alguns atributos personalizados sobre as propriedades geradas. Para conseguir isso, basta usar o destino [property: ] em listas de atributos em campos anotados e o Kit de Ferramentas MVVM encaminhará automaticamente esses atributos para as propriedades geradas.

Por exemplo, considere um campo como este:

[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;

Isso gerará uma propriedade Username, com esses dois atributos [JsonRequired] e [JsonPropertyName("name")] sobre ela. Você pode usar quantas listas de atributos direcionadas à propriedade desejar e todas elas serão encaminhadas para as propriedades geradas.

Exemplos

  • Confira o aplicativo de exemplo (para várias estruturas de interface do usuário) para ver o Kit de Ferramentas MVVM em ação.
  • Você também pode encontrar mais exemplos nos testes de unidade.