Поделиться через


Привязка данных и MVVM

Browse sample.Просмотрите пример. Обзор примера

Шаблон Model-View-ViewModel (MVVM) обеспечивает разделение между тремя слоями программного обеспечения — пользовательским интерфейсом XAML, который называется представлением, базовыми данными, называемыми моделью, и посредником между представлением и моделью, называемой моделью представления. Представление и модель представления часто подключаются через привязки данных, определенные в XAML. Представление BindingContext обычно является экземпляром модели представления.

Внимание

Многоплатформенный пользовательский интерфейс приложения .NET (.NET MAUI) маршалирует обновления привязки к потоку пользовательского интерфейса. При использовании MVVM это позволяет обновлять свойства представления, привязанные к данным, из любого потока, с подсистемой привязки .NET MAUI, которая позволяет обновлять поток пользовательского интерфейса.

Существует несколько подходов к реализации шаблона MVVM, и в этой статье рассматривается простой подход. Он использует представления и модели представления, но не модели, чтобы сосредоточиться на привязке данных между двумя уровнями. Подробное описание использования шаблона MVVM в .NET MAUI см. в статье Model-View-ViewModel (MVVM) в шаблонах корпоративных приложений с помощью .NET MAUI. Руководство по реализации шаблона MVVM см. в разделе "Обновление приложения с помощью концепций MVVM".

Простой MVVM

В расширениях разметки XAML вы узнали , как определить новое объявление пространства имен XML, чтобы позволить XAML-файлу ссылаться на классы в других сборках. В следующем примере используется x:Static расширение разметки для получения текущей даты и времени из статического DateTime.Now свойства в 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"
             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>

В этом примере извлекаемое DateTime значение задается как значение в объекте StackLayoutBindingContext . При установке BindingContext элемента он наследуется всеми дочерними элементами этого элемента. Это означает, что все дочерние элементы StackLayout одного и того же BindingContext, и они могут содержать привязки к свойствам этого объекта:

Screenshot of a page displaying the date and time.Снимок экрана: страница с датой и временем.

Однако проблема заключается в том, что дата и время задаются один раз при создании и инициализации страницы и никогда не изменяются.

Предупреждение

В классе, наследуемом от BindableObjectсвойства типа BindableProperty , можно привязать. Например, VisualElement.IsLoaded и Element.Parent не являются привязываемыми.

Страница XAML может отображать часы, которые всегда показывают текущее время, но требует дополнительного кода. Шаблон MVVM является естественным выбором для приложений .NET MAUI при привязке данных из свойств между визуальными объектами и базовыми данными. При мышлении с точки зрения MVVM модель и модель представления являются классами, написанными полностью в коде. Представление часто представляет собой XAML-файл, который ссылается на свойства, определенные в представлении с помощью привязок данных. В MVVM модель не учитывается в режиме просмотра, и модель представления не учитывается. Однако часто вы настраиваете типы, предоставляемые в режиме просмотра, с типами, связанными с пользовательским интерфейсом.

Примечание.

В простых примерах MVVM, таких как показанные здесь, часто нет модели вообще, и шаблон включает только представление и представление, связанное с привязками данных.

В следующем примере показана модель представления для часов с одним свойством, которое DateTime обновляется каждую секунду:

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 обычно реализует INotifyPropertyChanged интерфейс, который предоставляет возможность для класса вызывать PropertyChanged событие всякий раз, когда одно из его свойств изменяется. Механизм привязки данных в .NET MAUI присоединяет обработчик к этому PropertyChanged событию, чтобы его можно было получать уведомления при изменении свойства и обновлять целевой объект новым значением. В предыдущем примере OnPropertyChanged кода метод обрабатывает событие при автоматическом определении имени источника свойства: DateTime

В следующем примере показано XAML, которое использует 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"
             x:DataType="local:ClockViewModel">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

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

В этом примере ClockViewModel задано значение BindingContext тегов ContentPage элементов свойства. Кроме того, файл программной части может создать экземпляр viewmodel.

Binding Расширение разметки Label для Text свойства форматирования DateTime свойства. На следующем снимка экрана показан результат:

Screenshot of a page displaying the date and time via a viewmodel.Снимок экрана: страница с датой и временем с помощью модели просмотра.

Кроме того, можно получить доступ к отдельным свойствам DateTime свойства объекта viewmodel, разделив свойства с точками:

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

Интерактивный MVVM

