Vinculação de dados e MVVM
O padrão MVVM (Model-View-ViewModel) impõe 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 viewmodel. O modo de exibição e o viewmodel geralmente são conectados por meio de associações de dados definidas em XAML. O BindingContext
para o modo de exibição geralmente é uma instância do viewmodel.
Importante
A interface do usuário do aplicativo multiplataforma .NET (.NET MAUI) marca as atualizações de vinculação ao thread da interface do usuário. Ao usar o MVVM, isso permite que você atualize as propriedades viewmodel vinculadas 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 modos de exibição e modelos de exibição, mas não modelos, para se concentrar na vinculação de dados entre as duas camadas. Para obter uma explicação detalhada do uso do padrão MVVM no .NET MAUI, consulte Model-View-ViewModel (MVVM) em Enterprise Application Patterns using .NET MAUI. Para obter um tutorial que ajuda você a implementar o padrão MVVM, consulte 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 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 para obter a data e a x:Static
hora atuais da propriedade static DateTime.Now
no System
namespace:
<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 StackLayoutarquivo . 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 ligações às 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.
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 .NET MAUI quando se vinculam dados de propriedades entre objetos visuais e os dados subjacentes. Ao pensar em termos de MVVM, o modelo e viewmodel são classes escritas inteiramente em código. O modo de exibição geralmente é um arquivo XAML que faz referência às propriedades definidas no viewmodel por meio de associações de dados. No MVVM, um modelo é ignorante do viewmodel, e um viewmodel é ignorante do view. No entanto, geralmente você personaliza os tipos expostos pelo viewmodel para os tipos associados à interface do usuário.
Observação
Em exemplos simples de MVVM, como os mostrados aqui, muitas vezes não há nenhum modelo, e o padrão envolve apenas uma exibição e um modelo de exibição vinculados a associações de dados.
O exemplo a seguir mostra um viewmodel para 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));
}
Viewmodels normalmente implementam a interface, que fornece a INotifyPropertyChanged
capacidade de uma classe para gerar o PropertyChanged
evento sempre que uma de suas propriedades é alterada. O mecanismo de vinculação de dados no .NET MAUI anexa um manipulador a esse PropertyChanged
evento 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 manipula a elevação do evento enquanto determina automaticamente o OnPropertyChanged
nome da origem da propriedade: DateTime
.
O exemplo a seguir mostra 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
das marcas de elemento de ContentPage propriedade using . Como alternativa, o arquivo code-behind pode instanciar o viewmodel.
A Binding
extensão de marcação na Text
propriedade dos formata Label a DateTime
propriedade. A captura de tela a seguir mostra o resultado:
Além disso, é possível acessar propriedades individuais da DateTime
propriedade do viewmodel separando as propriedades com pontos:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interativo
O MVVM é frequentemente 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 Color valor em Hue
, Saturation
e Luminosity
valores 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 , e fazem com que a Color
propriedade seja alterada, Saturation
e Luminosity
as alterações na propriedade fazem com que as Hue
Color
outras três propriedades sejam alteradas. Isso pode parecer um loop infinito, exceto que o viewmodel não invoca o evento, a PropertyChanged
menos que a propriedade tenha sido alterada.
O exemplo XAML a seguir contém uma BoxView propriedade cuja Color
propriedade está vinculada à Color
propriedade do viewmodel e três e três Label Slider modos de exibição vinculados às Hue
propriedades , 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 um Label é o padrão OneWay
. Ele só precisa exibir o valor. No entanto, a associação padrão em cada um Slider é TwoWay
. Isso permite que o seja inicializado a Slider partir do viewmodel. Quando o viewmodel é instanciado, Color
sua propriedade é definida como Aqua
. Uma alteração em um define um Slider 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, exigindo que o usuário inicie 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 modo de exibição, como um Button clique. Associações de dados são definidas entre esses comandos e o Button.
Para permitir uma associação de dados entre um e um Button viewmodel, o Button define duas propriedades:
Command
do tipoSystem.Windows.Input.ICommand
CommandParameter
do tipoObject
Observação
Muitos outros controles também definem Command
e CommandParameter
propriedades.
A ICommand interface é 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 vincular essas propriedades à Command
propriedade de cada Button um ou outro elemento, ou talvez a um modo de exibição personalizado que implemente essa interface. Opcionalmente, você pode definir a propriedade para identificar objetos individuais Button (ou outros elementos) que estão vinculados a CommandParameter
essa propriedade viewmodel. Internamente, o chama o método sempre que o usuário toca no , passando para o método o Execute
Button Execute
seu CommandParameter
.Button
O CanExecute
método e CanExecuteChanged
o evento são usados para casos em que um Button toque pode ser inválido no momento, caso em que o Button deve se desabilitar. As Button chamadas CanExecute
quando a Command
propriedade é definida pela primeira vez e sempre que o CanExecuteChanged
evento é gerado. Se CanExecute
retornar false
, o Button se desativa e não gera Execute
chamadas.
Você pode usar a classe ou Command<T>
incluída no .NET MAUI para implementar a Command
ICommand interface. Essas duas classes definem vários construtores mais um ChangeCanExecute
método que o viewmodel pode chamar para forçar o objeto a gerar o CanExecuteChanged
Command
evento.
O exemplo a seguir mostra um viewmodel para um teclado simples destinado a 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 e CanExecute
para os Execute
comandos são definidos como funções lambda no construtor. O viewmodel assume que a AddCharCommand
propriedade está vinculada à Command
propriedade de vários botões (ou qualquer outro controle que tenha uma interface de comando), cada um dos quais é identificado pelo CommandParameter
. Esses botões adicionam caracteres a uma InputString
propriedade, que é formatada como um número de telefone para a DisplayText
propriedade. Há também uma segunda propriedade do tipo ICommand chamada DeleteCharCommand
. Isso está vinculado a um botão de espaçamento entre trás, mas o botão deve ser desativado se não houver caracteres para excluir.
O exemplo a seguir 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 Command
propriedade do primeiro Button que está vinculada ao DeleteCharCommand
. Os outros botões são vinculados ao AddCharCommand
com um CommandParameter
que é o mesmo que o caractere que aparece no Button: