Udostępnij za pośrednictwem


Powiązanie danych i mvVM

Browse sample. Przeglądanie przykładu

Wzorzec Model-View-ViewModel (MVVM) wymusza separację między trzema warstwami oprogramowania — interfejsem użytkownika XAML nazywanym widokiem, danymi bazowymi, nazywanymi modelem i pośrednikiem między widokiem a modelem, nazywanym modelem viewmodel. Widok i model widoku są często połączone za pośrednictwem powiązań danych zdefiniowanych w języku XAML. Element BindingContext dla widoku jest zwykle wystąpieniem modelu widoków.

Ważne

Interfejs użytkownika aplikacji wieloplatformowej platformy .NET (.NET MAUI) marshalsuje aktualizacje powiązań do wątku interfejsu użytkownika. W przypadku korzystania z maszyny MVVM umożliwia to aktualizowanie właściwości modelu widoku powiązanego z danymi z dowolnego wątku, a aparat powiązań programu .NET MAUI wprowadza aktualizacje do wątku interfejsu użytkownika.

Istnieje wiele podejść do implementowania wzorca MVVM, a ten artykuł koncentruje się na prostym podejściu. Używa widoków i modeli widoków, ale nie modeli, aby skupić się na powiązaniu danych między dwiema warstwami. Aby uzyskać szczegółowe wyjaśnienie użycia wzorca MVVM w programie .NET MAUI, zobacz Model-View-ViewModel (MVVM) w temacie Wzorce aplikacji dla przedsiębiorstw przy użyciu interfejsu MAUI platformy .NET. Aby zapoznać się z samouczkiem, który pomaga zaimplementować wzorzec MVVM, zobacz Uaktualnianie aplikacji za pomocą pojęć dotyczących maszyn wirtualnych MVVM.

Prosta maszyna MVVM

W rozszerzeniach znaczników XAML pokazano, jak zdefiniować nową deklarację przestrzeni nazw XML, aby umożliwić plikowi XAML odwoływanie się do klas w innych zestawach. W poniższym przykładzie x:Static użyto rozszerzenia znaczników, aby uzyskać bieżącą datę i godzinę z właściwości statycznej DateTime.NowSystem w przestrzeni nazw:

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

W tym przykładzie pobrana wartość jest ustawiana DateTime jako BindingContext w obiekcie StackLayout. Po ustawieniu BindingContext elementu on jest dziedziczony przez wszystkie elementy podrzędne tego elementu. Oznacza to, że wszystkie elementy podrzędne obiektu StackLayout mają ten sam BindingContextelement , i mogą zawierać powiązania z właściwościami tego obiektu:

Screenshot of a page displaying the date and time.

Problem polega jednak na tym, że data i godzina są ustawiane raz, gdy strona jest konstruowana i inicjowana, i nigdy nie zmienia się.

Strona XAML może wyświetlać zegar, który zawsze pokazuje bieżący czas, ale wymaga dodatkowego kodu. Wzorzec MVVM jest naturalnym wyborem dla aplikacji .NET MAUI, gdy dane są powiązane z właściwościami między obiektami wizualizacji a podstawowymi danymi. Podczas myślenia pod względem MVVM model i model widoków są klasami napisanymi w całości w kodzie. Widok jest często plikiem XAML, który odwołuje się do właściwości zdefiniowanych w modelu viewmodel za pomocą powiązań danych. W modelu MVVM model jest ignorowany przez model widoku, a model widoku nie jest wymykany z widoku. Jednak często dopasowuje się typy uwidocznione przez model widoku do typów skojarzonych z interfejsem użytkownika.

Uwaga

W prostych przykładach maszyny MVVM, takich jak pokazano tutaj, często nie ma modelu, a wzorzec obejmuje tylko widok i model widoku połączony z powiązaniami danych.

W poniższym przykładzie przedstawiono model widoku zegara z jedną właściwością o nazwie DateTime , która jest aktualizowana co sekundę:

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

Modele widoków zwykle implementują INotifyPropertyChanged interfejs, który umożliwia klasy wywoływanie PropertyChanged zdarzenia za każdym razem, gdy jedna z jego właściwości ulegnie zmianie. Mechanizm powiązania danych w programie .NET MAUI dołącza procedurę obsługi do tego PropertyChanged zdarzenia, aby można było otrzymywać powiadomienia o zmianie właściwości i aktualizowaniu obiektu docelowego przy użyciu nowej wartości. W poprzednim przykładzie OnPropertyChanged kodu metoda obsługuje wywoływanie zdarzenia podczas automatycznego określania nazwy źródła właściwości: DateTime.

W poniższym przykładzie pokazano kod XAML, który korzysta z ClockViewModelelementu :

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

W tym przykładzie ClockViewModel parametr jest ustawiany na BindingContextContentPage tagi elementów właściwości using. Alternatywnie plik związany z kodem może utworzyć wystąpienie modelu widoku.

Binding Rozszerzenie znaczników we Text właściwości Label formatuje DateTime właściwość . Poniższy zrzut ekranu przedstawia wynik:

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

Ponadto można uzyskać dostęp do poszczególnych właściwości DateTime właściwości modelu widoku, oddzielając właściwości kropkami:

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

Interaktywna maszyna MVVM

