.NET MAUI - MVVM, DataBinding e a interface ICommand - IV


  Vou continuar a apresentar os principais conceitos sobre MVVM, DataBinding e da interface ICommand : Usando o Mvvm Community ToolKit para simplificar a implementação do padrão MVVM.

Continuando o artigo anterior veremos como simplificar a implementação do padrão MVVM, particularmente na implementação da interface INotifyPropertyChanged, usando o pacote MVVM Community TooloKit.

O padrão MVVM ajuda a separar as responsabilidades entre a View a lógica de negócio da aplicação mas sua implementação nos leva a ter que incluir manualmente na implementação da interface INotifyPropertyChanged muito código repetitivo que esta sujeito a erros.

Para cada propriedade e cada comando, precisamos implementar um backing field, setters e getters e gerar um evento para notificar a UI de que algo mudou e isso requer muito código clichê.

Por exemplo para uma única propriedade Nome definida na viewmodel teremos que criar algo como o código abaixo:

private string _nome;
public string Nome
{
    get => _nome;
    set
    {
        if (_nome.Equals(value))
        {
            return;
        }

        _nome = value;
        OnPropertyChanged();
    }
}

Isso pode se tornar caro rapidamente, porque além da lógica de negócios real, há muito código adicional que precisa ser mantido. Isso custa um tempo valioso de desenvolvimento e aumenta o risco de bugs.

Aqui entra o pacote MVVM Community ToolKit da Microsoft que é um pacote que oferece diversas ferramentas e utilitários para ajudar na implementação do padrão MVVM em projetos .NET.

Para evitar esses erros comuns e ajudar com o princípio Don't Repeat Yourself (DRY), o MVVM Community Toolkit nos ajuda a reduzir a quantidade desse código clichê. Ele faz isso de várias maneiras:

Isso reduz drasticamente o tempo de desenvolvimento e nossos arquivos de código parecem muito mais limpos. Então, usando os Source Generators, nosso código acima pode ser reduzido para escrever uma linha como esta:

[ObservableProperty] private string _nome;

Uma das funcionalidades do pacote é a classe ObservableObject, que simplifica a implementação da interface INotifyPropertyChanged e reduz a quantidade de código repetitivo necessário para criar objetos observáveis.

Para mostrar como o pacote MVVM atua, vamos dar uma olhada em um ViewModel comum que implementa a interface INotifyPropertyChanged e depois veremos como ela muda quando usamos o recurso do MVVM Source Generators

Para ilustrar vamos nos basear em uma aplicação  .NET MAUI bem simples que vai permitir informar o nome, sobrenome, rua codigo postal e cidade e durante a entrada dos dados a aplicação vai exibir em um Frame o endereço montado e vai ter também um botão que permite imprimir o endereço, que no nosso exemplo, vai usar um DisplayAlert para exibir o endereço completo.

Abaixo temos a aplicação em execução :

Neste projeto temos uma view chamada EnderecoView.xaml com seguinte código:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiEnderecoCompleto.Mvvm.Views.EnderecoView"
             Title="Endereço">
    <Grid>
        <VerticalStackLayout Padding="20" Spacing="20"
            VerticalOptions="Fill">
            <Grid
                ColumnDefinitions="*,*"
                RowDefinitions="auto,auto,auto,auto,auto">

                <Label Grid.Row="0" Grid.Column="0"
                  Text="Nome :"
                  VerticalTextAlignment="Center" />
                <Entry Grid.Row="0" Grid.Column="1"
                  Text="{Binding Nome}" />

                <Label Grid.Row="1" Grid.Column="0"
                  Text="Sobrenome :"
                  VerticalTextAlignment="Center" />
                <Entry Grid.Row="1" Grid.Column="1"
                  Text="{Binding Sobrenome}" />

                <Label Grid.Row="2" Grid.Column="0"
                  Text="Rua / numero :"
                  VerticalTextAlignment="Center" />
                <Entry Grid.Row="2" Grid.Column="1"
                  Text="{Binding Rua}" />

                <Label Grid.Row="3" Grid.Column="0"
                  Text="Código Postal :"
                  VerticalTextAlignment="Center" />
                <Entry Grid.Row="3" Grid.Column="1"
                  Text="{Binding Cep}" />

                <Label Grid.Row="4" Grid.Column="0"
                  Text="Cidade / Estado :"
                  VerticalTextAlignment="Center" />
                <Entry Grid.Row="4" Grid.Column="1"
                  Text="{Binding Cidade}" />
            </Grid>

            <Frame BorderColor="Gray"
                   CornerRadius="50"
                   HasShadow="True">
               <Label MaxLines="4"
                      FontSize="20"
                      FontAttributes="Bold"
                      HorizontalOptions="Center"
                   Text="{Binding Endereco}"
                   VerticalOptions="End" />
            </Frame>

            <Button
                Command="{Binding ImprimirEnderecoCommand}"
                CommandParameter="{Binding Endereco}"

                FontSize="20"
                Text="Imprimir Endereço" />
        </VerticalStackLayout>

    </Grid>
