Parte 5. De associações de dados a MVVM
O padrão de arquitetura MVVM (Model-View-ViewModel) foi inventado com XAML em mente. O padrão impõe uma separação entre três camadas de software — a interface do usuário XAML, chamada de View; os dados subjacentes, chamados de Modelo; e um intermediário entre o View e o Model, chamado ViewModel. O View e o ViewModel geralmente são conectados por meio de associações de dados definidas no arquivo XAML. O BindingContext para o View é geralmente uma instância do ViewModel.
Um ViewModel simples
Como uma introdução ao ViewModels, vamos primeiro olhar para um programa sem um.
Anteriormente, 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. Aqui está um programa que define uma declaração de namespace XML para o System
namespace:
xmlns:sys="clr-namespace:System;assembly=netstandard"
O programa pode usar x:Static
para obter a data e hora atuais da propriedade estática DateTime.Now
e definir esse DateTime
valor como on a BindingContext
StackLayout
:
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
é uma propriedade especial: 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 esse mesmo BindingContext
, e eles podem conter ligações simples às propriedades desse objeto.
No programa One-Shot DateTime, dois dos filhos contêm ligações a propriedades desse DateTime
valor, mas dois outros filhos contêm ligações que parecem estar faltando um caminho de vinculação. Isso significa que o DateTime
próprio valor é usado para o StringFormat
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
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">
<StackLayout BindingContext="{x:Static sys:DateTime.Now}"
HorizontalOptions="Center"
VerticalOptions="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}'}" />
</StackLayout>
</ContentPage>
O problema é que a data e a hora são definidas uma vez quando a página é criada pela primeira vez, e nunca mudam:
Um arquivo XAML pode exibir um relógio que sempre mostra a hora atual, mas precisa de algum código para ajudar. Ao pensar em termos de MVVM, o Model e ViewModel são classes escritas inteiramente em código. O View geralmente é um arquivo XAML que faz referência a propriedades definidas no ViewModel por meio de associações de dados.
Um Modelo apropriado é ignorante do ViewModel, e um ViewModel apropriado é ignorante do View. No entanto, muitas vezes, um programador adapta os tipos de dados expostos pelo ViewModel aos tipos de dados associados a interfaces de usuário específicas. Por exemplo, se um Model acessa um banco de dados que contém cadeias de caracteres ASCII de 8 bits, o ViewModel precisará converter entre essas cadeias de caracteres em cadeias de caracteres Unicode para acomodar o uso exclusivo de Unicode na interface do usuário.
Em exemplos simples de MVVM (como os mostrados aqui), muitas vezes não há nenhum modelo, e o padrão envolve apenas um View e ViewModel vinculados a associações de dados.
Aqui está um ViewModel para um relógio com apenas uma única propriedade chamada DateTime
, que atualiza essa DateTime
propriedade a cada segundo:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
ViewModels geralmente implementam a interface, o INotifyPropertyChanged
que significa que a classe dispara um PropertyChanged
evento sempre que uma de suas propriedades é alterada. O mecanismo de vinculação de dados em Xamarin.Forms 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.
Um relógio baseado neste ViewModel pode ser tão simples como isto:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label.BindingContext>
<local:ClockViewModel />
</Label.BindingContext>
</Label>
</ContentPage>
Observe como o ClockViewModel
é definido como o BindingContext
das marcas de elemento de Label
propriedade de uso. Como alternativa, você pode instanciar o ClockViewModel
em uma Resources
coleção e defini-lo como o através de uma StaticResource
extensão de BindingContext
marcação. Ou, o arquivo code-behind pode instanciar o ViewModel.
A Binding
extensão de marcação na Text
propriedade dos formata Label
a DateTime
propriedade. Aqui está a exibição:
Também é 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.
Aqui está uma classe chamada HslViewModel
que converte um Color
valor em Hue
, Saturation
e Luminosity
valores, e vice-versa:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
public class HslViewModel : INotifyPropertyChanged
{
double hue, saturation, luminosity;
Color color;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (hue != value)
{
hue = value;
OnPropertyChanged("Hue");
SetNewColor();
}
}
get
{
return hue;
}
}
public double Saturation
{
set
{
if (saturation != value)
{
saturation = value;
OnPropertyChanged("Saturation");
SetNewColor();
}
}
get
{
return saturation;
}
}
public double Luminosity
{
set
{
if (luminosity != value)
{
luminosity = value;
OnPropertyChanged("Luminosity");
SetNewColor();
}
}
get
{
return luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
OnPropertyChanged("Color");
Hue = value.Hue;
Saturation = value.Saturation;
Luminosity = value.Luminosity;
}
}
get
{
return color;
}
}
void SetNewColor()
{
Color = Color.FromHsla(Hue, Saturation, Luminosity);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
As alterações nas Hue
propriedades , Saturation
e Luminosity
fazem com que a Color
propriedade seja alterada e as alterações fazem Color
com que as outras três propriedades sejam alteradas. Isso pode parecer um loop infinito, exceto que a classe não invoca o PropertyChanged
evento, a menos que a propriedade tenha sido alterada. Isso põe fim ao loop de feedback incontrolável.
O arquivo XAML a seguir contém uma BoxView
propriedade cuja Color
está vinculada à Color
propriedade ViewModel e três Slider
e três Label
modos de exibição vinculados às Hue
propriedades , Saturation
e Luminosity
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<StackLayout Padding="10, 0">
<BoxView Color="{Binding Color}"
VerticalOptions="FillAndExpand" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
</StackLayout>
</ContentPage>
A associação em cada um Label
é o padrão OneWay
. Ele só precisa exibir o valor. Mas a vinculação em cada um Slider
é TwoWay
. Isso permite que o Slider
seja inicializado a partir do ViewModel. Observe que a Color
propriedade é definida como Aqua
quando o ViewModel é instanciado. Mas uma alteração no Slider
também precisa definir um novo valor para a propriedade no ViewModel, que calcula uma nova cor.
Comandando com ViewModels
Em muitos casos, o padrão MVVM é restrito à manipulação de itens de dados: objetos de interface do usuário no Exibir objetos de dados paralelos no ViewModel.
No entanto, às vezes, o View precisa conter botões que disparam várias ações no ViewModel. Mas o ViewModel não deve conter Clicked
manipuladores para os botões porque isso vincularia o ViewModel a um paradigma de interface do usuário específico.
Para permitir que ViewModels seja mais independente de objetos de interface do usuário específicos, mas ainda permitir que métodos sejam chamados dentro do ViewModel, existe uma interface de comando . Essa interface de comando é suportada pelos seguintes elementos no Xamarin.Forms:
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(e, portanto, tambémImageCell
)ListView
TapGestureRecognizer
Com exceção do SearchBar
elemento and ListView
, esses elementos definem duas propriedades:
Command
do tipoSystem.Windows.Input.ICommand
CommandParameter
do tipoObject
O SearchBar
define SearchCommand
e SearchCommandParameter
propriedades, enquanto o ListView
define uma RefreshCommand
propriedade do tipo ICommand
.
A ICommand
interface define 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 CommandParameter
propriedade para identificar objetos individuais Button
(ou outros elementos) que estão vinculados a essa propriedade ViewModel. Internamente, o Button
chama o Execute
método sempre que o usuário toca no Button
, passando para o Execute
método o seu CommandParameter
.
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 é acionado. Se CanExecute
retornar false
, o Button
se desativa e não gera Execute
chamadas.
Para obter ajuda com a adição de comandos aos seus ViewModels, Xamarin.Forms define duas classes que implementam ICommand
: Command
e Command<T>
onde T
é o tipo dos argumentos para Execute
e CanExecute
. Essas duas classes definem vários construtores mais um ChangeCanExecute
método que o ViewModel pode chamar para forçar o Command
objeto a disparar o CanExecuteChanged
evento.
Aqui está um ViewModel para um teclado simples que se destina a inserir números de telefone. Observe que o Execute
método e CanExecute
são definidos como funções lambda diretamente no construtor:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamlSamples
{
class KeypadViewModel : INotifyPropertyChanged
{
string inputString = "";
string displayText = "";
char[] specialChars = { '*', '#' };
public event PropertyChangedEventHandler PropertyChanged;
// Constructor
public KeypadViewModel()
{
AddCharCommand = new Command<string>((key) =>
{
// Add the key to the input string.
InputString += key;
});
DeleteCharCommand = new Command(() =>
{
// Strip a character from the input string.
InputString = InputString.Substring(0, InputString.Length - 1);
},
() =>
{
// Return true if there's something to delete.
return InputString.Length > 0;
});
}
// Public properties
public string InputString
{
protected set
{
if (inputString != value)
{
inputString = value;
OnPropertyChanged("InputString");
DisplayText = FormatText(inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
get { return inputString; }
}
public string DisplayText
{
protected set
{
if (displayText != value)
{
displayText = value;
OnPropertyChanged("DisplayText");
}
}
get { return displayText; }
}
// ICommand implementations
public ICommand AddCharCommand { protected set; get; }
public ICommand DeleteCharCommand { protected set; get; }
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
string formatted = str;
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
}
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;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Este ViewModel pressupõe que a AddCharCommand
propriedade está vinculada à Command
propriedade de vários botões (ou qualquer outra coisa 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 teclado a seguir não é tão visualmente sofisticado quanto poderia ser. Em vez disso, a marcação foi reduzida ao mínimo para demonstrar mais claramente o uso da interface de comando:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<Grid HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:KeypadViewModel />
</Grid.BindingContext>
<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>
<!-- Internal Grid for top row of items -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="0"
OutlineColor="Accent">
<Label Text="{Binding DisplayText}" />
</Frame>
<Button Text="⇦"
Command="{Binding DeleteCharCommand}"
Grid.Column="1"
BorderWidth="0" />
</Grid>
<Button Text="1"
Command="{Binding AddCharCommand}"
CommandParameter="1"
Grid.Row="1" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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>
A Command
propriedade do primeiro Button
que aparece nessa marcação está ligada ao DeleteCharCommand
, os demais estão ligados ao AddCharCommand
com um CommandParameter
que é o mesmo que o caractere que aparece no Button
rosto. Aqui está o programa em ação:
Invocando métodos assíncronos
Os comandos também podem invocar métodos assíncronos. Isso é obtido usando as async
palavras-chave e await
ao especificar o Execute
método:
DownloadCommand = new Command (async () => await DownloadAsync ());
Isso indica que o DownloadAsync
método é um Task
e deve ser aguardado:
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implementando um menu de navegação
O programa de exemplo que contém todo o código-fonte nesta série de artigos usa um ViewModel para sua home page. Este ViewModel é uma definição de uma classe curta com três propriedades chamadas Type
, Title
e Description
que contêm o tipo de cada uma das páginas de exemplo, um título e uma breve descrição. Além disso, o ViewModel define uma propriedade estática chamada All
que é uma coleção de todas as páginas no programa:
public class PageDataViewModel
{
public PageDataViewModel(Type type, string title, string description)
{
Type = type;
Title = title;
Description = description;
}
public Type Type { private set; get; }
public string Title { private set; get; }
public string Description { private set; get; }
static PageDataViewModel()
{
All = new List<PageDataViewModel>
{
// Part 1. Getting Started with XAML
new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
"Display a Label with many properties set"),
new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
"Interact with a Slider and Button"),
// Part 2. Essential XAML Syntax
new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
"Explore XAML syntax with the Grid"),
new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
"Explore XAML syntax with AbsoluteLayout"),
// Part 3. XAML Markup Extensions
new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
"Using resource dictionaries to share resources"),
new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
"Using the x:Static markup extensions"),
new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
"Explore XAML markup extensions"),
// Part 4. Data Binding Basics
new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
"Bind properties of two views on the page"),
new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
"Use Sliders with reverse bindings"),
new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
"Use a ListView with data bindings"),
// Part 5. From Data Bindings to MVVM
new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
"Obtain the current DateTime and display it"),
new PageDataViewModel(typeof(ClockPage), "Clock",
"Dynamically display the current time"),
new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
"Use a view model to select HSL colors"),
new PageDataViewModel(typeof(KeypadPage), "Keypad",
"Use a view model for numeric keypad logic")
};
}
public static IList<PageDataViewModel> All { private set; get; }
}
O arquivo XAML para MainPage
define uma ListBox
propriedade cuja ItemsSource
é definida como essa All
propriedade e que contém um TextCell
para exibir as Title
propriedades e Description
de cada página:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.MainPage"
Padding="5, 0"
Title="XAML Samples">
<ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
ItemSelected="OnListViewItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}"
Detail="{Binding Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
As páginas são mostradas em uma lista rolável:
O manipulador no arquivo code-behind é acionado quando o usuário seleciona um item. O manipulador define a SelectedItem
propriedade do ListBox
back para null
e, em seguida, instancia a página selecionada e navega até ela:
private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
(sender as ListView).SelectedItem = null;
if (args.SelectedItem != null)
{
PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
Page page = (Page)Activator.CreateInstance(pageData.Type);
await Navigation.PushAsync(page);
}
}
Vídeo
Xamarin Evolve 2016: MVVM simplificado com Xamarin.Forms e Prism
Resumo
O XAML é uma ferramenta poderosa para definir interfaces de usuário em Xamarin.Forms aplicativos, especialmente quando a vinculação de dados e o MVVM são usados. O resultado é uma representação limpa, elegante e potencialmente ferramental de uma interface do usuário com todo o suporte em segundo plano no código.
Links relacionados
- Parte 1. Introdução ao XAML
- Parte 2. Sintaxe essencial de XAML
- Parte 3. Extensões de Marcação XAML
- Parte 4. Conceitos básicos da associação de dados
Vídeos Relacionados
Encontre mais vídeos sobre o Xamarin no Channel 9 e no YouTube.