Maszyna MVVM jest często używana z dwukierunkowymi powiązaniami danych dla interaktywnego widoku opartego na bazowym modelu danych.

W poniższym przykładzie pokazano, HslViewModel że konwertuje Color wartość na Huewartości , Saturationi Luminosity i z powrotem:

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

W tym przykładzie Huezmiany właściwości , Saturationi Luminosity powodują Color zmianę właściwości, a zmiany Color właściwości powodują zmianę pozostałych trzech właściwości. Może to wydawać się nieskończoną pętlą, z tą różnicą, że model widoku nie wywołuje PropertyChanged zdarzenia, chyba że właściwość uległa zmianie.

Poniższy przykład XAML zawiera BoxView właściwość, której Color właściwość jest powiązana z właściwością Color modelu viewmodel, a trzy Slider i trzy Label widoki powiązane z Huewłaściwościami , Saturationi 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>

Powiązanie dla każdego Label z nich jest ustawieniem domyślnym OneWay. Musi tylko wyświetlić wartość. Jednak domyślne powiązanie dla każdego z nich Slider to TwoWay. Umożliwia to zainicjowanie elementu Slider z modelu viewmodel. Gdy model viewmodel jest tworzone wystąpienie, właściwość Color jest ustawiona na Aqua. Zmiana w obiekcie Slider ustawia nową wartość właściwości w modelu viewmodel, która następnie oblicza nowy kolor:

MVVM using two-way data bindings.

Komendanta

Czasami aplikacja musi wykraczać poza powiązania właściwości, wymagając od użytkownika zainicjowania poleceń, które mają wpływ na coś w modelu widoków. Te polecenia są zwykle sygnalizowane przez kliknięcia przycisków lub naciśnięcia palcem i tradycyjnie są przetwarzane w pliku za pomocą kodu w programie obsługi dla Clicked zdarzenia Button lub Tapped zdarzenia TapGestureRecognizer.

Interfejs wiersza polecenia zapewnia alternatywne podejście do implementowania poleceń, które są znacznie lepiej dostosowane do architektury MVVM. Model widoku może zawierać polecenia, które są metodami wykonywanymi w reakcji na określone działanie w widoku, takim jak Button kliknięcie. Powiązania danych są definiowane między tymi poleceniami a elementem Button.

Aby zezwolić na powiązanie danych między modelem a Button modelem widoków, Button definiuje dwie właściwości:

Uwaga

Wiele innych kontrolek definiuje Command również właściwości i CommandParameter .

Interfejs ICommand jest zdefiniowany w przestrzeni nazw System.Windows.Input i składa się z dwóch metod i jednego zdarzenia:

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

Model widoku może definiować właściwości typu ICommand. Następnie można powiązać te właściwości z właściwością Command każdego Button lub innego elementu, a może z widokiem niestandardowym, który implementuje ten interfejs. Opcjonalnie można ustawić CommandParameter właściwość, aby zidentyfikować poszczególne Button obiekty (lub inne elementy), które są powiązane z tą właściwością viewmodel. Wewnętrznie metoda wywołuje metodę za każdym razem, Button gdy użytkownik naciśnie metodę Button, przekazując ją do Execute metody CommandParameter.Execute

Metoda CanExecute i CanExecuteChanged zdarzenie są używane w przypadkach, Button gdy naciśnięcie może być obecnie nieprawidłowe, w takim przypadku Button polecenie powinno zostać wyłączone. Wywołania ButtonCanExecute , gdy Command właściwość jest ustawiana jako pierwsza, i za każdym razem, gdy zdarzenie jest wywoływane CanExecuteChanged . Jeśli CanExecute funkcja zwraca falsewartość , Button funkcja wyłącza się i nie generuje Execute wywołań.

Do zaimplementowania interfejsu można użyć klasy lub Command<T> dołączonej Command do programu ICommand .NET MAUI. Te dwie klasy definiują kilka konstruktorów oraz metodę ChangeCanExecute , którą model viewmodel może wywołać w celu wymuszenia Command wywołania obiektu w CanExecuteChanged celu wywołania zdarzenia.

W poniższym przykładzie przedstawiono model widoku dla prostej klawiatury przeznaczonej do wprowadzania numerów telefonów:

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

W tym przykładzie Execute metody i CanExecute dla poleceń są zdefiniowane jako funkcje lambda w konstruktorze. Model widoku zakłada, że AddCharCommand właściwość jest powiązana z właściwością Command kilku przycisków (lub innych kontrolek, które mają interfejs polecenia), z których każda jest identyfikowana przez CommandParameterelement . Przyciski te dodają znaki do InputString właściwości, która jest następnie sformatowana jako numer telefonu dla DisplayText właściwości. Istnieje również druga właściwość typu ICommand o nazwie DeleteCharCommand. Jest to powiązane z przyciskiem odstępu wstecz, ale przycisk powinien być wyłączony, jeśli nie ma znaków do usunięcia.

W poniższym przykładzie pokazano kod XAML, który korzysta z elementu 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>

W tym przykładzie Command właściwość pierwszego Button , która jest powiązana z elementem DeleteCharCommand. Inne przyciski są powiązane AddCharCommand z elementem , CommandParameter który jest taki sam jak znak wyświetlany w elemecie Button:

Screenshot of a calculator using MVVM and commands.