Teilen über


Datenbindungen und MVVM

Beispiel durchsuchen.Durchsuchen Sie das Beispiel

Das Model-View-ViewModel (MVVM)-Muster erzwingt eine Trennung zwischen drei Softwareschichten - der XAML-Benutzeroberfläche, genannt View, den zugrunde liegenden Daten, genannt Model, und einem Vermittler zwischen View und Model, genannt Viewmodel. Der View und das Viewmodel sind oft durch in XAML definierte Datenbindungen verbunden. Der BindingContext für View ist normalerweise eine Instanz des Viewmodels.

Wichtig

.NET Multiplatform App UI (.NET MAUI) überträgt Bindungsaktualisierungen an den UI-Thread. Wenn Sie MVVM verwenden, können Sie datengebundene Viewmodel-Eigenschaften aus einem beliebigen Thread aktualisieren, wobei das Bindungsmodul von .NET MAUI die Aktualisierungen in den UI-Thread bringt.

Es gibt mehrere Ansätze zur Implementierung des MVVM-Musters, und dieser Artikel konzentriert sich auf einen einfachen Ansatz. Er verwendet Views und Viewmodels, aber keine Models, um sich auf die Datenbindung zwischen den beiden Schichten zu konzentrieren. Eine ausführliche Erklärung zur Verwendung des MVVM-Musters in .NET MAUI finden Sie unter Model-View-ViewModel (MVVM) in Enterprise Application Patterns using .NET MAUI. Ein Tutorial, das Ihnen hilft, das MVVM-Muster zu implementieren, finden Sie unter Aktualisieren Ihrer App mit MVVM Konzepten.

Einfaches MVVM

In XAML-Markuperweiterungen haben Sie gesehen, wie Sie eine neue XML-Namespacedeklaration definieren, damit eine XAML-Datei auf Klassen in anderen Assemblys verweist. Das folgende Beispiel verwendet die Markuperweiterung x:Static, um das aktuelle Datum und die Uhrzeit aus der statischen Eigenschaft DateTime.Now im Namespace System abzurufen:

<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"
             x:DataType="sys:DateTime">

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

In diesem Beispiel wird der abgerufene DateTime-Wert als BindingContext auf einen StackLayout festgelegt. Wenn Sie BindingContext für ein Element festlegen, wird es von allen untergeordneten Elementen dieses Elements geerbt. Das bedeutet, dass alle untergeordneten Elemente des StackLayout das gleiche BindingContext haben und Bindungen zu Eigenschaften dieses Objekts enthalten können:

Screenshot einer Seite, auf der das Datum und die Uhrzeit angezeigt werden.

Das Problem besteht jedoch darin, dass das Datum und die Uhrzeit einmal festgelegt werden, wenn die Seite erstellt und initialisiert wird und sich nie ändern.

Warnung

In einer Klasse, die von BindableObject abgeleitet ist, sind nur Eigenschaften vom Typ BindableProperty bindbar. Zum Beispiel sind VisualElement.IsLoaded und Element.Parent nicht bindbar.

Eine XAML-Seite kann eine Uhr anzeigen, die immer die aktuelle Zeit anzeigt, aber dafür ist zusätzlicher Code erforderlich. Das MVVM-Muster ist eine natürliche Wahl für .NET MAUI Apps, wenn die Datenbindung von Eigenschaften zwischen visuellen Objekten und den zugrunde liegenden Daten erfolgt. Im Sinne von MVVM sind Model und Viewmodel Klassen, die vollständig in Code geschrieben sind. Bei der Ansicht handelt es sich häufig um eine XAML-Datei, die über Datenbindungen auf im ViewModel definierte Eigenschaften verweist. In MVVM kennt ein Model das ViewModel nicht, und ViewModel kennt View nicht. Häufig passen Sie jedoch die Typen an, die vom ViewModel auf die Typen angepasst werden, die der Benutzeroberfläche zugeordnet sind.

Hinweis

