Implementando INotifyPropertyChanged da maneira mais fácil

Concluído

Se você seguiu as lições anteriores, pode pensar que implementar a vinculação de dados é um esforço muito grande. Por que passar por todo o trabalho de implementar INotifyPropertyChanged, disparar eventos para a esquerda e para a direita, quando você poderia simplesmente usar TimeTextBlock.Text = DateTime.Now.ToLongTime() para exibir o tempo? E é verdade, neste caso simples, a vinculação de dados parece exagerada.

No entanto, a vinculação de dados é capaz de muito mais. Ele pode transferir dados em ambas as direções entre a interface do usuário e o código, exibir listas de itens e dar suporte à edição de dados. Tudo isso com uma arquitetura que oferece uma separação clara dos dados em que a lógica do seu aplicativo trabalha e a apresentação dos dados.

Mas como podemos reduzir a quantidade de código que o desenvolvedor tem que escrever? Ninguém quer inserir dez linhas de código para cada propriedade que precisa declarar. Felizmente, podemos extrair a funcionalidade comum e reduzir os setters de propriedade a uma única linha de código. Esta lição mostra como.

O objetivo

Nosso objetivo é mover todo o encanamento para implementar a interface para uma classe separada, para simplificar a criação de uma propriedade que possa notificar a INotifyPropertyChanged interface do usuário quando ela for alterada. Como lembrete, aqui está o código que queremos simplificar:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set
    {
        if (value != _isNameNeeded)
        {
            _isNameNeeded = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        }
    }
}

Propriedades automáticas (como public bool IsNameNeeded { get; set;}) não podem ser usadas aqui, porque precisamos fazer algo no setter. Portanto, não há muito a ser feito sobre o campo de apoio, a linha de declaração de propriedade. Usando recursos modernos de C#, poderíamos alterar o getter para get => _isNameNeeded;, mas isso salva apenas alguns pressionamentos de teclas. Então, precisamos focar nossa atenção no setter da propriedade. Podemos transformar isso numa única linha?

A ObservableObject classe

Podemos criar uma nova classe base: ObservableObject. É chamado de observável porque pode ser observado pela interface do usuário, usando a INotifyPropertyChanged interface. Os dados e a lógica são hospedados em classes que herdam dele, e a interface do usuário também está vinculada a instâncias dessas classes herdadas.

1. Crie a ObservableObject classe

Vamos criar uma nova classe chamada ObservableObject. Clique com o botão direito do mouse no projeto no Gerenciador de Soluções, selecione Adicionar / Classe e digite ObservableObject como o DatabindingSample nome da classe. Selecione Adicionar para criar a classe.

1. Crie a ObservableObject classe

Vamos criar uma nova classe chamada ObservableObject. Clique com o botão direito do DatabindingSampleWPF mouse no projeto no Gerenciador de Soluções, selecione Adicionar / Classe e digite ObservableObject como o nome da classe. Selecione Adicionar para criar a classe.

Screenshot of Visual Studio showing the Add New Item dialog with a Visual C# class type selected.

2. Implementar a INotifyPropertyChanged interface

Em seguida, temos que implementar a INotifyPropertyChanged interface e tornar nossa classe pública. Altere a assinatura da classe para que ela tenha esta aparência:

public class ObservableObject : INotifyPropertyChanged

Visual Studio indica que há vários problemas com INotifyPropertyChangedo . Ele reside em um namespace não referenciado. Vamos adicioná-lo como mostrado aqui.

using System.ComponentModel;

Em seguida, temos que implementar a interface. Adicione esta linha dentro do corpo da classe.

public event PropertyChangedEventHandler? PropertyChanged;

3. O RaisePropertyChanged método

Em lições anteriores, muitas vezes levantamos o PropertyChangedEvent nosso código, mesmo fora dos setters de propriedade. Embora o C# moderno e o operador nulo-condicional ou (?.) nos permitissem fazer isso em uma linha, ainda podemos simplificar criando uma função de conveniência como esta:

protected void RaisePropertyChanged(string? propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Então, agora, nas classes que herdam do ObservableObject, tudo o que temos que fazer para levantar o evento é o PropertyChanged seguinte:

RaisePropertyChanged(nameof(MyProperty));

4. O Set<T> método

Mas o que podemos fazer com o padrão setter que verifica se o valor é o mesmo que era, define o valor se não e aumenta o PropertyChanged evento? Idealmente, gostaríamos de transformá-lo em uma linha única, assim:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }  // Just one line!
}

