Часть 5. От привязки данных до MVVM
Шаблон архитектуры Model-ViewModel (MVVM) был создан с учетом XAML. Шаблон обеспечивает разделение между тремя слоями программного обеспечения — пользовательский интерфейс XAML, называемый представлением; базовые данные, называемые моделью; и посредник между представлением и моделью, который называется ViewModel. Представление и ViewModel часто подключаются через привязки данных, определенные в XAML-файле. BindingContext для представления обычно является экземпляром ViewModel.
Простой видModel
Как введение в ViewModels, давайте сначала рассмотрим программу без одного.
Ранее вы узнали, как определить новое объявление пространства имен XML, чтобы разрешить XAML-файлу ссылаться на классы в других сборках. Ниже приведена программа, которая определяет объявление пространства имен XML для System
пространства имен:
xmlns:sys="clr-namespace:System;assembly=netstandard"
Программа может использовать x:Static
для получения текущей даты и времени из статического DateTime.Now
свойства и задать это DateTime
значение в BindingContext
StackLayout
:
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>
BindingContext
— специальное свойство: при установке BindingContext
элемента он наследуется всеми дочерними элементами этого элемента. Это означает, что все дочерние элементы StackLayout
имеют то же самое BindingContext
, и они могут содержать простые привязки к свойствам этого объекта.
В программе One-Shot DateTime два дочерних элемента содержат привязки к свойствам этого DateTime
значения, но два других дочерних элемента содержат привязки, которые, как представляется, отсутствуют пути привязки. Это означает, что DateTime
само значение используется для StringFormat
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
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">
<StackLayout BindingContext="{x:Static sys:DateTime.Now}"
HorizontalOptions="Center"
VerticalOptions="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}'}" />
</StackLayout>
</ContentPage>
Проблема заключается в том, что дата и время задаются один раз при первой сборке страницы и никогда не изменяются:
XAML-файл может отображать часы, которые всегда показывают текущее время, но требуется некоторый код, чтобы помочь. При думать с точки зрения MVVM модель и ViewModel являются классами, написанными полностью в коде. Представление часто представляет собой XAML-файл, который ссылается на свойства, определенные в ViewModel с помощью привязок данных.
Правильная модель не учитывается в представлении ViewModel, и соответствующий ViewModel не учитывает представление. Однако зачастую программист настраивает типы данных, предоставляемые ViewModel, с типами данных, связанными с определенными пользовательскими интерфейсами. Например, если модель обращается к базе данных, содержащей 8-разрядные строки ASCII, viewModel потребуется преобразовать между этими строками в строки Юникода, чтобы обеспечить монопольное использование Юникода в пользовательском интерфейсе.
В простых примерах MVVM (например, показанных здесь), часто нет модели вообще, и шаблон включает только представление и ViewModel, связанные с привязками данных.
Вот ViewModel для часов с одним именем свойства DateTime
, которое обновляет это DateTime
свойство каждые секунды:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
ViewModels обычно реализует INotifyPropertyChanged
интерфейс, что означает, что класс запускает PropertyChanged
событие всякий раз, когда одно из его свойств изменяется. Механизм привязки данных при Xamarin.Forms присоединении обработчика к этому PropertyChanged
событию позволяет получать уведомления при изменении свойства и обновлять целевой объект новым значением.
Часы, основанные на этом ViewModel, могут быть так же просты, как это:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label.BindingContext>
<local:ClockViewModel />
</Label.BindingContext>
</Label>
</ContentPage>
Обратите внимание, как ClockViewModel
задано значение BindingContext
тегов Label
элементов свойства. Кроме того, можно создать экземпляр ClockViewModel
коллекции Resources
и задать его с BindingContext
помощью StaticResource
расширения разметки. Или файл программной части может создать экземпляр ViewModel.
Binding
Расширение разметки Label
для Text
свойства форматирования DateTime
свойства. Ниже показан экран:
Кроме того, можно получить доступ к отдельным свойствам DateTime
свойства viewModel, разделив свойства с точками:
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
Интерактивный MVVM
MVVM часто используется с двусторонними привязками данных для интерактивного представления на основе базовой модели данных.
Ниже приведен класс с именем HslViewModel
, который преобразует Color
значение в Hue
, Saturation
а также Luminosity
значения и наоборот:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
public class HslViewModel : INotifyPropertyChanged
{
double hue, saturation, luminosity;
Color color;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (hue != value)
{
hue = value;
OnPropertyChanged("Hue");
SetNewColor();
}
}
get
{
return hue;
}
}
public double Saturation
{
set
{
if (saturation != value)
{
saturation = value;
OnPropertyChanged("Saturation");
SetNewColor();
}
}
get
{
return saturation;
}
}
public double Luminosity
{
set
{
if (luminosity != value)
{
luminosity = value;
OnPropertyChanged("Luminosity");
SetNewColor();
}
}
get
{
return luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
OnPropertyChanged("Color");
Hue = value.Hue;
Saturation = value.Saturation;
Luminosity = value.Luminosity;
}
}
get
{
return color;
}
}
void SetNewColor()
{
Color = Color.FromHsla(Hue, Saturation, Luminosity);
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Hue
Saturation
Изменения свойств и Luminosity
свойств приводят Color
к изменению свойства и изменениям, что приводит к Color
изменению других трех свойств. Это может показаться бесконечным циклом, за исключением того, что класс не вызывает PropertyChanged
событие, если свойство не изменилось. Это ставит конец неконтролируемому циклу обратной связи.
Следующий XAML-файл содержит свойство, свойство которого привязано BoxView
к Color
свойству ViewModel, а три и три Slider
Label
представления привязаны к свойствам и Saturation
Luminosity
свойствамHue
:Color
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<StackLayout Padding="10, 0">
<BoxView Color="{Binding Color}"
VerticalOptions="FillAndExpand" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue, Mode=TwoWay}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation, Mode=TwoWay}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity, Mode=TwoWay}" />
</StackLayout>
</ContentPage>
Привязка для каждой из них Label
— это значение по умолчанию OneWay
. Оно должно отображаться только для отображения значения. Но привязка для каждой Slider
из них TwoWay
. Это позволяет Slider
инициализироваться из ViewModel. Обратите внимание, что Color
свойство задано Aqua
при создании экземпляра ViewModel. Но изменение также Slider
должно задать новое значение для свойства в ViewModel, которое затем вычисляет новый цвет.
Командирование с помощью ViewModels
Во многих случаях шаблон MVVM ограничен манипуляцией с элементами данных: объекты пользовательского интерфейса в объектах параллельных данных View в ViewModel.
Однако иногда представление должно содержать кнопки, которые активируют различные действия в ViewModel. Но ViewModel не должен содержать Clicked
обработчики для кнопок, так как это привязывает ViewModel к определенной парадигме пользовательского интерфейса.
Чтобы разрешить ViewModels быть более независимыми от конкретных объектов пользовательского интерфейса, но по-прежнему разрешать вызов методов в ViewModel, существует командный интерфейс. Этот командный интерфейс поддерживается следующими элементами:Xamarin.Forms
Button
MenuItem
ToolbarItem
SearchBar
TextCell
(и, следовательно, такжеImageCell
)ListView
TapGestureRecognizer
За исключением SearchBar
элемента и ListView
элемента, эти элементы определяют два свойства:
Command
типаSystem.Windows.Input.ICommand
CommandParameter
типаObject
Определяет SearchBar
и SearchCommandParameter
свойства, а определяет ListView
RefreshCommand
свойство типаICommand
.SearchCommand
Интерфейс ICommand
определяет два метода и одно событие:
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
ViewModel может определять свойства типа ICommand
. Затем эти свойства можно привязать к Command
свойству каждого Button
или другого элемента или, возможно, пользовательское представление, реализующее этот интерфейс. При необходимости можно задать CommandParameter
свойство для идентификации отдельных Button
объектов (или других элементов), привязанных к этому свойству ViewModel. Внутри этого метода вызывается всякий раз, Button
когда пользователь нажимает методButton
, передав его методу Execute
CommandParameter
.Execute
Метод CanExecute
и CanExecuteChanged
событие используются для случаев, когда Button
касание в настоящее время может быть недопустимым, в этом случае Button
следует отключить сам. Вызовы CanExecute
Button
при Command
первом наборе свойства и при CanExecuteChanged
каждом запуске события. Если CanExecute
возвращается false
, Button
он отключает и не создает Execute
вызовы.
Для справки по добавлению команд в ViewModels определяет два класса, Xamarin.Forms реализующих ICommand
: Command
и Command<T>
где T
тип аргументов Execute
и CanExecute
. Эти два класса определяют несколько конструкторов, а также ChangeCanExecute
метод, который ViewModel может вызвать для принудительного Command
CanExecuteChanged
вызова объекта для запуска события.
Вот viewModel для простой клавиатуры, предназначенной для ввода номеров телефонов. Обратите внимание, что Execute
метод CanExecute
определяется как лямбда-функции прямо в конструкторе:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamlSamples
{
class KeypadViewModel : INotifyPropertyChanged
{
string inputString = "";
string displayText = "";
char[] specialChars = { '*', '#' };
public event PropertyChangedEventHandler PropertyChanged;
// Constructor
public KeypadViewModel()
{
AddCharCommand = new Command<string>((key) =>
{
// Add the key to the input string.
InputString += key;
});
DeleteCharCommand = new Command(() =>
{
// Strip a character from the input string.
InputString = InputString.Substring(0, InputString.Length - 1);
},
() =>
{
// Return true if there's something to delete.
return InputString.Length > 0;
});
}
// Public properties
public string InputString
{
protected set
{
if (inputString != value)
{
inputString = value;
OnPropertyChanged("InputString");
DisplayText = FormatText(inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
get { return inputString; }
}
public string DisplayText
{
protected set
{
if (displayText != value)
{
displayText = value;
OnPropertyChanged("DisplayText");
}
}
get { return displayText; }
}
// ICommand implementations
public ICommand AddCharCommand { protected set; get; }
public ICommand DeleteCharCommand { protected set; get; }
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
string formatted = str;
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
}
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;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
В этом представлении предполагается, что AddCharCommand
свойство привязано к Command
свойству нескольких кнопок (или что-либо другое с интерфейсом команды), каждое из которых определяется параметром CommandParameter
. Эти кнопки добавляют символы в InputString
свойство, которое затем форматируется как номер телефона для DisplayText
свойства.
Существует также второе свойство типа ICommand
с именем DeleteCharCommand
. Это привязано к кнопке с интервалом назад, но кнопка должна быть отключена, если нет символов для удаления.
Следующая клавиатура не так визуально сложна, как это может быть. Вместо этого разметка была сокращена до минимума, чтобы продемонстрировать более четкое использование интерфейса команды:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<Grid HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:KeypadViewModel />
</Grid.BindingContext>
<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>
<!-- Internal Grid for top row of items -->
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="0"
OutlineColor="Accent">
<Label Text="{Binding DisplayText}" />
</Frame>
<Button Text="⇦"
Command="{Binding DeleteCharCommand}"
Grid.Column="1"
BorderWidth="0" />
</Grid>
<Button Text="1"
Command="{Binding AddCharCommand}"
CommandParameter="1"
Grid.Row="1" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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" Grid.Column="0" />
<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
лице. Вот программа в действии:
Вызов асинхронных методов
Команды также могут вызывать асинхронные методы. Это достигается с помощью async
ключевое слово await
при указании Execute
метода:
DownloadCommand = new Command (async () => await DownloadAsync ());
Это означает, что DownloadAsync
метод является и Task
должен ожидаться:
async Task DownloadAsync ()
{
await Task.Run (() => Download ());
}
void Download ()
{
...
}
Реализация меню навигации
Пример программы, содержащей весь исходный код в этой серии статей, использует ViewModel для домашней страницы. Этот ViewModel — это определение короткого класса с тремя свойствами с именем Type
, Title
и Description
которые содержат тип каждой из примеров страниц, заголовка и краткого описания. Кроме того, ViewModel определяет статическое свойство, которое All
является коллекцией всех страниц в программе:
public class PageDataViewModel
{
public PageDataViewModel(Type type, string title, string description)
{
Type = type;
Title = title;
Description = description;
}
public Type Type { private set; get; }
public string Title { private set; get; }
public string Description { private set; get; }
static PageDataViewModel()
{
All = new List<PageDataViewModel>
{
// Part 1. Getting Started with XAML
new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
"Display a Label with many properties set"),
new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
"Interact with a Slider and Button"),
// Part 2. Essential XAML Syntax
new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
"Explore XAML syntax with the Grid"),
new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
"Explore XAML syntax with AbsoluteLayout"),
// Part 3. XAML Markup Extensions
new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
"Using resource dictionaries to share resources"),
new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
"Using the x:Static markup extensions"),
new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
"Explore XAML markup extensions"),
// Part 4. Data Binding Basics
new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
"Bind properties of two views on the page"),
new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
"Use Sliders with reverse bindings"),
new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
"Use a ListView with data bindings"),
// Part 5. From Data Bindings to MVVM
new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
"Obtain the current DateTime and display it"),
new PageDataViewModel(typeof(ClockPage), "Clock",
"Dynamically display the current time"),
new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
"Use a view model to select HSL colors"),
new PageDataViewModel(typeof(KeypadPage), "Keypad",
"Use a view model for numeric keypad logic")
};
}
public static IList<PageDataViewModel> All { private set; get; }
}
XAML-файл для MainPage
определения ListBox
свойства, для которого ItemsSource
задано это All
свойство, и который содержит объект TextCell
для отображения Title
и Description
свойств каждой страницы:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.MainPage"
Padding="5, 0"
Title="XAML Samples">
<ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
ItemSelected="OnListViewItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}"
Detail="{Binding Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Страницы отображаются в прокручиваемом списке:
Обработчик в файле программной части активируется, когда пользователь выбирает элемент. Обработчик задает SelectedItem
свойство задней null
части, а затем создает экземпляр выбранной ListBox
страницы и переходит к нему:
private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
(sender as ListView).SelectedItem = null;
if (args.SelectedItem != null)
{
PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
Page page = (Page)Activator.CreateInstance(pageData.Type);
await Navigation.PushAsync(page);
}
}
Видео
Xamarin Evolve 2016: MVVM Сделано простым с Xamarin.Forms и Prism
Итоги
XAML — это мощный инструмент для определения пользовательских интерфейсов в Xamarin.Forms приложениях, особенно при использовании привязки данных и MVVM. Результатом является чистое, элегантное и потенциально инструментируемое представление пользовательского интерфейса со всеми фоновыми поддержками в коде.
Связанные ссылки
- Часть 1. Начало работы с XAML
- Part 2 (Развертывание виртуальных машин в облаке, часть 2). Основной синтаксис XAML
- Часть 3. Расширения разметки XAML
- Часть 4. Основы привязки данных