</ContentPage>

Temos também uma view model chamada EnderecoViewModel com o seguinte código :

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows.Input;

namespace MauiEnderecoCompleto.Mvvm.ViewModels;

public class EnderecoViewModel : INotifyPropertyChanged
{
    public ICommand ImprimirEnderecoCommand { get; private set; }
    public EnderecoViewModel()
    {
        ImprimirEnderecoCommand = new Command<string>(ImprimirEndereco);
    }

    private string _nome;
    public string Nome
    {
        get => _nome;
        set
        {
            if (SetField(ref _nome, value))
            {
                OnPropertyChanged(nameof(Endereco));
            }
        }
    }

    private string _sobrenome;
    public string Sobrenome
    {
        get => _sobrenome;
        set
        {
            if (SetField(ref _sobrenome, value))
            {
                OnPropertyChanged(nameof(Endereco));
            }
        }
    }

    private string _rua;
    public string Rua
    {
        get => _rua;
        set
        {
            if (SetField(ref _rua, value))
            {
                OnPropertyChanged(nameof(Endereco));
            }
        }
    }

    private string _cep;
    public string Cep
    {
        get => _cep;
        set
        {
            if (SetField(ref _cep, value))
            {
                OnPropertyChanged(nameof(Endereco));
            }
        }
    }

    private string _cidade;
    public string Cidade
    {
        get => _cidade;
        set
        {
            if (SetField(ref _cidade, value))
            {
                OnPropertyChanged(nameof(Endereco));
            }
        }
    }

    public string Endereco
    {
        get
        {
            var stringBuilder = new StringBuilder();

            stringBuilder
                .AppendLine($"{Nome} {Sobrenome}")
                .AppendLine(Rua)
                .AppendLine($"{Cep} - {Cidade}");

            return stringBuilder.ToString();
        }
    }

    private void ImprimirEndereco(string endereco)
    {
        App.Current.MainPage.DisplayAlert("Endereço", endereco, "Ok");
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
        {
            return false;
        }

        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

}

Observe a quantidade de código necessária na implementação da interface INotifyPropertyChanged.

Ufa !!!, é muito código para apenas algumas propriedades e um mísero comando ! Quanta repetição...

Nota: Poderia ser ainda pior, se a comparação de igualdade tivesse sido implementada separadamente em cada propriedade também.

Tecnicamente, o código acima é bom e sólido, mas fazer a mesma coisa várias vezes para cada ViewModel diferente usada em uma aplicação pode acabar se tornando bastante tedioso e sujeito a erros.

Felizmente o pacote MVVM Community ToolKit vem em nosso auxílio e veremos que após a sua utilização o código vai ser reduzido drásticamente.  Vamos entender como usar o MVVM Community ToolKit destacando seus principais recursos.

Usando o MVVM Community ToolKit

A primeira coisa a fazer é instalar o pacote nuger CommunityToolkit.MVVM no projeto .NET MAUI.

Atualmente a versão mais recente é a 8.2.0 (lançada em maio/2023).

Depois de instalado, podemos extrair as classes e atributos necessários adicionando a seguinte instrução using a nossa ViewModel EnderecoViewModel:

using CommunityToolkit.Mvvm.ComponentModel;

O MVVM Community Toolkit também fornece funcionalidade MVVM regular e classes base que podem ser usadas para simplificar ViewModels que você pode usar com ou sem os geradores de código-fonte.

Após instalar e referenciar o pacote em nossa view model podemos iniciar a utilização dos seus recursos.

Assim podemos usar a classe ObservableObject como uma classe base ou usar o atributo [INotifyPropertyChanged] em uma classe parcial para gerar automaticamente a implementação INotifyPropertyChanged para nós.

Se a sua viewmodel não precisar herdar de outra classe, como é o nosso exemplo, podemos usar a classe base ObservableObject :

...
using
CommunityToolkit.Mvvm.ComponentModel;

namespace MauiEnderecoCompleto.Mvvm.ViewModels;

public partial class EnderecoViewModel : ObservableObject
{
   ...
}

Se a sua view model precisar herdar de outra classe base podemos usar o atributo[INotifyPropertyChanged] (e, opcionalmente, também o atributo [INotifyPropertyChanging]):

using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiEnderecoCompleto.Mvvm.ViewModels;

[INotifyPropertyChanged]
[INotifyPropertyChanging]
//opcional
public
partial class EnderecoViewModel : OutraClasse
{
   ...
}

Cabe destacar que ao usar o atributo [INotifyPropertyChanged] ou a classe base ObservableObject, sua classe deve ser declarada como partial, porque os geradores de código criam um código que complementa a classe.

Atributos

O MVVM Community Toolkit usa atributos para permitir que os MVVM Source Generators saibam que algo deve ser gerado automaticamente. Os atributos podem ser colocados acima ou na frente dos campos e assinaturas de método.

Ao usar os geradores de código, você provavelmente usará os seguintes atributos com mais frequência do que qualquer um dos outros:

1 - [ObservableProperty]


Este atributo é usado para gerar automaticamente uma propriedade para um campo de apoio ou backing field. A propriedade resultante, por convenção, sempre começa com letra maiúscula.

Os campos de apoio devem ser declarados privados e começar com uma letra minúscula ou com um sublinhado (_):

Para o nosso exemplo o código usado para cada uma das propriedades ficaria assim:

using System.ComponentModel;
using
System.Runtime.CompilerServices;
using
System.Text;
using
System.Windows.Input;

using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiEnderecoCompleto.Mvvm.ViewModels;

public partial class EnderecoViewModel : ObservableObject
{

