Vinculação de dados e MVVM
O padrão MVVM (Model-View-ViewModel) aplica uma separação entre três camadas de software — a interface do usuário XAML, chamada de exibição, os dados subjacentes, chamados de modelo, e um intermediário entre a exibição e o modelo, chamado de viewmodel. A exibição e o viewmodel geralmente são conectados por meio de associações de dados definidas em XAML. O BindingContext
da exibição geralmente é uma instância do viewmodel.
Importante
O .NET MAUI (.NET Multi-platform App UI) realiza marshals de atualizações de associação para o thread da IU. Ao usar o MVVM, isso permite que você atualize as propriedades viewmodel associadas a dados de qualquer thread, com o mecanismo de associação do .NET MAUI trazendo as atualizações para o thread da interface do usuário.
Há várias abordagens para implementar o padrão MVVM, e este artigo se concentra em uma abordagem simples. Ele usa exibições e viewmodels, mas não modelos, para se concentrar na associação de dados entre as duas camadas. Para uma explicação detalhada sobre como usar o padrão MVVM no .NET MAUI, consulte MVVM (Model-View-ViewModel) em Padrões de Aplicativo Empresarial usando o .NET MAUI. Consulte um tutorial que ajuda a implementar o padrão MVVM em Atualizar seu aplicativo com conceitos MVVM.
MVVM simples
Nas extensões de marcação XAML, você viu como definir uma nova declaração de namespace de XML para permitir que um arquivo XAML faça referência a classes em outros assemblies. O exemplo a seguir usa a extensão de marcação x:Static
para obter a data e a hora atuais da propriedade DateTime.Now
estática no namespace System
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="XamlSamples.OneShotDateTimePage"
Title="One-Shot DateTime Page">
<VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
Spacing="25" Padding="30,0"
VerticalOptions="Center" HorizontalOptions="Center">
<Label Text="{Binding Year, StringFormat='The year is {0}'}" />
<Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
<Label Text="{Binding Day, StringFormat='The day is {0}'}" />
<Label Text="{Binding StringFormat='The time is {0:T}'}" />
</VerticalStackLayout>
</ContentPage>
Neste exemplo, o valor recuperado DateTime
é definido como o BindingContext
em um StackLayout. Quando você define o BindingContext
em um elemento, ele é herdado por todos os filhos desse elemento. Isso significa que todos os filhos do StackLayout têm o mesmo BindingContext
, e eles podem conter associações a propriedades desse objeto:
No entanto, o problema é que a data e a hora são definidas uma vez quando a página é construída e inicializada e nunca mudam.
Aviso
Em uma classe derivada de BindableObject, somente as propriedades do tipo BindableProperty são associáveis. Por exemplo, VisualElement.IsLoaded e Element.Parent não são associáveis.
Uma página XAML pode exibir um relógio que sempre mostra a hora atual, mas requer código adicional. O padrão MVVM é uma escolha natural para aplicativos do .NET MAUI quando a associação de dados de propriedades entre objetos visuais e os dados subjacentes. Ao pensar em termos de MVVM, o modelo e o viewmodel são classes escritas inteiramente em código. O modo de exibição geralmente é um arquivo XAML que faz referência a propriedades definidas no viewmodel por meio de associações de dados. No MVVM, um modelo ignora o viewmodel e um viewmodel ignora a exibição. No entanto, muitas vezes você adapta os tipos expostos pelo viewmodel aos tipos associados à IU.
Observação
Em exemplos simples de MVVM, como os mostrados aqui, geralmente não há nenhum modelo e o padrão envolve apenas uma exibição e um viewmodel vinculados a associações de dados.
O exemplo a seguir mostra um viewmodel de um relógio, com uma única propriedade chamada DateTime
que é atualizada a cada segundo:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class ClockViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private DateTime _dateTime;
private Timer _timer;
public DateTime DateTime
{
get => _dateTime;
set
{
if (_dateTime != value)
{
_dateTime = value;
OnPropertyChanged(); // reports this property
}
}
}
public ClockViewModel()
{
this.DateTime = DateTime.Now;
// Update the DateTime property every second.
_timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
~ClockViewModel() =>
_timer.Dispose();
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Os viewmodels normalmente implementam a interface INotifyPropertyChanged
, que fornece a capacidade de uma classe gerar o evento PropertyChanged
sempre que uma de suas propriedades for alterada. O mecanismo de associação de dados no .NET MAUI anexa um manipulador a esse evento PropertyChanged
para que ele possa ser notificado quando uma propriedade for alterada e manter o destino atualizado com o novo valor. No exemplo de código anterior, o método OnPropertyChanged
manipula a geração do evento enquanto determina automaticamente o nome da origem da propriedade: DateTime
.
O seguinte exemplo mostra o XAML que consome ClockViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<ContentPage.BindingContext>
<local:ClockViewModel />
</ContentPage.BindingContext>
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="18"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
Neste exemplo, ClockViewModel
é definido como o BindingContext
da ContentPage usando marcas de elemento da propriedade. Como alternativa, o arquivo code-behind pode instanciar o viewmodel.
A extensão de marcação Binding
na propriedade Text
do Label formata a propriedade DateTime
. A captura de tela a seguir mostra o resultado:
Além disso, é possível acessar propriedades individuais da propriedade DateTime
do viewmodel separando as propriedades com pontos:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interativo
O MVVM geralmente é usado com associações de dados bidirecionais para uma exibição interativa com base em um modelo de dados subjacente.
O exemplo a seguir mostra o HslViewModel
que converte um valor Color em valores Hue
, Saturation
, e Luminosity
e vice-versa:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class HslViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private float _hue, _saturation, _luminosity;
private Color _color;
public float Hue
{
get => _hue;
set
{
if (_hue != value)
Color = Color.FromHsla(value, _saturation, _luminosity);
}
}
public float Saturation
{
get => _saturation;
set
{
if (_saturation != value)
Color = Color.FromHsla(_hue, value, _luminosity);
}
}
public float Luminosity
{
get => _luminosity;
set
{
if (_luminosity != value)
Color = Color.FromHsla(_hue, _saturation, value);
}
}
public Color Color
{
get => _color;
set
{
if (_color != value)
{
_color = value;
_hue = _color.GetHue();
_saturation = _color.GetSaturation();
_luminosity = _color.GetLuminosity();
OnPropertyChanged("Hue");
OnPropertyChanged("Saturation");
OnPropertyChanged("Luminosity");
OnPropertyChanged(); // reports this property
}
}
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Neste exemplo, as alterações nas propriedades Hue
, Saturation
e Luminosity
fazem com que a propriedade Color
seja alterada e as alterações na propriedade Color
fazem com que as outras três propriedades sejam alteradas. Isso pode parecer um loop infinito, exceto que o viewmodel não invoca o evento PropertyChanged
, a menos que a propriedade tenha sido alterada.
O exemplo de XAML a seguir contém uma propriedade BoxView cuja propriedade Color
está associada à propriedade Color
do viewmodel, e três exibições Slider e Label associadas às propriedades Hue
, Saturation
e Luminosity
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="10, 0, 10, 30">
<BoxView Color="{Binding Color}"
HeightRequest="100"
WidthRequest="100"
HorizontalOptions="Center" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue}"
Margin="20,0,20,0" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation}"
Margin="20,0,20,0" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity}"
Margin="20,0,20,0" />
</VerticalStackLayout>
</ContentPage>
A associação em cada Label é o padrão OneWay
. Ela só precisa exibir o valor. No entanto, a associação padrão em cada Slider é TwoWay
. Isso permite que Slider seja inicializado no viewmodel. Quando o viewmodel é instanciado, sua propriedade Color
é definida como Aqua
. Uma alteração em um Slider define um novo valor para a propriedade no viewmodel, que calcula uma nova cor:
Comando
Às vezes, um aplicativo tem necessidades que vão além das associações de propriedade ao exigir que o usuário inicie os comandos que afetam algo no viewmodel. Esses comandos geralmente são sinalizados por cliques de botões ou toques de dedos e são tradicionalmente processados no arquivo code-behind em um manipulador para o evento Clicked
do Button ou o evento Tapped
de um TapGestureRecognizer.
A interface de comando oferece uma abordagem alternativa à implementação de comandos, que é bem mais adequada à arquitetura MVVM. O viewmodel pode conter comandos, que são métodos executados em reação a uma atividade específica no view, como um clique Button. Associações de dados são definidas entre esses comandos e o Button.
Para permitir uma associação de dados entre um Button e um viewmodel, o Button define duas propriedades:
Command
do tipoSystem.Windows.Input.ICommand
CommandParameter
do tipoObject
Observação
Muitos outros controles também definem as propriedades Command
e CommandParameter
.
A interface ICommand é definida no namespace System.Windows.Input e consiste em dois métodos e um evento:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
O viewmodel pode definir propriedades do tipo ICommand. Em seguida, você pode associar essas propriedades à propriedade Command
de cada Button ou outro elemento, ou talvez a uma exibição personalizada que implemente essa interface. Opcionalmente, você pode definir a propriedade CommandParameter
para identificar objetos Button individuais (ou outros elementos) que estão associados a essa propriedade viewmodel. Internamente, o Button chama o método Execute
sempre que o usuário toca no Button, passando para o método Execute
seu CommandParameter
.
O método CanExecute
e o evento CanExecuteChanged
são usados para casos em que um toque do Button possa ser inválido no momento, caso em que o Button deve se desabilitar. O Button chama CanExecute
quando a propriedade Command
é definida pela primeira vez e sempre que o evento CanExecuteChanged
é gerado. Se CanExecute
retornar false
, o Button se desativa e não gera chamadas Execute
.
Você pode usar a classe Command
ou Command<T>
incluída no .NET MAUI para implementar a interface ICommand. Essas duas classes definem vários construtores, além de um método ChangeCanExecute
que o viewmodel pode chamar para forçar o objeto Command
a gerar o evento CanExecuteChanged
.
O exemplo a seguir mostra um viewmodel de um teclado simples para inserir números de telefone:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace XamlSamples;
class KeypadViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _inputString = "";
private string _displayText = "";
private char[] _specialChars = { '*', '#' };
public ICommand AddCharCommand { get; private set; }
public ICommand DeleteCharCommand { get; private set; }
public string InputString
{
get => _inputString;
private set
{
if (_inputString != value)
{
_inputString = value;
OnPropertyChanged();
DisplayText = FormatText(_inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
}
public string DisplayText
{
get => _displayText;
private set
{
if (_displayText != value)
{
_displayText = value;
OnPropertyChanged();
}
}
}
public KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);
// Command to delete a character from the input string when allowed
DeleteCharCommand =
new Command(
// Command will strip a character from the input string
() => InputString = InputString.Substring(0, InputString.Length - 1),
// CanExecute is processed here to return true when there's something to delete
() => InputString.Length > 0
);
}
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
string formatted = str;
// Format the string based on the type of data and the length
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
// Special characters exist, or the string is too small or large for special formatting
// Do nothing
}
else if (str.Length < 8)
formatted = string.Format("{0}-{1}", str.Substring(0, 3), str.Substring(3));
else
formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6));
return formatted;
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Neste exemplo, os métodos Execute
e CanExecute
dos comandos são definidos como funções lambda no construtor. O viewmodel pressupõe que a propriedade AddCharCommand
está associada à propriedade Command
de vários botões (ou qualquer outro controle que tenha uma interface de comando), cada um identificado pelo CommandParameter
. Esses botões adicionam caracteres a uma propriedade InputString
, que é formatada como um número de telefone da propriedade DisplayText
. Há também uma segunda propriedade do tipo ICommand chamada DeleteCharCommand
. Ela é associada a um botão de espaçamento traseiro, mas o botão deve ser desabilitado se não houver caracteres a serem excluídos.
O seguinte exemplo mostra o XAML que consome o KeypadViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<ContentPage.BindingContext>
<local:KeypadViewModel />
</ContentPage.BindingContext>
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<Label Text="{Binding DisplayText}"
Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" HorizontalTextAlignment="End"
Grid.ColumnSpan="2" />
<Button Text="⇦" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>
<Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
<Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
<Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />
<Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
<Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
<Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />
<Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
<Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
<Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />
<Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
<Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
<Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
</Grid>
</ContentPage>
Neste exemplo, a propriedade Command
do primeiro Button que está associada ao DeleteCharCommand
. Os outros botões estão associados ao AddCharCommand
com um CommandParameter
que é o mesmo que o caractere que aparece no Button: