Часть 4. Основы привязки данных
Привязки данных позволяют связать свойства двух объектов таким образом, чтобы изменение в одном из них приводило к изменению другого. Это очень ценное средство, и хотя привязки данных можно определить полностью в коде, XAML предоставляет сочетания клавиш и удобство. Следовательно, одним из наиболее важных расширений разметки является Xamarin.Forms Binding.
Привязки данных
Привязки данных соединяют свойства двух объектов, называемых источником и целевым объектом. В коде необходимо выполнить два шага: BindingContext
свойство целевого объекта должно быть задано исходному объекту, а SetBinding
метод (часто используемый в сочетании с Binding
классом) должен вызываться для целевого объекта, чтобы привязать свойство этого объекта к свойству исходного объекта.
Целевое свойство должно быть привязываемым свойством, что означает, что целевой объект должен быть производным от BindableObject
. В интерактивной Xamarin.Forms документации указывается, какие свойства являются привязываемыми свойствами. Свойство Label
, связанное Text
с привязываемым свойством TextProperty
.
В разметке необходимо также выполнить те же два шага, которые требуются в коде, за исключением того, что Binding
расширение разметки занимает место SetBinding
вызова и Binding
класса.
Однако при определении привязок данных в XAML существует несколько способов установки BindingContext
целевого объекта. Иногда он устанавливается из файла кода программной части, иногда с помощью StaticResource
x:Static
расширения разметки, а иногда и содержимого BindingContext
тегов элементов свойства.
Привязки чаще всего используются для подключения визуальных элементов программы к базовой модели данных, обычно в реализации архитектуры приложения MVVM (Model-View-ViewModel), как описано в части 5. Из привязок данных к MVVM, но возможны другие сценарии.
Привязки представления к представлению
Привязки данных можно определить для связывания свойств двух представлений на одной странице. В этом случае вы устанавливаете BindingContext
целевой объект с помощью x:Reference
расширения разметки.
Вот XAML-файл, содержащий Slider
и два Label
представления, одно из которых повернуто Slider
значением, а другой — значением Slider
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SliderBindingsPage"
Title="Slider Bindings Page">
<StackLayout>
<Label Text="ROTATION"
BindingContext="{x:Reference Name=slider}"
Rotation="{Binding Path=Value}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Slider x:Name="slider"
Maximum="360"
VerticalOptions="CenterAndExpand" />
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage>
x:Name
Содержит Slider
атрибут, на который ссылается два Label
представления с помощью x:Reference
расширения разметки.
Расширение x:Reference
привязки определяет свойство с именем Name
указанного элемента в данном случае slider
. Однако класс, определяющий x:Reference
расширение разметки, ReferenceExtension
также определяет ContentProperty
атрибут для Name
, что означает, что оно не является явным образом обязательным. Только для разнообразия, первый x:Reference
включает "Name=", но второй не:
BindingContext="{x:Reference Name=slider}"
…
BindingContext="{x:Reference slider}"
Расширение Binding
разметки может иметь несколько свойств, как и BindingBase
Binding
класс. Path
Binding
Значение ContentProperty
имеет значение, но часть "Path=" расширения разметки может быть опущена, если путь является первым элементом Binding
в расширении разметки. Первый пример имеет "Path=", но второй пример не указывает на это:
Rotation="{Binding Path=Value}"
…
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
Все свойства могут находиться в одной строке или разделены на несколько строк:
Text="{Binding Value,
StringFormat='The angle is {0:F0} degrees'}"
Сделайте все, что удобно.
Обратите внимание на StringFormat
свойство во втором Binding
расширении разметки. В Xamarin.Formsпривязках не выполняются неявные преобразования типов, и если необходимо отобразить нестроковый объект в виде строки, необходимо предоставить преобразователь типов или использовать StringFormat
. Вне сцены статический String.Format
метод используется для реализации StringFormat
. Это потенциально проблема, так как спецификации форматирования .NET включают фигурные скобки, которые также используются для разделителя расширений разметки. Это создает риск запутать средство синтаксического анализа XAML. Чтобы избежать этого, поместите всю строку форматирования в одинарные кавычки:
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
Вот запущенная программа:
Режим привязки
Одно представление может содержать привязки данных для нескольких его свойств. Однако каждое представление может иметь только одну BindingContext
, поэтому несколько привязок данных в этом представлении должны иметь все ссылочные свойства одного и того же объекта.
Решение этих и других проблем включает Mode
свойство, которое задается элементом BindingMode
перечисления:
Default
OneWay
— значения передаются из источника в целевой объектOneWayToSource
— значения передаются из целевого объекта в источникTwoWay
— значения передаются обоими способами между источником и целевым объектом.OneTime
— данные идут из источника в целевой объект, но только приBindingContext
изменении
В следующей программе демонстрируется одно общее использование режимов привязки OneWayToSource
и TwoWay
режимов привязки. Четыре Slider
представления предназначены для управления Scale
свойствами Rotate
RotateX
и RotateY
свойствами объекта Label
. Во-первых, кажется, что эти четыре свойства должны быть целевыми Slider
объектами привязки Label
данных, так как каждая из них задается. Тем не менее, может BindingContext
Label
быть только один объект, и существует четыре разных ползунка.
По этой причине все привязки задаются, казалось бы, обратно: BindingContext
для каждого из четырех ползунков задано значение , и привязки задаются Label
в Value
свойствах ползунка. С помощью OneWayToSource
и режимов эти Value
свойства могут задавать исходные свойства, которые являются свойствами Scale
, Rotate
RotateX
и RotateY
свойствами:Label
TwoWay
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SliderTransformsPage"
Padding="5"
Title="Slider Transforms Page">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Scaled and rotated Label -->
<Label x:Name="label"
Text="TEXT"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<!-- Slider and identifying Label for Scale -->
<Slider x:Name="scaleSlider"
BindingContext="{x:Reference label}"
Grid.Row="1" Grid.Column="0"
Maximum="10"
Value="{Binding Scale, Mode=TwoWay}" />
<Label BindingContext="{x:Reference scaleSlider}"
Text="{Binding Value, StringFormat='Scale = {0:F1}'}"
Grid.Row="1" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for Rotation -->
<Slider x:Name="rotationSlider"
BindingContext="{x:Reference label}"
Grid.Row="2" Grid.Column="0"
Maximum="360"
Value="{Binding Rotation, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationSlider}"
Text="{Binding Value, StringFormat='Rotation = {0:F0}'}"
Grid.Row="2" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for RotationX -->
<Slider x:Name="rotationXSlider"
BindingContext="{x:Reference label}"
Grid.Row="3" Grid.Column="0"
Maximum="360"
Value="{Binding RotationX, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationXSlider}"
Text="{Binding Value, StringFormat='RotationX = {0:F0}'}"
Grid.Row="3" Grid.Column="1"
VerticalTextAlignment="Center" />
<!-- Slider and identifying Label for RotationY -->
<Slider x:Name="rotationYSlider"
BindingContext="{x:Reference label}"
Grid.Row="4" Grid.Column="0"
Maximum="360"
Value="{Binding RotationY, Mode=OneWayToSource}" />
<Label BindingContext="{x:Reference rotationYSlider}"
Text="{Binding Value, StringFormat='RotationY = {0:F0}'}"
Grid.Row="4" Grid.Column="1"
VerticalTextAlignment="Center" />
</Grid>
</ContentPage>
Привязки для трех Slider
представлений — OneWayToSource
это означает, что Slider
значение вызывает изменение свойства его BindingContext
, которое является Label
именованным label
. Эти три Slider
представления вызывают изменения Rotate
в свойствах RotateX
и RotateY
свойствах Label
объекта .
Однако привязка Scale
свойства имеет значение TwoWay
. Это связано с тем, что Scale
свойство имеет значение по умолчанию 1, и использование TwoWay
привязки приводит Slider
к тому, что начальное значение должно быть задано в 1, а не 0. Если эта привязка была OneWayToSource
, Scale
свойство изначально будет иметь значение 0 из Slider
значения по умолчанию. Не Label
будет видимым, и это может вызвать некоторую путаницу для пользователя.
Примечание.
Класс VisualElement
также имеет ScaleX
и ScaleY
свойства, которые масштабируется VisualElement
на оси x и y соответственно.
Привязки и коллекции
Ничего не иллюстрирует силу привязок XAML и данных лучше, чем шаблон.ListView
ListView
ItemsSource
определяет свойство типа IEnumerable
и отображает элементы в этой коллекции. Эти элементы могут быть объектами любого типа. По умолчанию ListView
использует ToString
метод каждого элемента для отображения этого элемента. Иногда это просто то, что требуется, ToString
но во многих случаях возвращает только полное имя класса объекта.
Однако элементы в ListView
коллекции можно отображать любым способом, который требуется использовать с помощью шаблона, который включает класс, производный от Cell
. Шаблон клонируется для каждого элемента в ListView
привязках данных, установленных на шаблоне, передается отдельным клонам.
Очень часто необходимо создать пользовательскую ячейку для этих элементов с помощью ViewCell
класса. Этот процесс несколько беспорядок в коде, но в XAML он становится очень простым.
В проект XamlSamples входит класс NamedColor
. Каждый NamedColor
объект имеет Name
и FriendlyName
свойства типа string
, а Color
также свойство типа Color
. Кроме того, NamedColor
имеет 141 статических полей Color
типа, соответствующих цветам, определенным в Xamarin.FormsColor
классе. Статический IEnumerable<NamedColor>
конструктор создает коллекцию, содержащую NamedColor
объекты, соответствующие этим статическим полям, и назначает его общедоступному статическому All
свойству.
Настройка статического NamedColor.All
свойства ItemsSource
для объекта ListView
просто с помощью x:Static
расширения разметки:
<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.ListViewDemoPage"
Title="ListView Demo Page">
<ListView ItemsSource="{x:Static local:NamedColor.All}" />
</ContentPage>
Результатом отображения устанавливается, что элементы действительно относятся к типу XamlSamples.NamedColor
:
Это не так много информации, но ListView
прокручиваемая и выборная.
Чтобы определить шаблон для элементов, необходимо разбить ItemTemplate
свойство как элемент свойства и задать для него значение DataTemplate
, которое затем ссылается на ViewCell
объект. View
Для свойства ViewCell
можно определить макет одного или нескольких представлений для отображения каждого элемента. Ниже приведен простой пример:
<ListView ItemsSource="{x:Static local:NamedColor.All}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Label Text="{Binding FriendlyName}" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Примечание.
Источником привязки для ячеек и дочерних ячеек является ListView.ItemsSource
коллекция.
Элемент Label
присваивается View
свойству объекта ViewCell
. ViewCell.View
(Теги не нужны, так как View
свойство является свойством содержимого ViewCell
.) Эта разметка отображает FriendlyName
свойство каждого NamedColor
объекта:
Гораздо лучше. Теперь все, что необходимо, заключается в том, чтобы спрутить шаблон элемента с дополнительными сведениями и фактическим цветом. Для поддержки этого шаблона в словаре ресурсов страницы определены некоторые значения и объекты:
<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.ListViewDemoPage"
Title="ListView Demo Page">
<ContentPage.Resources>
<ResourceDictionary>
<OnPlatform x:Key="boxSize"
x:TypeArguments="x:Double">
<On Platform="iOS, Android, UWP" Value="50" />
</OnPlatform>
<OnPlatform x:Key="rowHeight"
x:TypeArguments="x:Int32">
<On Platform="iOS, Android, UWP" Value="60" />
</OnPlatform>
<local:DoubleToIntConverter x:Key="intConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ListView ItemsSource="{x:Static local:NamedColor.All}"
RowHeight="{StaticResource rowHeight}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="5, 5, 0, 5"
Orientation="Horizontal"
Spacing="15">
<BoxView WidthRequest="{StaticResource boxSize}"
HeightRequest="{StaticResource boxSize}"
Color="{Binding Color}" />
<StackLayout Padding="5, 0, 0, 0"
VerticalOptions="Center">
<Label Text="{Binding FriendlyName}"
FontAttributes="Bold"
FontSize="Medium" />
<StackLayout Orientation="Horizontal"
Spacing="0">
<Label Text="{Binding Color.R,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat='R={0:X2}'}" />
<Label Text="{Binding Color.G,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat=', G={0:X2}'}" />
<Label Text="{Binding Color.B,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat=', B={0:X2}'}" />
</StackLayout>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Обратите внимание, что используется для OnPlatform
определения размера BoxView
и высоты ListView
строк. Хотя значения для всех платформ одинаковы, разметка может быть легко адаптирована для других значений для точной настройки дисплея.
Преобразователи значений привязки
В предыдущем файле ListView Demo XAML отображаются отдельные R
G
и B
свойства Xamarin.FormsColor
структуры. Эти свойства имеют тип double
и диапазон от 0 до 1. Если вы хотите отобразить шестнадцатеричные значения, нельзя просто использовать StringFormat
с спецификацией форматирования X2. Это работает только для целых чисел и, кроме того, double
значения должны быть умножены на 255.
Эта небольшая проблема была решена с помощью преобразователя значений, также называемого преобразователем привязки. Это класс, реализующий IValueConverter
интерфейс, который означает, что он имеет два метода с именем Convert
и ConvertBack
. Метод Convert
вызывается при передаче значения из источника в целевой объект. ConvertBack
Метод вызывается для передачи из целевого объекта в OneWayToSource
источник или TwoWay
привязки:
using System;
using System.Globalization;
using Xamarin.Forms;
namespace XamlSamples
{
class DoubleToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
double multiplier;
if (!Double.TryParse(parameter as string, out multiplier))
multiplier = 1;
return (int)Math.Round(multiplier * (double)value);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
double divider;
if (!Double.TryParse(parameter as string, out divider))
divider = 1;
return ((double)(int)value) / divider;
}
}
}
Метод ConvertBack
не играет роль в этой программе, так как привязки являются единственным способом от источника к целевому объекту.
Привязка ссылается на преобразователь привязки со свойством Converter
. Преобразователь привязки также может принимать параметр, указанный свойством ConverterParameter
. Для некоторой универсальности это способ указания умножения. Преобразователь привязки проверяет параметр преобразователя для допустимого double
значения.
Преобразователь создается в словаре ресурсов, чтобы его можно было совместно использовать для нескольких привязок:
<local:DoubleToIntConverter x:Key="intConverter" />
Три привязки данных ссылались на один экземпляр. Обратите внимание, что Binding
расширение разметки содержит внедренное StaticResource
расширение разметки:
<Label Text="{Binding Color.R,
Converter={StaticResource intConverter},
ConverterParameter=255,
StringFormat='R={0:X2}'}" />
Вот результат:
Это ListView
довольно сложно в обработке изменений, которые могут динамически возникать в базовых данных, но только в том случае, если предпринять определенные действия. Если коллекция элементов, назначенных ItemsSource
свойству ListView
изменений во время выполнения, то есть, если элементы можно добавить в коллекцию или удалить их, используйте ObservableCollection
класс для этих элементов. ObservableCollection
реализует интерфейс и установит INotifyCollectionChanged
обработчик событияCollectionChanged
.ListView
Если свойства элементов сами изменяются во время выполнения, элементы в коллекции должны реализовывать INotifyPropertyChanged
интерфейс и сигнализировать об изменениях значений свойств с помощью PropertyChanged
события. Это демонстрируется в следующей части этой серии, часть 5. Из привязки данных к MVVM.
Итоги
Привязки данных обеспечивают мощный механизм связывания свойств между двумя объектами на странице или между визуальными объектами и базовыми данными. Но когда приложение начинает работать с источниками данных, популярный шаблон архитектуры приложений начинает появляться в качестве полезной парадигмы. Это рассматривается в части 5. Из привязок данных к MVVM.