Partager via


Liaison de données et MVVM

Parcourez l’exemple. Parcourir l'exemple

Le modèle modèle-vue-vue modèle (MVVM) 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ée vue modèle. La vue et la vue modèle sont souvent connectése par le biais de liaisons de données définies dans XAML. La vue BindingContext est généralement une instance de la vue modèle.

Important

.NET Multi-platform App UI (.NET MAUI) marshale les mises à jour de liaison vers le thread d’interface utilisateur. Lorsque vous utilisez MVVM, cela vous permet de mettre à jour les propriétés de modèle-vue liées aux données à partir de n’importe quel thread, avec le moteur de liaison de .NET MAUI qui apporte les mises à jour au thread d’interface utilisateur.

Il existe plusieurs approches pour implémenter le modèle MVVM, et cet article se concentre sur une approche simple. Il utilise des vues et des vues modèles, mais pas des modèles, pour se concentrer sur la liaison de données entre les deux calques. Pour obtenir une explication détaillée de l’utilisation du modèle MVVM dans .NET MAUI, consultez modèle-vue-vue modèle (MVVM) dans les modèles d’application d’entreprise à l’aide de .NET MAUI. Pour obtenir un tutoriel qui vous aide à implémenter le modèle MVVM, consultez Mettre à niveau votre application avec des concepts MVVM.

MVVM simple

Dans les extensions de balisage XAML, 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. L’exemple suivant utilise l’extension de balisage x:Static pour obtenir la date et l’heure actuelles de la propriété statique DateTime.Now dans l’espace de noms System :

<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>

Dans cet exemple, la valeur récupérée DateTime est définie comme le BindingContext sur un StackLayout. Lorsque vous définissez le BindingContext sur un élément, il est hérité par tous les enfants de cet élément. Cela signifie que tous les enfants du StackLayout ont le même BindingContext, et qu’ils peuvent contenir des liaisons à des propriétés de cet objet :

Capture d’écran d’une page affichant la date et l’heure.

Toutefois, le problème est que la date et l’heure sont définies une fois lorsque la page est construite et initialisée, et ne change jamais.

Avertissement

Dans une classe qui dérive de BindableObject, seules les propriétés du type BindableProperty peuvent être liées. Par exemple, VisualElement.IsLoaded et Element.Parent ne peuvent pas être liés.

Une page XAML peut afficher une horloge qui affiche toujours l’heure actuelle, mais nécessite du code supplémentaire. Le modèle MVVM est un choix naturel pour les applications .NET MAUI lors de la liaison de données à partir de propriétés entre les objets visuels et les données sous-jacentes. En termes de MVVM, le modèle et la vue modèle 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 la vue modèle par le biais de liaisons de données. Dans MVVM, un modèle ignore la vue modèle et une vue modèle ignore la vue. Toutefois, vous personnalisez souvent les types exposés par la vue modèle aux types associés à l’interface utilisateur.

Remarque

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 une vue modèle liées avec des liaisons de données.

L’exemple suivant montre une vue modèle pour une horloge, avec une propriété unique nommée DateTime qui est mise à jour toutes les secondes :

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));
}

Les vues modèles implémentent généralement l’interface INotifyPropertyChanged, ce qui permet à une classe de déclencher l’événement PropertyChanged chaque fois qu’une de ses propriétés change. Le mécanisme de liaison de données dans .NET MAUI attache un gestionnaire à cet événement PropertyChanged afin d’être averti lorsqu’une propriété change et de mettre à jour la cible avec la nouvelle valeur. Dans l’exemple de code précédent, la méthode OnPropertyChanged gère le déclenchement de l’événement tout en déterminant automatiquement le nom de la source de la propriété : DateTime.

L’exemple suivant montre XAML qui consomme 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>

Dans cet exemple, ClockViewModel est défini sur le BindingContext de la ContentPage à l’aide des balises d’élément de propriété. Sinon, le fichier code-behind peut instancier la vue modèle.

L’extension de balisage Binding lie sur la propriété Text du Label formate la propriété DateTime. La capture d’écran suivante montre le résultat :

Capture d’écran d’une page affichant la date et l’heure via une vue modèle.

En outre, il est possible d’accéder aux propriétés individuelles de la propriété DateTime de la vue modèle en séparant les propriétés par des points :

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

MVVM interactif

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.

L’exemple suivant montre le HslViewModel qui convertit une valeur Color en valeurs Hue, Saturation et Luminosity, et inversement :

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));
}