Não pode ficar mais simples do que isso. Chamamos uma função, passamos uma referência para o campo de suporte da propriedade e definimos o novo valor. Então, como é esse Set método?

protected bool Set<T>(
    ref T field,
    T newValue,
    [CallerMemberName] string? propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, newValue))
    {
        return false;
    }

    field = newValue;
    RaisePropertyChanged(propertyName);
    return true;
}

Copie o código anterior para o corpo da ObservableObject classe. Para [CallerMemberName], você também precisa adicionar a seguinte linha à parte superior do arquivo:

using System.Runtime.CompilerServices;

Há muita magia avançada de C# e compilador acontecendo aqui. Uma visão mais detalhada.

Set<T> é um método genérico, ajudando o compilador a certificar-se de que o campo de suporte e o valor são do mesmo tipo. O terceiro parâmetro do método, o propertyName, é decorado [CallerMemberName] pelo atributo. Se não definirmos o quando chamar o propertyName método, ele pegará o nome do membro chamador e o colocará lá durante o tempo de compilação. Assim, se chamarmos Set a partir do setter do IsNameNeeded método, o compilador coloca a string literal, "IsNameNeeded", como o terceiro parâmetro. Não há necessidade de hardcode strings ou mesmo usar nameof()!

Em seguida, o método invoca EqualityComparer<T>.Default.Equals para comparar o valor atual e o Set novo valor do campo. Se os valores antigo e novo forem iguais, o Set método retornará false. Caso contrário, o campo de suporte é definido como o novo valor e o evento é gerado antes de PropertyChanged retornar true. Você pode usar o valor de retorno do Set método para determinar se o valor foi alterado.

Com a ObservableObject aula implementada, vamos ver como podemos usá-la em nosso aplicativo!

5. Crie a MainPageLogic classe

No início desta lição, movemos todos os nossos dados e lógica para fora da MainPage classe e para uma classe que herda do ObservableObject.

Vamos criar uma nova classe, chamada MainPageLogic. Clique com o botão direito do mouse no projeto no Gerenciador de Soluções, selecione Adicionar / Classe e digite MainPageLogic como o DatabindingSample nome da classe. Selecione Adicionar para criar a classe.

Altere a assinatura da classe, para que ela seja pública e herde do ObservableObject.

public class MainPageLogic : ObservableObject
{
}

6. Mova o recurso de relógio para a MainPageLogic classe

O código para o recurso de relógio consiste em três partes: o _timer campo, a configuração do DispatcherTimer no construtor e a CurrentTime propriedade. Aqui está o código como o deixamos na segunda lição:

private DispatcherTimer _timer;

public MainPage()
{
    this.InitializeComponent();
    _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

    _timer.Tick += (sender, o) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

    _timer.Start();
}

public string CurrentTime => DateTime.Now.ToLongTimeString();

Vamos mover todo o código que tem a ver com o _timer para a MainPageLogic classe. As linhas no construtor (exceto para a this.InitializeComponent() chamada) devem ser movidas para o MainPageLogicconstrutor do . A partir do código anterior, tudo o MainPage que deve ser deixado no é a InitializeComponent chamada no construtor.

public MainPage()
{
    this.InitializeComponent();
}

Por enquanto, toque apenas nesta parte do código. Voltaremos ao resto do código da MainPage classe em breve.

Após a mudança, a MainPageLogic classe fica assim:

public class MainPageLogic : ObservableObject
{
    private DispatcherTimer _timer;

    public MainPageLogic()
    {
        _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

        _timer.Tick += (sender, o) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

        _timer.Start();
    }

    public string CurrentTime => DateTime.Now.ToLongTimeString();
}

Lembre-se, temos uma função de conveniência para levantar o PropertyChanged evento. Vamos usar isso no _timer.Tick manipulador.

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

7. Altere o XAML para usar o MainPageLogic

Se você tentar compilar o projeto agora, receberá um erro dizendo que "Propriedade 'CurrentTime' não pode ser encontrada no tipo 'MainPage'" em MainPage.xaml. E, com certeza, a MainPage classe não tem mais propriedade CurrentTime . Foi transferido para a MainPageLogic turma. Para corrigir isso, criaremos uma propriedade chamada Logic na MainPage classe. Isso será do tipo MainPageLogic, e faremos todas as nossas vinculações através disso.

Adicione o seguinte à MainPage classe:

public MainPageLogic Logic { get; } = new MainPageLogic();

