Partager via


Liaison de données et MVVM

Browse sample. Parcourir l’exemple

Le modèle Model-View-ViewModel (MVVM) applique une séparation entre trois couches logicielles : l’interface utilisateur XAML, appelée vue, données sous-jacentes, appelées modèle et intermédiaire entre la vue et le modèle, appelée viewmodel. La vue et le viewmodel sont souvent connectés par le biais de liaisons de données définies en XAML. La BindingContext vue est généralement une instance du viewmodel.

Important

L’interface utilisateur de l’application .NET multiplateforme (.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 viewmodel 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 viewmodels, mais pas des modèles, pour se concentrer sur la liaison de données entre les deux couches. Pour obtenir une explication détaillée de l’utilisation du modèle MVVM dans .NET MAUI, consultez Model-View-ViewModel (MVVM) dans les modèles d’application d’entreprise à l’aide de .NET MAUI. Pour obtenir un didacticiel qui vous aide à implémenter le modèle MVVM, consultez Mettre à niveau votre application avec des concepts MVVM.

Simple MVVM

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 x:Static balisage pour obtenir la date et l’heure actuelles de la propriété statique DateTime.Now dans l’espace System de noms :

<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 sur BindingContext un StackLayout. 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 le même BindingContext, et qu’ils peuvent contenir des liaisons à des propriétés de cet objet :

Screenshot of a page displaying the date and time.

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.

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. Lorsque vous pensez en termes de MVVM, le modèle et le 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 le viewmodel par le biais de liaisons de données. Dans MVVM, un modèle ignore le viewmodel et un viewmodel ignore la vue. Toutefois, vous personnalisez souvent les types exposés par le viewmodel 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 un viewmodel lié avec des liaisons de données.

L’exemple suivant montre un viewmodel 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));
}

Viewmodels implémente 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 PropertyChanged événement afin qu’il puisse être averti lorsqu’une propriété change et conserve la cible mise à jour avec la nouvelle valeur. Dans l’exemple de code précédent, la OnPropertyChanged méthode gère le déclenchement de l’événement tout en déterminant automatiquement le nom de la source de propriété : DateTime.

L’exemple suivant montre le code 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 l’utilisation ContentPageBindingContext des balises d’élément de propriété. Sinon, 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é. La capture d’écran suivante montre le résultat :

Screenshot of a page displaying the date and time via a viewmodel.

En outre, il est 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

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.

L’exemple suivant montre la HslViewModel conversion d’une Color valeur en Huevaleurs , Saturationet Luminosity les valeurs, puis de nouveau :

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 au Hue, Saturationet Luminosity les propriétés entraînent la modification de la Color propriété et les modifications apportées à la Color propriété entraînent la modification des trois autres propriétés. Cela peut ressembler à une boucle infinie, sauf que le viewmodel n’appelle pas l’événement PropertyChanged , sauf si la propriété a changé.

L’exemple XAML suivant contient une BoxView propriété dont Color la propriété est liée à la Color propriété du viewmodel, et trois Slider et trois Label vues liées aux Huepropriétés , Saturationet Luminosity aux propriétés :

<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 chacun Label est la valeur par défaut OneWay. Il doit uniquement afficher la valeur. Toutefois, la liaison par défaut sur chacune d’elles Slider est TwoWay. Cela permet l’initialisation Slider à partir du viewmodel. Lorsque le viewmodel est instancié, sa Color propriété est définie sur Aqua. Modification d’une Slider nouvelle valeur pour la propriété dans le viewmodel, qui calcule ensuite une nouvelle couleur :

MVVM using two-way data bindings.

Commandes

Parfois, une application a besoin d’aller au-delà des liaisons de propriétés en demandant à l’utilisateur de lancer des commandes qui affectent quelque chose dans le viewmodel. 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. Le viewmodel peut contenir des commandes, qui sont des méthodes exécutées en réaction à une activité spécifique dans l’affichage, comme un Button clic. Des liaisons de données sont définies entre ces commandes et le Button.

Pour autoriser une liaison de données entre un viewmodel et un Button viewmodel, les Button deux propriétés définies sont les suivantes :

Remarque

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

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

Le 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 , Buttonpassant à 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 ButtonCanExecute 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.

Vous pouvez utiliser la ou Command<T> la Command classe incluse dans .NET MAUI pour implémenter l’interfaceICommand. 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 .

L’exemple suivant montre un viewmodel 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 et CanExecute les Execute méthodes des commandes sont définies en tant que fonctions lambda dans le constructeur. Le viewmodel part du principe que la AddCharCommand propriété est liée à la Command propriété 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 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.

L’exemple suivant montre le code XAML qui consomme les KeypadViewModeléléments suivants :

<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 Command propriété du premier Button lié au DeleteCharCommand. Les autres boutons sont liés à l’objet AddCharCommand avec un CommandParameter caractère identique au caractère qui apparaît sur le Button:

Screenshot of a calculator using MVVM and commands.