Sdílet prostřednictvím


Datová vazba a MVVM

Browse sample. Procházení ukázky

Model-View-ViewModel (MVVM) vynucuje oddělení mezi třemi softwarovými vrstvami – uživatelským rozhraním XAML, označovaným jako zobrazení, podkladová data, označovaná jako model, a zprostředkovatelem mezi zobrazením a modelem, označovaným jako model viewmodel. Zobrazení a model viewmodel jsou často propojeny prostřednictvím datových vazeb definovaných v XAML. Zobrazení BindingContext je obvykle instancí modelu viewmodel.

Důležité

Rozhraní .NET Multi-Platform App UI (.NET MAUI) zařazuje aktualizace vazeb na vlákno uživatelského rozhraní. Při použití MVVM to umožňuje aktualizovat vlastnosti modelu zobrazení vázaného na data z libovolného vlákna pomocí vazbového modulu .NET MAUI, který přináší aktualizace do vlákna uživatelského rozhraní.

K implementaci modelu MVVM existuje několik přístupů a tento článek se zaměřuje na jednoduchý přístup. Používá zobrazení a modely zobrazení, ale ne modely, aby se zaměřily na datová vazba mezi těmito dvěma vrstvami. Podrobné vysvětlení použití vzoru MVVM v rozhraní .NET MAUI najdete v tématu Model-View-ViewModel (MVVM) ve vzorech podnikových aplikací pomocí rozhraní .NET MAUI. Kurz, který vám pomůže implementovat model MVVM, najdete v tématu Upgrade aplikace pomocí konceptů MVVM.

Jednoduchý MVVM

V rozšířeních značek XAML jste viděli, jak definovat novou deklaraci oboru názvů XML, která umožní souboru XAML odkazovat na třídy v jiných sestaveních. Následující příklad používá x:Static rozšíření značek k získání aktuální datum a čas ze statické DateTime.Now vlastnosti v System oboru názvů:

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

V tomto příkladu je načtená DateTime hodnota nastavena jako on BindingContext a StackLayout. Když nastavíte prvek BindingContext na element, zdědí ho všechny podřízené položky tohoto elementu. To znamená, že všechny podřízené položky StackLayout mají stejné BindingContexta mohou obsahovat vazby na vlastnosti tohoto objektu:

Screenshot of a page displaying the date and time.

Problém ale spočívá v tom, že datum a čas jsou nastaveny jednou při vytváření a inicializaci stránky a nikdy se nemění.

Stránka XAML může zobrazit hodiny, které vždy zobrazují aktuální čas, ale vyžadují další kód. Model MVVM je přirozenou volbou pro aplikace .NET MAUI, když jsou datové vazby z vlastností mezi vizuálními objekty a podkladovými daty. Když uvažujete o MVVM, model a model viewmodel jsou třídy napsané zcela v kódu. Zobrazení je často soubor XAML, který odkazuje na vlastnosti definované v modelu viewmodel prostřednictvím datových vazeb. V MVVM je model ignorant modelu viewmodel a model viewmodel je ignorant tohoto zobrazení. Často ale typy vystavené modelem viewmodel přizpůsobíte typům přidruženým k uživatelskému rozhraní.

Poznámka:

V jednoduchých příkladech MVVM, jako jsou zde uvedené, často neexistuje žádný model a vzor zahrnuje pouze zobrazení a model zobrazení propojený s datovými vazbami.

Následující příklad ukazuje model viewmodel pro hodiny s jednou vlastností s názvem DateTime , která se aktualizuje každou sekundu:

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 obvykle implementuje INotifyPropertyChanged rozhraní, které poskytuje schopnost třídy vyvolat PropertyChanged událost vždy, když se změní jedna z jejích vlastností. Mechanismus datové vazby v rozhraní .NET MAUI připojí obslužnou rutinu k této PropertyChanged události, aby bylo možné upozornit, když se změní vlastnost a cíl se aktualizuje o novou hodnotu. V předchozím příkladu OnPropertyChanged kódu metoda zpracovává vyvolání události při automatickém určování názvu zdroje vlastnosti: DateTime.

Následující příklad ukazuje XAML, který využívá 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>

V tomto příkladu ClockViewModelContentPage je nastavena na BindingContext značky elementu vlastnosti using. Alternativně může soubor s kódem vytvořit instanci modelu viewmodel.

Rozšíření Binding značek u Text vlastnosti Label formátuje DateTime vlastnost. Následující snímek obrazovky ukazuje výsledek:

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

Kromě toho je možné získat přístup k jednotlivým vlastnostem objektu DateTime viewmodel oddělením vlastností s tečkami:

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

Interaktivní virtuální počítač MVVM

MVVM se často používá s obousměrnými datovými vazbami pro interaktivní zobrazení na základě podkladového datového modelu.