MVVM часто используется с двусторонними привязками данных для интерактивного представления на основе базовой модели данных.

В следующем примере показано HslViewModel , как преобразовать Color значение в Hue, Saturationа также Luminosity значения и вернуться обратно:

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

В этом примере изменения свойств HueSaturationи Luminosity свойств приводят Color к изменению свойства, а изменение Color свойства приводит к изменению других трех свойств. Это может показаться бесконечным циклом, за исключением того, что viewmodel не вызывает PropertyChanged событие, если свойство не изменилось.

Следующий пример XAML содержит свойство, свойство которого привязано BoxView к Color свойству viewmodel, а три и три SliderLabel представления привязаны к Hueсвойствам , Saturationа LuminosityColor также свойствам:

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

Привязка для каждой из них Label — это значение по умолчанию OneWay. Оно должно отображаться только для отображения значения. Однако привязка по умолчанию для каждой из них Slider имеет значение TwoWay. Это позволяет Slider инициализироваться из модели представления. При создании экземпляра Color объекта viewmodel для свойства задано значение Aqua. Изменение в Slider наборе нового значения для свойства в режиме просмотра, которое затем вычисляет новый цвет:

MVVM using two-way data bindings.MVVM с двусторонними привязками данных.

Система команд

Иногда приложению требуется выйти за рамки привязок свойств, требуя от пользователя инициировать команды, влияющие на что-то в режиме просмотра. Эти команды обычно обозначаются путем нажатия кнопки или касания пальцем и традиционно обрабатываются в файле с выделенным кодом в обработчике для события Clicked объекта Button или события Tapped объекта TapGestureRecognizer.

Командный интерфейс предоставляет альтернативный подход к реализации команд, который гораздо лучше подходит для архитектуры MVVM. Модель представления может содержать команды, которые являются методами, выполняемыми в реакции на определенное действие в представлении, например щелчком Button мыши. Привязки данных определяются между этими командами и объектом Button.

Чтобы разрешить привязку данных между a Button и viewmodel, Button определяет два свойства:

Примечание.

Многие другие элементы управления также определяют Command и CommandParameter свойства.

Интерфейс ICommand определяется в пространстве имен System.Windows.Input и состоит из двух методов и одного события:

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

Модель представления может определять свойства типа ICommand. Затем эти свойства можно привязать к Command свойству каждого Button или другого элемента или, возможно, пользовательское представление, реализующее этот интерфейс. При необходимости можно задать CommandParameter свойство для идентификации отдельных Button объектов (или других элементов), привязанных к этому свойству viewmodel. Внутри этого метода вызывается всякий раз, Button когда пользователь нажимает методButton, передав его методу ExecuteCommandParameter.Execute

Метод CanExecute и CanExecuteChanged событие используются для случаев, когда Button касание в настоящее время может быть недопустимым, в этом случае Button следует отключить сам. Вызовы CanExecuteButton при Command первом наборе свойства и при CanExecuteChanged возникновении события. Если CanExecute возвращается false, Button он отключает и не создает Execute вызовы.

Для реализации интерфейса можно использовать класс или Command<T> класс, включенный CommandICommand в .NET MAUI. Эти два класса определяют несколько конструкторов, а также ChangeCanExecute метод, который может вызвать метод viewmodel, чтобы Command принудительно вызвать объект для вызова CanExecuteChanged события.

В следующем примере показана модель представления для простой клавиатуры, предназначенной для ввода телефонных номеров:

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

В этом примере Execute команды определяются CanExecute как лямбда-функции в конструкторе. В представлении предполагается, что AddCharCommand свойство привязано к Command свойству нескольких кнопок (или других элементов управления, имеющих командный интерфейс), каждое из которых определяется элементом CommandParameterуправления. Эти кнопки добавляют символы в InputString свойство, которое затем форматируется как номер телефона для DisplayText свойства. Существует также второе свойство типа ICommand с именем DeleteCharCommand. Это привязано к кнопке с интервалом назад, но кнопка должна быть отключена, если нет символов для удаления.

В следующем примере показан КОД XAML, который использует 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"
             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>

В этом примере Command свойство первого Button , привязанного к объекту DeleteCharCommand. Другие кнопки привязаны к AddCharCommand объекту, который совпадает с CommandParameter символом, отображаемым на Button:

Screenshot of a calculator using MVVM and commands.Снимок экрана: калькулятор с помощью MVVM и команд.