Partilhar via


ObservableObject

O ObservableObject é uma classe base para objetos que são observáveis ao implementar as interfaces INotifyPropertyChanged e INotifyPropertyChanging. Pode ser usado como ponto de partida para todos os tipos de objetos que precisam dar suporte a notificações de alteração de propriedade.

APIs de plataforma: ObservableObject, TaskNotifier, TaskNotifier<T>

Como ele funciona

O ObservableObject tem os seguintes recursos principais:

  • Ele fornece uma implementação base para INotifyPropertyChanged e INotifyPropertyChanging, expondo os eventos PropertyChanged e PropertyChanging.
  • Fornece uma série de métodos SetProperty que podem ser usados para definir facilmente valores da propriedade de tipos herdados de ObservableObject, e para gerar automaticamente os eventos apropriados.
  • Fornece o método SetPropertyAndNotifyOnCompletion, que é análogo ao SetProperty mas com a capacidade de definir as propriedades Task e gerar os eventos de notificação automaticamente quando as tarefas atribuídas forem concluídas.
  • Expõe os métodos OnPropertyChanged e OnPropertyChanging, que podem ser substituídos em tipos derivados para personalizar como os eventos de notificação são gerados.

Propriedade simples

Aqui está um exemplo de como implementar o suporte à notificação para uma propriedade personalizada:

public class User : ObservableObject
{
    private string name;

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

O método SetProperty<T>(ref T, T, string) fornecido verifica o valor atual da propriedade e o atualiza se for diferente e também gera automaticamente os eventos relevantes. O nome da propriedade é capturado automaticamente por meio do uso do atributo [CallerMemberName], portanto, não há necessidade de especificar manualmente qual propriedade está sendo atualizada.

Encapsulamento de um modelo não observável

Um cenário comum, por exemplo, ao trabalhar com itens de banco de dados, é criar um modelo "associável" de empacotamento que retransmite as propriedades do modelo de banco de dados e gera notificações de alteração da propriedade quando necessário. Isso também é necessário quando se deseja injetar suporte de notificação em modelos que não implementam a interface INotifyPropertyChanged. ObservableObject fornece um método dedicado para tornar esse processo mais simples. Para o exemplo a seguir, User é um modelo que mapeia diretamente uma tabela de banco de dados, sem herdar de ObservableObject:

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

Nesse caso, usamos a sobrecarga SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string). A assinatura é um pouco mais complexa que a anterior - isso é necessário para deixar o código ainda ser extremamente eficiente mesmo que não tenhamos acesso a um campo de suporte como no cenário anterior. Podemos examinar detalhadamente cada parte dessa assinatura de método para entender a função dos diferentes componentes:

  • TModel é um argumento de tipo, que indica o tipo do modelo que estamos encapsulando. Nesse caso, será nossa classe User. Observe que não precisamos especificar isso explicitamente - o compilador C# inferirá isso automaticamente pela forma como invocamos o método SetProperty.
  • T é o tipo da propriedade que desejamos definir. Da mesma forma que TModel, isso é inferido automaticamente.
  • T oldValue é o primeiro parâmetro e, nesse caso, usamos user.Name para passar o valor atual dessa propriedade que estamos encapsulando.
  • T newValue é o novo valor a ser definido como a propriedade e aqui passamos value, que é o valor de entrada dentro do setter da propriedade.
  • TModel model é o modelo de destino que estamos encapsulando, nesse caso, passamos a instância armazenada no campo user.
  • Action<TModel, T> callback é uma função que será invocada se o novo valor da propriedade for diferente do atual e a propriedade precisar ser definida. Isso será feito por esta função de retorno de chamada, que recebe como entrada o modelo alvo e o novo valor da propriedade a ser definido. Nesse caso, apenas atribuímos o valor de entrada (que chamamos n) à propriedade Name (atribuindo u.Name = n). É importante evitar a captura de valores do escopo atual e interagir apenas com aqueles fornecidos como entrada para o retorno de chamada, pois isso permite que o compilador C# armazene em cache a função de retorno de chamada e execute uma série de melhorias de desempenho. É por isso que não acessamos diretamente aqui apenas o campo user ou o parâmetro value no setter, mas usamos apenas os parâmetros de entrada da expressão lambda.

O método SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) torna a criação dessas propriedades de encapsulamento extremamente simples, pois cuida da recuperação e da definição das propriedades de destino, fornecendo uma API extremamente compacta.

Observação

Em comparação com a implementação desse método que usa expressões LINQ, especificamente por meio de um parâmetro de tipo Expression<Func<T>> em vez dos parâmetros de estado e de retorno de chamada, as melhorias de desempenho que podem ser obtidas dessa maneira são realmente significativas. Em particular, esta versão é aproximadamente 200x mais rápida que aquela que usa expressões LINQ e não faz nenhuma alocação de memória.

Manipulação de propriedades Task<T>

Se uma propriedade for um Task também será necessário gerar o evento de notificação assim que a tarefa for concluída, para que as associações sejam atualizadas no momento certo, por exemplo, para exibir um indicador de carregamento ou outras informações de status na operação representada pela tarefa. ObservableObject tem uma API para este cenário:

public class MyModel : ObservableObject
{
    private TaskNotifier<int>? requestTask;

    public Task<int>? RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

Aqui, o método SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string) cuidará da atualização do campo de destino, do monitoramento da nova tarefa, se presente, e da geração do evento de notificação quando essa tarefa for concluída. Dessa forma, é possível apenas associar a uma propriedade de tarefa e ser notificado quando seu status for alterado. O TaskNotifier<T> é um tipo especial exposto pelo ObservableObject que encapsula uma instância de Task<T> de destino e habilita a lógica de notificação necessária para esse método. O tipo TaskNotifier também estará disponível para uso diretamente se você tiver apenas um Task geral.

Observação

O método SetPropertyAndNotifyOnCompletion destina-se a substituir o uso do tipo NotifyTaskCompletion<T> do pacote Microsoft.Toolkit. Se esse tipo estiver sendo usado, poderá ser substituído apenas pela propriedade interna Task (ou Task<TResult>) e o método SetPropertyAndNotifyOnCompletion poderá ser usado para definir seu valor e gerar alterações de notificação. Todas as propriedades expostas pelo tipo NotifyTaskCompletion<T> estão disponíveis diretamente em instâncias de Task.

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.