Následující příklad ukazuje HslViewModel , že převede Color hodnotu na Hue, Saturationa Luminosity hodnoty a znovu:

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

V tomto příkladu HueSaturationzměny , a Luminosity vlastnosti způsobíColor, že vlastnost se změní a změny Color vlastnosti způsobí, že ostatní tři vlastnosti se změní. Může to vypadat jako nekonečná smyčka s tím rozdílem, že model viewmodel nevyvolá PropertyChanged událost, pokud se vlastnost nezměnila.

Následující příklad XAML obsahuje, BoxView jehož Color vlastnost je vázána Color na vlastnost viewmodel, a tři Slider a tři Label zobrazení vázané na Hue, Saturationa Luminosity vlastnosti:

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

Vazba na každém z nich Label je výchozí OneWay. Jenom musí zobrazit hodnotu. Výchozí vazba na každém z nich Slider je TwoWayvšak . To umožňuje Slider inicializaci z modelu viewmodel. Při vytvoření instance Color objektu viewmodel je vlastnost nastavena na Aquahodnotu . Změna v objektu Slider viewmodel nastaví novou hodnotu vlastnosti, která pak vypočítá novou barvu:

MVVM using two-way data bindings.

Velící

Někdy aplikace potřebuje, aby přesahovala vazby vlastností tím, že vyžaduje, aby uživatel inicioval příkazy, které mají vliv na něco v modelu viewmodel. Tyto příkazy jsou obecně signalizovány kliknutím na tlačítko nebo prstem klepněte a tradičně se zpracovávají v souboru kódu v obslužné rutině pro Clicked událost události Button nebo Tapped události TapGestureRecognizerudálosti .

Příkazové rozhraní poskytuje alternativní přístup k implementaci příkazů, které jsou pro architekturu MVVM mnohem vhodnější. Model viewmodel může obsahovat příkazy, které jsou metody spouštěné v reakci na konkrétní aktivitu v zobrazení, například Button kliknutí. Datové vazby jsou definovány mezi těmito příkazy a Button.

Chcete-li povolit datovou vazbu mezi modelem Button a modelem viewmodel, Button definuje dvě vlastnosti:

Poznámka:

Mnoho dalších ovládacích prvků také definuje Command a CommandParameter vlastnosti.

Rozhraní ICommand je definováno v oboru názvů System.Windows.Input a skládá se ze dvou metod a jedné události:

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

Model viewmodel může definovat vlastnosti typu ICommand. Tyto vlastnosti pak můžete svázat s Command vlastností každého Button nebo druhého prvku, nebo možná vlastní zobrazení, které implementuje toto rozhraní. Volitelně můžete nastavit CommandParameter vlastnost pro identifikaci jednotlivých Button objektů (nebo jiných prvků), které jsou vázány na tuto vlastnost viewmodel. Interně volá metodu pokaždé, Button když uživatel klepne Button, předat Execute metodě jeho CommandParameter.Execute

Metoda CanExecute a CanExecuteChanged událost se používají v případech, kdy Button může být klepnutí aktuálně neplatné, v takovém případě Button by se měl sám zakázat. Volání ButtonCanExecute při Command první nastavení vlastnosti a při každém CanExecuteChanged vyvolání události. Pokud CanExecute se vrátí false, Button zakáže se samotná a nevygeneruje Execute volání.

K implementaci ICommand rozhraní můžete použít Command rozhraní nebo Command<T> třídu, která je součástí rozhraní .NET MAUI. Tyto dvě třídy definují několik konstruktorů plus metodu ChangeCanExecute , kterou viewmodel může volat, aby Command objekt vyvolal CanExecuteChanged událost.

Následující příklad ukazuje model viewmodel pro jednoduchou klávesnici, která je určena pro zadávání telefonních čísel:

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

V tomto příkladu Execute jsou příkazy a CanExecute metody definované jako funkce lambda v konstruktoru. Model viewmodel předpokládá, že AddCharCommand vlastnost je vázána na Command vlastnost několika tlačítek (nebo cokoli jiného ovládacích prvků, které mají rozhraní příkazů), z nichž každá je identifikována CommandParameter. Tato tlačítka přidávají znaky do InputString vlastnosti, která se pak naformátuje jako telefonní číslo vlastnosti DisplayText . Existuje také druhá vlastnost typu ICommand s názvem DeleteCharCommand. To je vázáno na tlačítko pro mezery zpět, ale tlačítko by mělo být zakázáno, pokud neexistují žádné znaky k odstranění.

Následující příklad ukazuje XAML, který využívá 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>

V tomto příkladu Command vlastnost první Button , která je vázána na DeleteCharCommand. Ostatní tlačítka jsou svázaná AddCharCommand s znakem CommandParameter , který je stejný jako znak, který se zobrazí na Button:

Screenshot of a calculator using MVVM and commands.