   [ObservableProperty]
 
 private string _nome;

   [ObservableProperty]
 
 private string _sobrenome;

   [ObservableProperty]
  
private string _rua;

   [ObservableProperty]
  
private string _cep;

   [ObservableProperty]
  
private string _cidade;

...
}

Observe que especificamos apenas os campos de apoio para as propriedades, mas para acessá-los, ainda precisamos usar os nomes das propriedades apropriados. Eles podem ser vinculados dentro do código XAML usando os nomes das propriedades:

...
<
Entry Grid.Row="0" Grid.Column="1"  Text="{Binding Nome}" />
<
Entry Grid.Row="1" Grid.Column="1"  Text="{Binding Sobrenome}" />
<
Entry Grid.Row="2" Grid.Column="1"  Text="{Binding Rua
}" />
...

2- [RelayCommand]

Para gerar automaticamente um comando, você pode simplesmente colocar esse atributo acima ou na frente de um método. O comando resultante terá o mesmo nome do método, mas com o sufixo Command:

[RelayCommand]
private
void MeuMetodo()
{
  Console.WriteLine(
"Comando");
}

Este atributo também pode ser usado em métodos assíncronos, que na verdade criarão uma instância de AsyncRelayCommand 'debaixo dos panos', o que é muito conveniente ao usar MVVM com .NET MAUI:

[RelayCommand]
private
async Task MeuMetodoAsync()
{
  
await MessageService.ShowMessageAsync("Mensagem");
}

Os comandos resultantes podem ser vinculados acessando MeuMetodoCommand e MeuMetodoAsyncCommand do código XAML:

...
<Button Command =
"{Binding MeuMetodoCommand}" />
<Button Command=
"{Binding MeuMetodoAsyncCommand}" />
...

3- [NotifyPropertyChangedFor]

Às vezes, você também pode querer informar ao consumidor da propriedade que outra propriedade também foi alterada e que todas as vinculações devem ser atualizadas.

É para isso que serve o atributo [NotifyPropertyChangedFor] e só funciona em combinação com o atributo [ObservableProperty]. Enquanto os outros atributos não exigem um argumento, este requer o nome da propriedade para a qual um evento PropertyChanged deve ser gerado:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(NomeCompleto))]

private
string _nome;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(NomeCompleto))]

private
string _sobrenome;

//o evento PropertyChanged será disparado para esta propriedade
//sempre que ou o Nome ou o Sobrenome mudar

public
string NomeCompleto => $"{Nome} {Sobrenome}";
 

Reduzindo o código da view model

Agora vamos atualizar a nossa view model EnderecoViewModel usando o MVVM ToolKit para reduzir o código.

a- Vamos usar o atributo ObservableObject para implementar a interface INotifyProperyChanged

b- Vamos usar os atributos  [ObservableProperty] e NotifyPropertyChanged(nameof(Endereco)] para as propriedades da view model

c- Vamos usar o atributo [RelayCommand] para o método ImprimirEndereco(string endereco)