Em seguida, em MainPage.xaml, localize o que exibe o TextBlock relógio.

<TextBlock Text="{x:Bind CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

E altere a vinculação adicionando-a Logic. .

<TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

Agora o aplicativo compila e, se você executá-lo, o relógio está marcando como deveria. Bom!

8. Mova o resto da lógica

Vamos pegar o ritmo. Mova o restante do código na MainPage classe para MainPageLogic. Tudo o que deve restar é a Logic propriedade, o construtor e o PropertyChanged evento.

9. Simplifique IsNameNeeded

Em MainPageLogic.cs, substitua o setter de IsNameNeeded propriedade por uma chamada para nosso novo Set método.

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }
}

10. Corrija o OnSubmitClicked método

No nível da lógica, não nos importamos mais com o remetente do evento de clique no botão ou com os args do evento. Também é uma boa prática reconsiderar o nome do método. Não fazemos mais cliques em botões, enviamos lógica. Então, vamos renomear o OnSubmitClicked método para Submit, torná-lo público e remover os parâmetros.

Dentro do método, há a nossa antiga maneira de levantar o PropertyChanged evento. Substitua-o por uma chamada para ObservableObject.RaisePropertyChanged. No final, todo o método deve ficar assim:

public void Submit()
{
    if (string.IsNullOrEmpty(UserName))
    {
        return;
    }

    IsNameNeeded = false;
    RaisePropertyChanged(nameof(GetGreetingVisibility));
}

11. Altere o XAML para fazer referência ao Logic

Em seguida, volte para MainPage.xaml e altere as associações restantes para percorrer a Logic propriedade. Quando tudo estiver feito, o Grid deve ficar assim:

<Grid>
    <TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
               HorizontalAlignment="Right"
               Margin="10"/>

    <StackPanel HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Orientation="Horizontal"
                Visibility="{x:Bind Logic.IsNameNeeded, Mode=OneWay}">
        <TextBlock Margin="10"
                   VerticalAlignment="Center"
                   Text="Enter your name: "/>
        <TextBox Name="tbUserName"
                 Margin="10"
                 Width="150"
                 VerticalAlignment="Center"
                 Text="{x:Bind Logic.UserName, Mode=TwoWay}"/>
        <Button Margin="10"
                VerticalAlignment="Center"
                Click="{x:Bind Logic.Submit}" >Submit</Button>
    </StackPanel>

    <TextBlock Text="{x:Bind sys:String.Format('Hello {0}!',  tbUserName.Text), Mode=OneWay}"
               Visibility="{x:Bind Logic.GetGreetingVisibility(), Mode=OneWay}"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"
               Margin="10"/>
</Grid>

Observe como até mesmo o Button.Click evento pode ser vinculado ao Submit método na MainPageLogic classe.

Se você compilar o projeto agora, você ainda receberá um aviso que diz que o MainPage.PropertyChanged nunca é usado.

12. Arrume a MainPage aula

O aviso ocorre porque não precisamos mais da INotifyPropertyChanged interface na MainPage classe. Então, vamos removê-lo da declaração de classe, juntamente com o PropertyChanged evento.

No final, toda MainPage a classe fica assim:

public sealed partial class MainPage : Page
{
    public MainPageLogic Logic { get; } = new MainPageLogic();

    public MainPage()
    {
        this.InitializeComponent();
    }

}

Isto é tão limpo quanto possível.

13. Execute o aplicativo

Se tudo correu bem, você deve ser capaz de executar o aplicativo neste momento e verificar se ele funciona exatamente como funcionava anteriormente. Parabéns!

Resumo

Então, o que conseguimos com todo esse trabalho? Embora o aplicativo funcione da mesma forma que antes, chegamos a uma arquitetura escalável, sustentável e testável.

A MainPage aula agora é muito simples. Ele contém uma referência à lógica e simplesmente recebe e encaminha um evento de clique de botão. Todo o fluxo de dados entre a lógica e a interface do usuário acontece por meio da vinculação de dados, que é rápida, robusta e comprovada.

A MainPageLogic classe agora é agnóstica da interface do usuário. Não importa se o relógio é exibido em um TextBlock ou algum outro controle. O envio do formulário pode acontecer de várias maneiras. Essas maneiras incluem um clique no botão, pressionar a tecla Enter ou um algoritmo de reconhecimento facial detetando um sorriso. O formulário também pode ser enviado usando testes de unidade automáticos que visam a lógica e garantem que ela funcione de acordo com os requisitos do projeto.

