Partie 5. Des liaisons de données à MVVM
Le modèle architectural Model-View-ViewModel (MVVM) a été inventé en XAML à l’esprit. Le modèle applique une séparation entre trois couches logicielles : l’interface utilisateur XAML, appelée View ; les données sous-jacentes, appelées modèle ; et un intermédiaire entre la vue et le modèle, appelé ViewModel. La vue et le ViewModel sont souvent connectés via des liaisons de données définies dans le fichier XAML. BindingContext pour la vue est généralement une instance de ViewModel.
A Simple ViewModel
En guise d’introduction aux ViewModels, examinons d’abord un programme sans un.
Précédemment, vous avez vu comment définir une nouvelle déclaration d’espace de noms XML pour permettre à un fichier XAML de référencer des classes dans d’autres assemblys. Voici un programme qui définit une déclaration d’espace de noms XML pour l’espace System
de noms :
xmlns:sys="clr-namespace:System;assembly=netstandard"
Le programme peut utiliser x:Static
pour obtenir la date et l’heure actuelles de la propriété statique DateTime.Now
et définir cette DateTime
valeur sur un StackLayout
BindingContext
:
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
est une propriété spéciale : lorsque vous définissez l’élément BindingContext
sur un élément, il est hérité par tous les enfants de cet élément. Cela signifie que tous les enfants de l’objet StackLayout
ont ce même BindingContext
, et qu’ils peuvent contenir des liaisons simples aux propriétés de cet objet.
Dans le programme DateTime one-Shot, deux des enfants contiennent des liaisons à des propriétés de cette DateTime
valeur, mais deux autres enfants contiennent des liaisons qui semblent manquer un chemin de liaison. Cela signifie que la DateTime
valeur elle-même est utilisée pour :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>
Le problème est que la date et l’heure sont définies une fois lorsque la page est créée pour la première fois et ne change jamais :
Un fichier XAML peut afficher une horloge qui affiche toujours l’heure actuelle, mais il a besoin d’un code pour vous aider. Lorsque vous pensez en termes de MVVM, le Modèle et ViewModel sont des classes écrites entièrement dans le code. La vue est souvent un fichier XAML qui fait référence aux propriétés définies dans ViewModel par le biais de liaisons de données.
Un modèle approprié est ignorant du ViewModel, et un ViewModel approprié est ignorant de la vue. Toutefois, souvent, un programmeur adapte les types de données exposés par ViewModel aux types de données associés à des interfaces utilisateur particulières. Par exemple, si un modèle accède à une base de données qui contient des chaînes ASCII de caractères 8 bits, ViewModel doit effectuer une conversion entre ces chaînes en chaînes Unicode pour prendre en charge l’utilisation exclusive d’Unicode dans l’interface utilisateur.
Dans des exemples simples de MVVM (comme ceux présentés ici), il n’existe souvent aucun modèle, et le modèle implique simplement une vue et viewModel liée avec des liaisons de données.
Voici un ViewModel pour une horloge avec une seule propriété nommée DateTime
, qui met à jour cette DateTime
propriété toutes les secondes :
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 implémente généralement l’interface INotifyPropertyChanged
, ce qui signifie que la classe déclenche un PropertyChanged
événement chaque fois qu’une de ses propriétés change. Le mécanisme de liaison de données dans Xamarin.Forms attache un gestionnaire à cet PropertyChanged
événement afin qu’il puisse être averti lorsqu’une propriété change et conserve la cible mise à jour avec la nouvelle valeur.
Une horloge basée sur ce ViewModel peut être aussi simple que celle-ci :
<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>
Notez comment le ClockViewModel
paramètre est défini sur les Label
BindingContext
balises d’élément de propriété using. Vous pouvez également instancier le ClockViewModel
fichier dans une Resources
collection et le BindingContext
définir via une StaticResource
extension de balisage. Ou, le fichier code-behind peut instancier le ViewModel.
Extension Binding
de balisage sur la Text
propriété des Label
formats de la DateTime
propriété. Voici l’affichage :
Il est également possible d’accéder aux propriétés individuelles de la DateTime
propriété de ViewModel en séparant les propriétés par des points :
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
Interactive MVVM
La machine virtuelle virtuelle est souvent utilisée avec des liaisons de données bidirectionnelle pour une vue interactive basée sur un modèle de données sous-jacent.
Voici une classe nommée HslViewModel
qui convertit une Color
valeur en Hue
valeurs, Saturation
et Luminosity
inversement :
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));
}
}
}
Les modifications apportées aux Hue
propriétés et Luminosity
aux propriétés Saturation
entraînent la modification de la Color
propriété et les modifications qui Color
entraînent la modification des trois autres propriétés. Cela peut sembler une boucle infinie, sauf que la classe n’appelle pas l’événement PropertyChanged
, sauf si la propriété a changé. Cela met fin à la boucle de commentaires incontrôlable autrement incontrôlable.
Le fichier XAML suivant contient une BoxView
propriété dont Color
la propriété est liée à la Color
propriété de ViewModel, et trois Slider
et trois Label
vues liées à la Hue
propriété , Saturation
et Luminosity
aux propriétés :
<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>
La liaison sur chacun Label
est la valeur par défaut OneWay
. Il doit uniquement afficher la valeur. Mais la liaison sur chacun d’eux Slider
est TwoWay
. Cela permet l’initialisation Slider
à partir de ViewModel. Notez que la Color
propriété est définie Aqua
lorsque ViewModel est instancié. Toutefois, une modification de la Slider
valeur doit également définir une nouvelle valeur pour la propriété dans ViewModel, qui calcule ensuite une nouvelle couleur.
Commandes avec ViewModels
Dans de nombreux cas, le modèle MVVM est limité à la manipulation d’éléments de données : les objets d’interface utilisateur dans les objets de données parallèles view dans ViewModel.
Toutefois, parfois, la vue doit contenir des boutons qui déclenchent diverses actions dans ViewModel. Mais ViewModel ne doit pas contenir Clicked
de gestionnaires pour les boutons, car cela lierait ViewModel à un paradigme d’interface utilisateur particulier.
Pour permettre aux ViewModels d’être plus indépendants des objets d’interface utilisateur particuliers, mais toujours autoriser les méthodes à appeler dans ViewModel, une interface de commande existe. Cette interface de commande est prise en charge par les éléments suivants dans Xamarin.Forms:
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(et donc aussiImageCell
)ListView
TapGestureRecognizer
À l’exception de l’élément et ListView
de l’élémentSearchBar
, ces éléments définissent deux propriétés :
Command
de typeSystem.Windows.Input.ICommand
CommandParameter
de typeObject
Définit SearchBar
et SearchCommandParameter
propriétés, tandis que la ListView
propriété définit une RefreshCommand
propriété de type ICommand
.SearchCommand
L’interface ICommand
définit deux méthodes et un événement :
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
ViewModel peut définir des propriétés de type ICommand
. Vous pouvez ensuite lier ces propriétés à la Command
propriété de chacun Button
ou d’un autre élément, ou peut-être une vue personnalisée qui implémente cette interface. Vous pouvez éventuellement définir la CommandParameter
propriété pour identifier des objets individuels Button
(ou d’autres éléments) liés à cette propriété ViewModel. En interne, les Button
appels de la Execute
méthode chaque fois que l’utilisateur appuie sur , Button
passant à la Execute
méthode son CommandParameter
.
La méthode et CanExecuteChanged
l’événement CanExecute
sont utilisés pour les cas où un Button
appui peut être actuellement non valide, auquel cas il Button
doit se désactiver. Appels Button
CanExecute
lorsque la propriété est définie pour la Command
première fois et chaque fois que l’événement CanExecuteChanged
est déclenché. Si CanExecute
elle est retournée false
, le Button
désactive lui-même et ne génère Execute
pas d’appels.
Pour obtenir de l’aide sur l’ajout de commandes à vos ViewModels, Xamarin.Forms définit deux classes qui implémentent ICommand
: Command
et Command<T>
où T
est le type des arguments vers Execute
et CanExecute
. Ces deux classes définissent plusieurs constructeurs plus une ChangeCanExecute
méthode que ViewModel peut appeler pour forcer l’objet Command
à déclencher l’événement CanExecuteChanged
.
Voici un ViewModel pour un pavé numérique simple destiné à entrer des numéros de téléphone. Notez que la méthode et CanExecute
la Execute
méthode sont définies en tant que fonctions lambda directement dans le constructeur :
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));
}
}
}
Ce ViewModel part du principe que la AddCharCommand
propriété est liée à la Command
propriété de plusieurs boutons (ou tout autre élément qui a une interface de commande), chacun d’entre eux est identifié par le CommandParameter
. Ces boutons ajoutent des caractères à une InputString
propriété, qui est ensuite mise en forme comme numéro de téléphone pour la DisplayText
propriété.
Il existe également une deuxième propriété de type ICommand
nommée DeleteCharCommand
. Cela est lié à un bouton d’espacement arrière, mais le bouton doit être désactivé s’il n’y a pas de caractères à supprimer.
Le pavé numérique suivant n’est pas aussi sophistiqué visuellement qu’il pourrait l’être. Au lieu de cela, le balisage a été réduit à un minimum pour illustrer plus clairement l’utilisation de l’interface de commande :
<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>
La Command
propriété de la première Button
qui apparaît dans ce balisage est liée au DeleteCharCommand
; le reste est lié au reste avec AddCharCommand
un CommandParameter
caractère identique au caractère qui apparaît sur le Button
visage. Voici le programme en action :
Appel de méthodes asynchrones
Les commandes peuvent également appeler des méthodes asynchrones. Pour ce faire, utilisez les mot clé et await
les async
mot clé lors de la spécification de la Execute
méthode :
DownloadCommand = new Command (async () => await DownloadAsync ());
Cela indique que la DownloadAsync
méthode est une Task
méthode et doit être attendue :
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implémentation d’un menu de navigation
L’exemple de programme qui contient tout le code source de cette série d’articles utilise un ViewModel pour sa page d’accueil. Ce ViewModel est une définition d’une classe courte avec trois propriétés nommées Type
, Title
et Description
qui contiennent le type de chacun des exemples de pages, un titre et une brève description. En outre, ViewModel définit une propriété statique nommée All
qui est une collection de toutes les pages du programme :
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; }
}
Le fichier XAML pour MainPage
définir une ListBox
propriété dont ItemsSource
la propriété est définie sur cette All
propriété et qui contient un TextCell
pour afficher les propriétés et Description
les Title
propriétés de chaque page :
<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>
Les pages sont affichées dans une liste à défilement :
Le gestionnaire du fichier code-behind est déclenché lorsque l’utilisateur sélectionne un élément. Le gestionnaire définit la SelectedItem
propriété du ListBox
retour null
, puis instancie la page sélectionnée et y accède :
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);
}
}
Vidéo
Xamarin Evolve 2016 : MVVM Simple avec Xamarin.Forms et Prism
Résumé
XAML est un outil puissant pour définir des interfaces utilisateur dans Xamarin.Forms les applications, en particulier lorsque la liaison de données et la machine virtuelle virtuelle sont utilisées. Le résultat est une représentation propre, élégante et potentiellement outilable d’une interface utilisateur avec toute la prise en charge en arrière-plan dans le code.
Liens associés
- Partie 1. Bien démarrer avec XAML
- Partie 2. Syntaxe XAML essentielle
- Partie 3. Extensions de balisage XAML
- Partie 4. Notions de base sur la liaison de données