Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Просмотрите пример. Обзор примера
Шаблон 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
, и они могут содержать привязки к свойствам этого объекта:
Снимок экрана: страница с датой и временем.
Однако проблема заключается в том, что дата и время задаются один раз при создании и инициализации страницы и никогда не изменяются.
Предупреждение
В классе, наследуемом от 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
свойства. На следующем снимка экрана показан результат:
Снимок экрана: страница с датой и временем с помощью модели просмотра.
Кроме того, можно получить доступ к отдельным свойствам 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));
}
В этом примере изменения свойств Hue
Saturation
и Luminosity
свойств приводят Color
к изменению свойства, а изменение Color
свойства приводит к изменению других трех свойств. Это может показаться бесконечным циклом, за исключением того, что viewmodel не вызывает PropertyChanged
событие, если свойство не изменилось.
Следующий пример XAML содержит свойство, свойство которого привязано BoxView к Color
свойству viewmodel, а три и три SliderLabel представления привязаны к Hue
свойствам , Saturation
а Luminosity
Color
также свойствам:
<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 с двусторонними привязками данных.
Система команд
Иногда приложению требуется выйти за рамки привязок свойств, требуя от пользователя инициировать команды, влияющие на что-то в режиме просмотра. Эти команды обычно обозначаются путем нажатия кнопки или касания пальцем и традиционно обрабатываются в файле с выделенным кодом в обработчике для события Clicked
объекта Button или события Tapped
объекта TapGestureRecognizer.
Командный интерфейс предоставляет альтернативный подход к реализации команд, который гораздо лучше подходит для архитектуры MVVM. Модель представления может содержать команды, которые являются методами, выполняемыми в реакции на определенное действие в представлении, например щелчком Button мыши. Привязки данных определяются между этими командами и объектом Button.
Чтобы разрешить привязку данных между a Button и viewmodel, Button определяет два свойства:
Command
типаSystem.Windows.Input.ICommand
Command
типаSystem.Windows.Input.ICommand
Примечание.
Многие другие элементы управления также определяют 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, передав его методу Execute
CommandParameter
.Execute
Метод CanExecute
и CanExecuteChanged
событие используются для случаев, когда Button касание в настоящее время может быть недопустимым, в этом случае Button следует отключить сам. Вызовы CanExecute
Button при Command
первом наборе свойства и при CanExecuteChanged
возникновении события. Если CanExecute
возвращается false
, Button он отключает и не создает Execute
вызовы.
Для реализации интерфейса можно использовать класс или Command<T>
класс, включенный Command
ICommand в .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="⇦" 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:
Снимок экрана: калькулятор с помощью MVVM и команд.