Dans cet exemple, les modifications apportées aux propriétés Hue, Saturationet Luminosity entraînent la modification de la propriété Color et les modifications apportées à la propriété Color entraînent la modification des trois autres propriétés. Cela peut ressembler à une boucle infinie, sauf que la vue modèle n’appelle pas l’événement PropertyChanged, sauf si la propriété a changé.

L’exemple XAML suivant contient une BoxView dont la propriété Color est liée à la propriété Color de la vue modèle, ainsi que les trois vues Slider et les trois vues Label liées aux propriétés Hue, Saturation et 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>

La liaison sur chaque Label est la valeur par défaut OneWay. Seule la valeur doit être affichée. Toutefois, la liaison par défaut sur chaque Slider est TwoWay. Le Slider peut ainsi être initialisé à partir de la vue modèle. Lorsque la vue modèle est instanciée, sa propriété Color est définie sur Aqua. Une modification du Slider définit une nouvelle valeur pour la propriété dans la vue modèle, qui calcule ensuite une nouvelle couleur :

MVVM utilisant des liaisons de données bidirectionnelle.

Commandes

Parfois, une application a des besoins qui vont au-delà de ces liaisons de propriété en exigeant de l’utilisateur qu’il lance des commandes qui affectent un élément dans la vue modèle. Ces commandes sont généralement signalées par des clics de bouton ou des appuis tactiles et, en règle générale, elles sont traitées dans le fichier code-behind dans un gestionnaire pour l’événement Clicked du Button ou l’événement Tapped d’un TapGestureRecognizer.

L’interface d’exécution de commandes fournit une alternative à l’implémentation des commandes qui est beaucoup mieux adaptée à l’architecture MVVM. La vue modèle peut contenir des commandes, qui sont des méthodes exécutées en réaction à une activité spécifique dans la vue, comme un clic sur un Button. Des liaisons de données sont définies entre ces commandes et le Button.

Pour permettre une liaison de données entre un Button et une vue modèle, le Button définit deux propriétés :

Remarque

De nombreux autres contrôles définissent également les propriétés Command et CommandParameter.

L’interface ICommand est définie dans l’espace de noms System.Windows.Input et se compose de deux méthodes et d’un événement :

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

La vue modèle peut définir des propriétés de type ICommand. Vous pouvez ensuite lier ces propriétés à la propriété Command de chaque élément Button ou autre, ou peut-être à une vue personnalisée qui implémente cette interface. Vous pouvez éventuellement définir la propriété CommandParameter pour identifier des objets individuels Button (ou d’autres éléments) liés à cette propriété vue modèle. En interne, le Button appelle la méthode Execute chaque fois que l’utilisateur appuie sur , le Button, passant à la méthode Execute son CommandParameter.

La méthode CanExecute et l’événement CanExecuteChanged sont utilisés pour les cas où un clic sur Button peut être actuellement non valide, auquel cas le Button doit se désactiver. Le Button appelle CanExecute lorsque la propriété Command est définie pour la première fois et que chaque fois que l’événement CanExecuteChanged est déclenché. Si CanExecute retourne false, le Button se désactive et ne génère pas d’appels Execute.

Vous pouvez utiliser la classe Command ou Command<T> incluse dans .NET MAUI pour implémenter l’interface ICommand. Ces deux classes définissent plusieurs constructeurs plus une méthode ChangeCanExecute que la vue modèle peut appeler pour forcer l’objet Command à déclencher l’événement CanExecuteChanged.

L’exemple suivant montre une vue modèle pour un pavé numérique simple destiné à entrer des numéros de téléphone :

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));
}

Dans cet exemple, les méthodes Execute et CanExecute des commandes sont définies en tant que fonctions lambda dans le constructeur. La vue modèle part du principe que la propriété AddCharCommand est liée à la propriété Command de plusieurs boutons (ou tout autre contrôle ayant une interface de commande), chacun d’entre eux est identifié par le CommandParameter. Ces boutons ajoutent des caractères à une propriété, InputString qui est ensuite mise en forme comme numéro de téléphone pour la propriété DisplayText. Il existe également une deuxième propriété de type ICommand nommée DeleteCharCommand. Cela est lié à un bouton de retour arrière, mais le bouton doit être désactivé s’il n’y a pas de caractères à supprimer.

L’exemple suivant montre le XAML qui consomme le 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="&#x21E6;" 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>

Dans cet exemple, la propriété Command du premier Button lié à la DeleteCharCommand. Les autres boutons sont liés à la AddCharCommand avec un CommandParameter identique au caractère qui apparaît sur le Button :

Capture d’écran d’une calculatrice utilisant MVVM et des commandes.