In einfachen MVVM-Beispielen, wie den hier gezeigten, gibt es oft überhaupt kein Model, und das Muster umfasst nur einen View und ein ViewModel, die mit Datenbindungen verknüpft sind.

Das folgende Beispiel zeigt ein ViewModel für eine Uhr, mit einer einzigen Eigenschaft namens DateTime, die jede Sekunde aktualisiert wird:

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 implementieren normalerweise die INotifyPropertyChanged-Schnittstelle, die einer Klasse die Möglichkeit gibt, das PropertyChanged-Ereignis auszulösen, wenn sich eine ihrer Eigenschaften ändert. Der Datenbindungsmechanismus in .NET MAUI fügt einen Handler an dieses PropertyChanged-Ereignis an, damit er benachrichtigt werden kann, wenn eine Eigenschaft geändert wird und das Ziel mit dem neuen Wert aktualisiert wird. Im vorherigen Code-Beispiel behandelt die OnPropertyChanged-Methode das Auslösen des Ereignisses und bestimmt dabei automatisch den Namen der Eigenschaftsquelle: DateTime.

Das folgende Beispiel zeigt XAML, das ClockViewModel verwendet:

<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"
             x:DataType="local:ClockViewModel">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="18"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

In diesem Beispiel wird ClockViewModel mit Hilfe von Eigenschaftselement-Tags auf BindingContext des ContentPage gesetzt. Alternativ dazu könnte die Code-Behind-Datei das ViewModel instanziieren.

Die Markuperweiterung Binding der Text-Eigenschaft des Label formatiert die DateTime-Eigenschaft. Der folgende Screenshot zeigt das Ergebnis:

Screenshot einer Seite, auf der das Datum und die Uhrzeit eines Ansichtsmodells angezeigt werden.

Darüber hinaus ist es möglich, auf einzelne Eigenschaften der DateTime-Eigenschaft des ViewModels zuzugreifen, indem die Eigenschaften durch Punkte getrennt werden:

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

Interaktive MVVM

MVVM wird häufig mit bidirektionale Datenbindungen für eine interaktive Ansicht basierend auf einem zugrunde liegenden Datenmodell verwendet.

Das folgende Beispiel zeigt den HslViewModel, der einen Color-Wert in Hue-, Saturation- und Luminosity-Werte und wieder zurück umwandelt:

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

In diesem Beispiel führen Änderungen an den Eigenschaften Hue, Saturation und Luminosity zu einer Änderung der Eigenschaft Color und Änderungen an der Eigenschaft Color zu einer Änderung der anderen drei Eigenschaften. Dies könnte wie eine Endlosschleife aussehen, außer dass das ViewModel das PropertyChanged-Ereignis erst dann aufruft, wenn sich die Eigenschaft geändert hat.

Das folgende XAML-Beispiel enthält ein BoxView, dessen Color-Eigenschaft an die Color-Eigenschaft des ViewModels gebunden ist, sowie drei Slider- und drei Label-Ansichten, die an die Hue-, Saturation- und Luminosity-Eigenschaften gebunden sind:

<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"
             x:DataType="local:HslViewModel">
    <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>

Die Bindung an jede Label ist Standard OneWay. Der Wert muss nur angezeigt werden. Die Standardbindung für jede Slider ist jedoch TwoWay. Dadurch kann Slider vom ViewModel aus initialisiert werden. Wenn das ViewModel instanziiert wird, wird seine Color Eigenschaft auf Aqua festgelegt. Eine Änderung in einem Slider setzt einen neuen Wert für die Eigenschaft im ViewModel, das dann eine neue Farbe berechnet:

MVVM mit bidirektionale Datenbindungen.

Befehle

Manchmal hat eine Anwendung Anforderungen, die über Eigenschaftsbindungen hinausgehen, weil der Benutzer Befehle auslösen muss, die etwas im ViewModel beeinflussen. Diese Befehle werden in der Regel ausgeführt, indem Sie auf Schaltflächen klicken oder auf den Bildschirm tippen. Normalerweise werden sie in der CodeBehind-Datei in einem Handler für das Clicked-Ereignis von Button oder das Tapped-Ereignis von TapGestureRecognizer verarbeitet.