Por esses motivos, assim como outros, é uma boa prática ter apenas recursos relacionados à interface do usuário no codebehind da página e separar a lógica em uma classe diferente. Aplicativos mais complicados também podem ter controle de animação e outros recursos concretos relacionados à interface do usuário. Ao trabalhar com aplicativos mais complicados, você apreciará a separação da interface do usuário e da lógica que criamos nesta lição.

Você pode reutilizar a ObservableObject classe em seu próprio projeto. Depois de um pouco de prática, você descobrirá que é realmente mais rápido e fácil abordar os problemas dessa maneira. Ou aproveite uma biblioteca existente e bem estabelecida, como o MVVM Toolkit, que segue e se baseia nos princípios que você aprendeu neste módulo.

5. Modifique a Clock classe para tirar proveito de ObservableObject

Altere a assinatura de , para que ela herde de Clockem vez de ObservableObjectINotifyPropertyChanged.

public class Clock : ObservableObject

Agora temos o PropertyChanged evento definido na classe e em sua classe base, o Clock que resulta em um aviso do compilador. Exclua o PropertyChanged evento da Clock classe.

Para elevar o PropertyChanged evento, criamos uma função de conveniência na ObservableObject classe. Para usá-lo, substitua a _timer.Tick linha pelo seguinte:

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

A Clock aula já se tornou mais simples. Mas vamos ver o que podemos fazer com a classe mais complexa MainWindowDataContext .

6. Modifique a MainWindowDataContext classe para tirar proveito de ObservableObject

Assim como acontece com a classe, começamos novamente alterando a Clock declaração de classe para que ela herde do ObservableObject.

public class MainWindowDataContext : ObservableObject

Certifique-se de excluir o PropertyChanged evento aqui também.

Dê uma olhada no setter da IsNameNeeded propriedade. É o que parece agora:

set
{
    if (value != _isNameNeeded)
    {
        _isNameNeeded = value;
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(GreetingVisibility)));
    }
}

Este é o padrão padrão INotifyPropertyChanged , com a invocação de evento extra PropertyChanged se o novo IsNameNeeded valor da propriedade for diferente.

Esta é exatamente a situação para a qual a ObservableObject.Set função foi criada. A Set função ainda retorna um bool valor que indica se os valores antigo e novo da propriedade são diferentes. Assim, o setter de propriedade acima pode ser simplificado assim:

if (Set(ref _isNameNeeded, value))
{
    RaisePropertyChanged(nameof(GreetingVisibility));
}

Nada mau!

7. Execute o aplicativo

Se tudo correu bem, você deve ser capaz de executar o aplicativo neste momento e verificar se ele funciona exatamente como funcionava anteriormente. Parabéns!

Resumo

Então, o que conseguimos com todo esse trabalho? Embora o aplicativo funcione da mesma forma que antes, chegamos a uma arquitetura escalável, sustentável e testável.

A MainWindow aula é muito simples. Ele contém uma referência à lógica e simplesmente recebe e encaminha um evento de clique de botão. Todo o fluxo de dados entre a lógica e a interface do usuário acontece por meio da vinculação de dados, que é rápida, robusta e comprovada.

A MainWindowDataContext classe agora é agnóstica da interface do usuário. Não importa se o relógio é exibido em um TextBlock ou algum outro controle. O envio do formulário pode acontecer de várias maneiras. Essas maneiras incluem um clique no botão, pressionar a tecla Enter ou um algoritmo de reconhecimento facial detetando um sorriso. O formulário também pode ser enviado usando testes de unidade automáticos que visam a lógica e garantem que ela funcione de acordo com os requisitos do projeto.

Por esses motivos, bem como outros, é uma boa prática ter apenas recursos relacionados à interface do usuário no code-behind da janela e separar a lógica em uma classe diferente. Aplicativos mais complexos também podem ter controle de animação e outros recursos concretos relacionados à interface do usuário. Ao trabalhar com aplicativos mais complexos, você apreciará a separação entre interface do usuário e lógica que criamos nesta lição.

Você pode reutilizar a ObservableObject classe em seu próprio projeto. Depois de um pouco de prática, você descobrirá que é realmente mais rápido e fácil abordar os problemas dessa maneira. Ou aproveite uma biblioteca existente e bem estabelecida, como o MVVM Toolkit, que segue e se baseia nos princípios que você aprendeu neste módulo.