Partie 5. Des liaisons de données à MVVM
Le modèle architectural MVVM (Model-ViewModel) a été inventé avec XAML à l’esprit. Le modèle applique une séparation entre trois couches logicielles : l’interface utilisateur XAML, appelée Vue ; 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. Le BindingContext pour la vue est généralement un instance du ViewModel.
Un mode d’affichage simple
En guise d’introduction à ViewModels, examinons d’abord un programme sans programme.
Vous avez vu plus tôt 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 de System
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 sur BindingContext
un StackLayout
:
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
est une propriété spéciale : lorsque vous définissez sur BindingContext
un élément, il est hérité par tous les enfants de cet élément. Cela signifie que tous les enfants de ont StackLayout
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 aux propriétés de cette DateTime
valeur, mais deux autres enfants contiennent des liaisons qui semblent manquer d’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 générée pour la première fois, et ne changent 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, model et ViewModel sont des classes entièrement écrites en code. La vue est souvent un fichier XAML qui fait référence aux propriétés définies dans ViewModel via des liaisons de données.
Un modèle approprié ignore le ViewModel, et un ViewModel approprié ignore la vue. Toutefois, un programmeur adapte souvent 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, le ViewModel doit convertir 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 (tels que ceux présentés ici), il n’existe souvent aucun modèle, et le modèle implique simplement une vue et un viewModel liés à 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émentent généralement l’interface INotifyPropertyChanged
, ce qui signifie que la classe déclenche un événement chaque fois qu’une PropertyChanged
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 maintenir la cible à jour avec la nouvelle valeur.
Une horloge basée sur ce ViewModel peut être aussi simple que ceci :
<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 est ClockViewModel
défini sur le BindingContext
des balises d’élément Label
de propriété using. Vous pouvez également instancier le ClockViewModel
dans une Resources
collection et le définir sur via une StaticResource
extension de BindingContext
balisage. Ou bien, le fichier code-behind peut instancier le ViewModel.
L’extension Binding
de balisage sur la Text
propriété du Label
met en forme la DateTime
propriété . Voici l’affichage :
Il est également possible d’accéder aux propriétés individuelles de la DateTime
propriété du ViewModel en séparant les propriétés par des points :
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
Interactive MVVM
MVVM est souvent utilisé 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
et 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));
}
}
}
Les modifications apportées aux Hue
propriétés , Saturation
et Luminosity
entraînent la modification de la Color
propriété, et les modifications apportées à Color
entraînent la modification des trois autres propriétés. Cela peut ressembler à 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 autrement incontrôlable.
Le fichier XAML suivant contient un BoxView
dont Color
la propriété est liée à la Color
propriété du ViewModel, et trois et trois Label
Slider
vues liées aux Hue
propriétés , Saturation
et 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>
La liaison sur chaque Label
est la valeur par défaut OneWay
. Il suffit d’afficher la valeur. Mais la liaison sur chacun d’eux Slider
est TwoWay
. Cela permet d’initialiser le Slider
à partir du ViewModel. Notez que la Color
propriété est définie sur Aqua
lorsque le ViewModel est instancié. Toutefois, une modification du Slider
doit également définir une nouvelle valeur pour la propriété dans le ViewModel, qui calcule ensuite une nouvelle couleur.
Commande avec ViewModels
Dans de nombreux cas, le modèle MVVM est limité à la manipulation d’éléments de données : objets d’interface utilisateur dans les objets de données parallèles Afficher les objets de données dans le ViewModel.
Toutefois, la vue doit parfois contenir des boutons qui déclenchent diverses actions dans le ViewModel. Toutefois, le ViewModel ne doit pas contenir de Clicked
gestionnaires pour les boutons, car cela lierait le ViewModel à un paradigme d’interface utilisateur particulier.
Pour permettre aux ViewModels d’être plus indépendants des objets d’interface utilisateur particuliers, mais tout en autorisant l’appel de méthodes dans le 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 SearchBar
et ListView
, ces éléments définissent deux propriétés :
Command
de typeSystem.Windows.Input.ICommand
CommandParameter
de typeObject
Définit SearchBar
SearchCommand
les propriétés et SearchCommandParameter
, tandis que définit ListView
une RefreshCommand
propriété de type ICommand
.
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) qui sont liés à cette propriété ViewModel. En interne, le Button
appelle la Execute
méthode chaque fois que l’utilisateur appuie sur , Button
en passant à la Execute
méthode son CommandParameter
.
La CanExecute
méthode et CanExecuteChanged
l’événement sont utilisés pour les cas où un Button
appui peut être actuellement non valide, auquel cas le Button
doit se désactiver lui-même. 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
retourne false
, le Button
se désactive lui-même et ne génère pas d’appels Execute
.
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 dans Execute
et CanExecute
. Ces deux classes définissent plusieurs constructeurs plus une ChangeCanExecute
méthode que le ViewModel peut appeler pour forcer l’objet Command
à déclencher l’événement CanExecuteChanged
.
Voici un ViewModel pour un clavier simple destiné à entrer des numéros de téléphone. Notez que la Execute
méthode et CanExecute
est définie en tant que fonctions lambda à droite 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 suppose 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 étant identifié par .CommandParameter
Ces boutons ajoutent des caractères à une InputString
propriété, qui est ensuite mise en forme en tant que 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
. Il 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 clavier suivant n’est pas aussi sophistiqué visuellement qu’il pourrait l’être. Au lieu de cela, le balisage a été réduit au 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é du premier Button
qui apparaît dans ce balisage est liée à ; DeleteCharCommand
les autres sont liés au AddCharCommand
avec un CommandParameter
qui est le même que le 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 async
mots clés et await
lors de la spécification de la Execute
méthode :
DownloadCommand = new Command (async () => await DownloadAsync ());
Cela indique que la DownloadAsync
méthode est un Task
et doit être attendue :
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Implémentation d’un menu de navigation
Le programme XamlSamples 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 chacune 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éfinit un ListBox
dont ItemsSource
la propriété est définie sur cette All
propriété et qui contient un TextCell
pour afficher les Title
propriétés et Description
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 dans le fichier code-behind est déclenché lorsque l’utilisateur sélectionne un élément. Le gestionnaire définit la SelectedItem
propriété du ListBox
back sur 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 fait simple avec Xamarin.Forms et Prism
Résumé
XAML est un outil puissant pour définir des interfaces utilisateur dans les applications, en Xamarin.Forms particulier lorsque la liaison de données et la machine virtuelle MVVM 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 d’arrière-plan dans le code.
Liens associés
- XamlSamples
- 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