Veja abaixo como ficou o código após a aplicação dos recursos do pacote MVVM Toolkit:

using CommunityToolkit.Mvvm.ComponentModel;
using
CommunityToolkit.Mvvm.Input;
using
System.Text;

namespace MauiEnderecoCompleto.Mvvm.ViewModels;

public partial class EnderecoViewModel : ObservableObject
{

   [ObservableProperty]
   [NotifyPropertyChangedFor(nameof(Endereco))]
  
private string _nome;

   [ObservableProperty]
   [NotifyPropertyChangedFor(nameof(Endereco))]
  
private string _sobrenome;

   [ObservableProperty]
   [NotifyPropertyChangedFor(nameof(Endereco))]
  
private string _rua;

   [ObservableProperty]
   [NotifyPropertyChangedFor(nameof(Endereco))]
   private string _cep;

   [ObservableProperty]
   [NotifyPropertyChangedFor(nameof(Endereco))]
  
private string _cidade;

   public string Endereco
   {
     
get
      {
       
var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine(
$"{Nome} {Sobrenome}")
                     .AppendLine(Rua)
                     .AppendLine(
$"{Cep} - {Cidade}");
        
return stringBuilder.ToString();
      }
    }

   [RelayCommand]
  
private void ImprimirEndereco(string endereco)
   {
      App.Current.MainPage.DisplayAlert(
"Endereço", endereco, "Ok");
   }
}

Uau !!!

Acabamos de economizar cerca de 50% das linhas de código em comparação com a versão inicial da nossa ViewModel.

Impressionante !!! não é ? 

Ao executar o projeto teremos o mesmo resultado.

Espiando o código gerado (sob o capô)

Agora, o que realmente acontece quando as propriedades e os comandos são gerados ?

 Como marcamos nossa ViewModel como sendo uma classe parcial, os geradores de código-fonte podem criar mais partes para a nossa classe em arquivos separados, como as propriedades e comandos baseados nos atributos que fornecemos.

Como os geradores de código-fonte em C# são construídos sobre os analisadores de código da plataforma do compilador .NET (Roslyn), eles são executados depois de fazer uma edição em um arquivo C# aberto. Dessa forma, as fontes geradas estão sempre disponíveis para o compilador C#.

Podemos encontrar os arquivos gerados automaticamente (que sempre terminam em .g.cs) para nosso projeto no Solution Explorer em Dependencies -> net7.0 -> Analyzers -> CommunityToolkit.Mvvm.SourceGenerators.



Se selecionarmos o ObservablePropertyGenerator, podemos encontrar um arquivo que termina em EnderecoViewModel.g.cs, que contém nossas propriedades geradas automaticamente com seus setters e getters, por exemplo para nossa propriedade Nome:

Como podemos ver, um EqualityComparer padrão é usado para o campo de apoio _nome, que é do tipo string.

Somente se as strings do campo de apoio e do objeto de valor forem diferentes, o evento PropertyChanging será gerado antes de atualizar o campo de apoio para o valor fornecido. Depois que o campo de apoio foi atualizado, o evento PropertyChanged é gerado, mas não apenas para nossa propriedade Nome, ele também é gerado para a propriedade Endereco, porque adicionamos o atributo [NotifyPropertyChangedFor(nameof(Endereco))] acima de nosso campo de apoio _nome.

Se você olhar de perto, notará que não há apenas chamadas de método OnPropertyChanging() e OnPropertyChanged(), mas também chamadas para um método OnNomeChanging() e um método OnNomeChanged().

Os dois primeiros métodos geram os eventos PropertyChanging e PropertyChanged, respectivamente, enquanto os outros dois são, na verdade, métodos parciais sem um corpo, que são declarados mais abaixo no arquivo de origem gerado automaticamente:


Desta forma, reduzir o código clichê em seu aplicativo que usa o padrão MVVM ficou ainda mais fácil e simples com os MVVM Source Generators do MVVM Community Toolkit.

Como mostramos, eles são muito convenientes e fáceis de usar - desde que você já esteja um pouco familiarizado com o padrão MVVM em aplicativos .NET e saiba o que está fazendo.

Até agora, vimos os casos de uso mais diretos e simples com os atributos comuns [ObservableProperty], [RelayCommand] e [NotifyPropertyChangedFor], e, em breve vamos abordar outros recursos.

Pegue o código do projeto aqui:  MauiEnderecoCompleto2.zip (sem as referências)

'Tendo sido, pois, justificados pela fé, temos paz com Deus, por nosso Senhor Jesus Cristo;'
Romanos 5:1

Referências:


José Carlos Macoratti