5. část: Od datových vazeb k MVVM
Model-View-ViewModel (MVVM) byl vynalezen s ohledem na XAML. Model vynucuje oddělení mezi třemi softwarovými vrstvami – uživatelským rozhraním XAML označovaným jako Zobrazení; podkladová data, označovaná jako model; a zprostředkovatelem mezi zobrazením a modelem, který se nazývá Model ViewModel. Zobrazení a model ViewModel jsou často propojeny prostřednictvím datových vazeb definovaných v souboru XAML. BindingContext pro zobrazení je obvykle instance Modelu ViewModel.
Jednoduchý model ViewModel
Jako úvod k modelu ViewModels se nejprve podíváme na program bez jednoho.
Dříve jste viděli, jak definovat novou deklaraci oboru názvů XML, která umožňuje souboru XAML odkazovat na třídy v jiných sestaveních. Tady je program, který definuje deklaraci oboru názvů XML pro System
obor názvů:
xmlns:sys="clr-namespace:System;assembly=netstandard"
Program může použít x:Static
k získání aktuálního data a času ze statické DateTime.Now
vlastnosti a nastavit tuto DateTime
hodnotu na BindingContext
hodnotu na :StackLayout
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
je speciální vlastnost: Při nastavení elementu BindingContext
je zděděna všemi podřízenými prvky tohoto prvku. To znamená, že všechny podřízené položky StackLayout
mají stejné BindingContext
a mohou obsahovat jednoduché vazby na vlastnosti tohoto objektu.
V programu One-Shot DateTime dva podřízené položky obsahují vazby na vlastnosti této DateTime
hodnoty, ale dvě další podřízené položky obsahují vazby, které zdánlivě chybí cesta vazby. To znamená, že samotná DateTime
hodnota se používá pro 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>
Problém je, že datum a čas jsou nastaveny jednou při prvním sestavení stránky a nikdy se nezmění:
Soubor XAML může zobrazit hodiny, které vždy zobrazují aktuální čas, ale potřebuje nějaký kód, který vám pomůže. Když uvažujete o MVVM, Model a ViewModel jsou třídy napsané zcela v kódu. Zobrazení je často soubor XAML, který odkazuje na vlastnosti definované v modelu ViewModel prostřednictvím datových vazeb.
Správný model je ignorant modelu ViewModel a správný Model ViewModel je ignorant zobrazení. Programátor ale často přizpůsobí datové typy vystavené modelem ViewModel datovým typům přidruženým ke konkrétním uživatelským rozhraním. Pokud například model přistupuje k databázi, která obsahuje 8bitové řetězce ASCII, model ViewModel by potřeboval převést mezi těmito řetězci na řetězce Unicode, aby vyhovoval výhradnímu použití unicode v uživatelském rozhraní.
Vjednoduchýchm modelech (MVVM) se v jednoduchých příkladech MVVM (například zde zobrazených) často žádný model vůbec nepoužívá a tento model zahrnuje pouze zobrazení a model ViewModel.
Tady je model ViewModel pro hodiny s pouze jednou vlastností s názvem DateTime
, která aktualizuje danou DateTime
vlastnost každou sekundu:
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 obecně implementuje INotifyPropertyChanged
rozhraní, což znamená, že třída aktivuje PropertyChanged
událost vždy, když se změní jedna z jejích vlastností. Mechanismus datové vazby připevňuje Xamarin.Forms obslužnou rutinu k této PropertyChanged
události, aby bylo možné upozornit, když se vlastnost změní a cíl se aktualizuje novou hodnotou.
Hodiny založené na tomto modelu ViewModel mohou být stejně jednoduché jako tyto:
<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>
Všimněte si, jak je nastavena ClockViewModel
na BindingContext
značky elementu Label
property. Alternativně můžete vytvořit instanci ClockViewModel
kolekce Resources
a nastavit ji na BindingContext
rozšíření prostřednictvím StaticResource
značek. Nebo může soubor s kódem vytvořit instanci modelu ViewModel.
Rozšíření Binding
značek u Text
vlastnosti Label
formátuje DateTime
vlastnost. Tady je zobrazení:
Přístup k jednotlivým vlastnostem objektu DateTime
ViewModel je také možný tak, že vlastnosti oddělíte tečkami:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
Interaktivní virtuální počítač MVVM
MVVM se často používá s obousměrnými datovými vazbami pro interaktivní zobrazení na základě podkladového datového modelu.
Tady je třída s názvem HslViewModel
, která převede Color
hodnotu na Hue
, Saturation
a Luminosity
hodnoty a naopak:
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));
}
}
}
Změny v objektu Hue
, Saturation
a Luminosity
vlastnosti způsobí Color
změnu vlastnosti a změny způsobí, že Color
se ostatní tři vlastnosti změní. Může to vypadat jako nekonečná smyčka s tím rozdílem, že třída nevyvolá PropertyChanged
událost, pokud se vlastnost nezměnila. Tím se ukončí jinak neovládnutelná smyčka zpětné vazby.
Následující soubor XAML obsahuje vlastnost BoxView
, jejíž Color
vlastnost je vázána na Color
vlastnost ViewModel, a tři Slider
a tři Label
zobrazení svázané s Hue
, Saturation
a Luminosity
vlastnosti:
<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>
Vazba na každém z nich Label
je výchozí OneWay
. Jenom musí zobrazit hodnotu. Ale vazba na každém Slider
z nich je TwoWay
. To umožňuje Slider
inicializaci z modelu ViewModel. Všimněte si, že Color
vlastnost je nastavena Aqua
při vytvoření instance Modelu ViewModel. Změna ale Slider
musí také nastavit novou hodnotu vlastnosti v Modelu ViewModel, která pak vypočítá novou barvu.
Příkazování pomocí modelů ViewModels
V mnoha případech je model MVVM omezen na manipulaci s datovými položkami: Objekty uživatelského rozhraní v zobrazení paralelních datových objektů v Modelu ViewModel.
Někdy ale zobrazení musí obsahovat tlačítka, která aktivují různé akce v modelu ViewModel. Model ViewModel však nesmí obsahovat Clicked
obslužné rutiny pro tlačítka, protože by to sváže model ViewModel s konkrétním paradigmatem uživatelského rozhraní.
Chcete-li povolit ViewModels být více nezávislý na konkrétních objektech uživatelského rozhraní, ale přesto povolit, aby metody být volány v rámci Modelu ViewModel, existuje příkazové rozhraní. Toto příkazové rozhraní je podporováno následujícími prvky v Xamarin.Forms:
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(a proto takéImageCell
)ListView
TapGestureRecognizer
S výjimkou elementu SearchBar
a ListView
elementu definují tyto prvky dvě vlastnosti:
Command
typuSystem.Windows.Input.ICommand
CommandParameter
typuObject
Definuje SearchBar
a SearchCommandParameter
vlastnosti, zatímco ListView
definuje RefreshCommand
vlastnost typu ICommand
.SearchCommand
Rozhraní ICommand
definuje dvě metody a jednu událost:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
Model ViewModel může definovat vlastnosti typu ICommand
. Tyto vlastnosti pak můžete svázat s Command
vlastností každého Button
nebo druhého prvku, nebo možná vlastní zobrazení, které implementuje toto rozhraní. Volitelně můžete nastavit CommandParameter
vlastnost pro identifikaci jednotlivých Button
objektů (nebo jiných prvků), které jsou vázány na tuto ViewModel vlastnost. Interně volá metodu pokaždé, Button
když uživatel klepne Button
, předat Execute
metodě jeho CommandParameter
.Execute
Metoda CanExecute
a CanExecuteChanged
událost se používají v případech, kdy Button
může být klepnutí aktuálně neplatné, v takovém případě Button
by se měl sám zakázat. Volání Button
CanExecute
při Command
prvním nastavení vlastnosti a při každém CanExecuteChanged
spuštění události. Pokud CanExecute
se vrátí false
, Button
zakáže se samotná a nevygeneruje Execute
volání.
Nápovědu k přidání příkazů do modelu ViewModels Xamarin.Forms definuje dvě třídy, které implementují ICommand
: Command
a Command<T>
kde T
je typ argumentů a Execute
CanExecute
. Tyto dvě třídy definují několik konstruktorů a metodu ChangeCanExecute
, kterou ViewModel může volat, aby objekt vynutil Command
vyvolání CanExecuteChanged
události.
Tady je model ViewModel pro jednoduchou klávesnici, která je určená pro zadávání telefonních čísel. Všimněte si, že v CanExecute
konstruktoru Execute
jsou funkce lambda definované jako funkce lambda:
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));
}
}
}
Tento model ViewModel předpokládá, že AddCharCommand
vlastnost je vázána na Command
vlastnost několika tlačítek (nebo cokoli jiného, co má příkazové rozhraní), z nichž každá je identifikována CommandParameter
. Tato tlačítka přidávají znaky do InputString
vlastnosti, která se pak naformátuje jako telefonní číslo vlastnosti DisplayText
.
Existuje také druhá vlastnost typu ICommand
s názvem DeleteCharCommand
. To je vázáno na tlačítko pro mezery zpět, ale tlačítko by mělo být zakázáno, pokud neexistují žádné znaky k odstranění.
Následující klávesnice není tak vizuálně sofistikovaná, jak by mohla. Místo toho byl redukován na minimum, aby bylo jasněji demonstrováno použití příkazového rozhraní:
<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>
Vlastnost Command
prvního Button
, který se zobrazí v této značce, je svázaná DeleteCharCommand
s AddCharCommand
; zbytek je svázán s znakem CommandParameter
, který je stejný jako znak, který se zobrazí na tváři Button
. Tady je program v akci:
Vyvolání asynchronních metod
Příkazy mohou také vyvolat asynchronní metody. Toho dosáhnete pomocí async
klíčových slov při await
zadávání Execute
metody:
DownloadCommand = new Command (async () => await DownloadAsync ());
To znamená, že DownloadAsync
metoda je a Task
měla by být očekávána:
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implementace navigační nabídky
Ukázkový program, který obsahuje veškerý zdrojový kód v této sérii článků, používá model ViewModel pro svou domovskou stránku. Tento Model ViewModel je definice krátké třídy se třemi vlastnostmi s názvem Type
, Title
a Description
které obsahují typ každé z ukázkových stránek, název a krátký popis. Kromě toho ViewModel definuje statickou vlastnost s názvem All
kolekce všech stránek v programu:
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; }
}
Soubor XAML pro MainPage
definuje, ListBox
jehož ItemsSource
vlastnost je nastavena na tuto All
vlastnost a která obsahuje TextCell
pro zobrazení Title
a Description
vlastnosti každé stránky:
<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>
Stránky se zobrazují v seznamu s možností posouvání:
Obslužná rutina v souboru kódu se aktivuje, když uživatel vybere položku. Obslužná rutina nastaví SelectedItem
vlastnost ListBox
zpět na null
vybranou stránku a pak vytvoří instanci vybrané stránky a přejde na ni:
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);
}
}
Video
Xamarin Evolve 2016: MVVM byl vyroben jednoduchým a prismem Xamarin.Forms
Shrnutí
XAML je výkonný nástroj pro definování uživatelských rozhraní v Xamarin.Forms aplikacích, zejména při použití datových vazeb a MVVM. Výsledkem je čistá, elegantní a potenciálně použitelná reprezentace uživatelského rozhraní se všemi podporami pozadí v kódu.
Související odkazy
- 1. část: Začínáme s jazykem XAML
- 2. část: Základní syntaxe XAML
- 3. část: Rozšíření značek XAML
- 4. část: Základy datových vazeb