Über die Befehlsschnittstelle kann jedoch ein alternativer Ansatz für das Implementieren von Befehlen verwendet werden, der für die MVVM-Architektur besser geeignet ist. Das Viewmodel kann Befehle enthalten, d.h. Methoden, die als Reaktion auf eine bestimmte Aktivität in der Ansicht ausgeführt werden, wie etwa einen Button Klick. Datenbindungen werden zwischen diesen Befehlen und Button definiert.

Um eine Datenbindung zwischen einem Button und einem Viewmodel zu ermöglichen, definiert das Button zwei Eigenschaften:

Hinweis

Viele andere Steuerelemente definieren auch die Eigenschaften Command und CommandParameter.

Die ICommand-Schnittstelle ist im Namespace System.Windows.Input definiert und besteht aus zwei Methoden und einem Ereignis:

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

Das ViewModel kann Eigenschaften vom Typ ICommand definieren. Sie können diese Eigenschaften dann an die Command-Eigenschaft jedes Button-Elements oder anderen Elements binden, oder vielleicht an eine benutzerdefinierte Ansicht, die diese Schnittstelle implementiert. Sie können optional die CommandParameter-Eigenschaft festlegen, um einzelne Button-Objekte (oder andere Elemente) zu identifizieren, die an diese ViewModel-Eigenschaft gebunden sind. Intern ruft Button die Methode Execute auf, wenn der Benutzer auf Button tippt, und übergibt der Methode Execute an sein CommandParameter.

Die CanExecute-Methode und das CanExecuteChanged-Ereignis werden für Fälle verwendet, in denen ein Button-Tippen derzeit ungültig sein könnte. In diesem Fall sollte sich Button selbst deaktivieren. Button ruft CanExecute auf, wenn die Command-Eigenschaft zum ersten Mal gesetzt wird und wenn das CanExecuteChanged-Ereignis ausgelöst wird. Wenn CanExecute false zurückgibt, deaktiviert sich Button selbst und erzeugt keine Execute-Aufrufe.

Sie können die Command- oder Command<T>-Klasse verwenden, die in .NET MAUI enthalten ist, um die ICommand-Schnittstelle zu implementieren. Diese beiden Klassen definieren mehrere Konstruktoren sowie eine ChangeCanExecute-Methode, die das ViewModel aufrufen kann, um das Command-Objekt zu zwingen, das CanExecuteChanged-Ereignis auszulösen.

Das folgende Beispiel zeigt ein ViewModel für ein einfaches Tastenfeld, das für die Eingabe von Telefonnummern vorgesehen ist:

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

In diesem Beispiel werden die Methoden Execute und CanExecute für die Befehle als Lambda-Funktionen im Konstruktor definiert. ViewModel geht davon aus, dass die AddCharCommand-Eigenschaft an die Command-Eigenschaft mehrerer Schaltflächen (oder anderer Steuerelemente, die über eine Befehlsschnittstelle verfügen) gebunden ist, von denen jedes durch CommandParameter gekennzeichnet ist. Diese Tasten fügen Zeichen zu einer InputString-Eigenschaft hinzu, die dann als Telefonnummer für die DisplayText-Eigenschaft formatiert wird. Es gibt auch eine zweite Eigenschaft vom Typ ICommand namens DeleteCharCommand. Dies ist an eine Rücktasten-Schaltfläche gebunden, aber die Schaltfläche sollte deaktiviert werden, wenn keine Zeichen zu löschen sind.

Das folgende Beispiel zeigt die XAML, die KeypadViewModel verwendet:

<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"
             x:DataType="local:KeypadViewModel">
    <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>

In diesem Beispiel ist es die Command-Eigenschaft der ersten Button, die an die DeleteCharCommand gebunden ist. Die anderen Schaltflächen sind mit dem AddCharCommand mit einem CommandParameter verbunden, das dem Zeichen entspricht, das auf dem Button erscheint:

Screenshot eines Rechners mit MVVM und Befehlen.