Atributo ObservableProperty

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

Note

Para funcionar, os campos anotados precisam estar em uma classe parcial com a infraestrutura necessária INotifyPropertyChanged . Se o tipo estiver aninhado, todos os tipos na á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, IRelayCommandObservableValidator, , PropertyChangedMessage<T>IMessenger

Como funciona

O ObservableProperty atributo pode ser usado para anotar um campo em um tipo parcial, da seguinte forma:

[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.

Note

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

Executando o código após as alterações

O código gerado é, na verdade, um pouco mais complexo do 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 ela ser atualizada, se necessário. Ou seja, o código gerado é, na verdade, 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 sobre o valor antigo e o 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 apenas qualquer número de métodos entre os que estão disponíveis ou nenhum deles. Se elas não forem implementadas (ou se apenas uma for), as chamadas inteiras serão removidas apenas pelo compilador, portanto, não haverá nenhum impacto no desempenho para casos em que essa funcionalidade adicional não for necessária.

Note

Os métodos gerados são métodos parciais sem implementação, o que significa que, se você optar por 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 simplesmente como métodos partial, e elas sempre terão acessibilidade privada implícita. Tentar adicionar uma acessibilidade explícita (por exemplo, adicionar public ou private) resultará em um erro, pois isso não é permitido em C#.

Notificando propriedades dependentes

Imagine que você tivesse uma propriedade FullName para a qual quisesse gerar uma notificação sempre que Name fosse alterada. Você pode fazer isso usando o NotifyPropertyChangedFor atributo, 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");
        }
    }
}

Notificando comandos dependentes

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

[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 IRelayCommand propriedade.

Solicitando validação de propriedade

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

[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 gerada ValidateProperty validará a propriedade e atualizará o estado do objeto ObservableValidator, para que os componentes da interface do usuário possam reagir a isso e exibir adequadamente quaisquer erros de validação.

Note

Por definição, somente os atributos de campo que herdam de ValidationAttribute serão repassados 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 adicionais 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, em vez disso, uma propriedade tradicional implementada manualmente.

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 também inserir código para enviar uma mensagem de alteração de propriedade quando a propriedade for alterada. Isso permitirá que os destinatários registrados reajam dinamicamente à alteração. Ou seja, considere 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 gerada Broadcast enviará uma nova PropertyChangedMessage<T> usando a instância IMessenger em uso no viewmodel atual a todos os assinantes registrados.

Adicionando atributos personalizados

Em alguns casos, pode ser útil também ter alguns atributos personalizados sobre as propriedades geradas. Para isso, você pode simplesmente usar o [property: ] target nas listas de atributos aplicadas a campos anotados, e o MVVM Toolkit 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 Username propriedade, com esses dois [JsonRequired] e [JsonPropertyName("name")] atributos sobre ela. Você pode usar quantas listas de atributos quiser direcionadas à propriedade, e todas elas serão repassadas 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.