Implementando INotifyPropertyChanged da maneira mais fácil
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.
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 INotifyPropertyChanged
o . 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 MainPageLogic
construtor 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 Clock
em vez de ObservableObject
INotifyPropertyChanged
.
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.