Xamarin.Forms 绑定模式
在上一篇文章中,“替代代码绑定”和“替代 XAML 绑定”页面都有一个 Label
,其 Scale
属性绑定到 Slider
的 Value
属性。 由于 Slider
初始值为 0,所以这导致 Label
的 Scale
属性被设置为 0 而不是 1,并且 Label
消失。
“反向绑定Label
”页面类似于上一篇文章中的程序,只是数据绑定是针对 而非 Slider
定义的:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DataBindingDemos.ReverseBindingPage"
Title="Reverse Binding">
<StackLayout Padding="10, 0">
<Label x:Name="label"
Text="TEXT"
FontSize="80"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Slider x:Name="slider"
VerticalOptions="CenterAndExpand"
Value="{Binding Source={x:Reference label},
Path=Opacity}" />
</StackLayout>
</ContentPage>
首先,这似乎是反向的:现在,Label
是数据绑定源,而 Slider
是目标。 绑定引用 Label
的 Opacity
属性,其默认值为 1。
正如你所料,Slider
从 Label
的初始 Opacity
值初始化为值 1。 如左侧的 iOS 屏幕截图中所示:
但是,正如 Android 屏幕截图所示,Slider
会继续工作,你可能会对此感到惊讶。 这似乎表明,当 Slider
(而非 Label
)是绑定目标时,数据绑定效果更好,因为初始化的工作方式与我们预期的一样。
反向绑定示例与早期示例之间的差异涉及绑定模式。
默认绑定模式
使用 BindingMode
枚举的成员指定绑定模式:
Default
TwoWay
- 数据在源和目标之间双向传输OneWay
- 数据从源到目标单向传输OneWayToSource
- 数据从目标到源单向传输OneTime
- 只有在BindingContext
更改时,数据才从源到目标单向传输(Xamarin.Forms 3.0 新增功能)
每个可绑定属性都有一个默认绑定模式,该模式在创建可绑定属性时进行设置,并且可从 BindableProperty
对象的 DefaultBindingMode
属性中获得。 此默认绑定模式指示该属性是数据绑定目标时有效的模式。
大多数属性(如 Rotation
、Scale
和 Opacity
)的默认绑定模式都是 OneWay
。 如果这些属性是数据绑定目标,则从源设置目标属性。
但是,Slider
的 Value
属性的默认绑定模式为 TwoWay
。 这意味着,如果 Value
属性是数据绑定目标时,则通常从源设置目标,但也可从目标设置源。 这就是允许从初始 Opacity
值设置 Slider
的原因。
这种双向绑定似乎会创建一个无限循环,但这种情况不会发生。 除非属性实际发生变化,否则可绑定属性不会发出属性更改的信号。 这样可以避免无限循环。
双向绑定
大多数可绑定属性的默认绑定模式都是 OneWay
,但以下属性的默认绑定模式为 TwoWay
:
DatePicker
的Date
属性Editor
、Entry
、SearchBar
和EntryCell
的Text
属性ListView
的IsRefreshing
属性MultiPage
的SelectedItem
属性Picker
的SelectedIndex
和SelectedItem
属性Slider
和Stepper
的Value
属性Switch
的IsToggled
属性SwitchCell
的On
属性TimePicker
的Time
属性
这些特定属性被定义为 TwoWay
,理由非常充分:
当数据绑定与模型-视图-视图模型 (MVVM) 应用程序体系结构一起使用时,ViewModel 类是数据绑定源,而由 Slider
等视图组成的 View 则是数据绑定目标。 MVVM 绑定更类似于反向绑定示例,而不是之前示例中的绑定。 你很可能会想要使用 ViewModel 中相应属性的值来初始化页面上的每个视图,但视图中的更改应该也会影响 ViewModel 属性。
默认绑定模式为 TwoWay
的属性是最有可能在 MVVM 方案中使用的属性。
单向数据源绑定
只读可绑定属性的默认绑定模式为 OneWayToSource
。 只有一个读/写可绑定属性的默认绑定模式为 OneWayToSource
:
ListView
的SelectedItem
属性
其基本原理是,对 SelectedItem
属性的绑定应该导致设置绑定源。 本文后面的示例将替代该行为。
一次性绑定
许多属性(包括 Entry
的 IsTextPredictionEnabled
属性)都具有 OneTime
的默认绑定模式。
只有在绑定上下文更改时,才会更新绑定模式为 OneTime
的目标属性。 对于这些目标属性的绑定,该模式简化了绑定基础结构,因为不必监视源属性中的更改。
ViewModel 和属性更改通知
“简易颜色选择器”页面演示了如何使用简单的 ViewModel。 数据绑定允许用户使用三个 Slider
元素为 Hue、Saturation 和 Luminosity 选择颜色。
ViewModel 是数据绑定源。 ViewModel 没有定义可绑定属性,但它实现了一种通知机制,允许在属性值更改时通知绑定基础结构。 此通知机制是 INotifyPropertyChanged
接口,该接口定义名为 PropertyChanged
的单个事件。 实现此接口的类通常在其某个公共属性更改值时触发该事件。 如果属性永远不会更改,则不需要触发该事件。 (INotifyPropertyChanged
接口也由 BindableObject
实现,并且只要可绑定属性更改值,就会触发 PropertyChanged
事件。)
HslColorViewModel
类定义四个属性:Hue
、Saturation
、Luminosity
和 Color
属性彼此相互关联。 当这三个颜色组件中的任何一个更改值时,都将重新计算 Color
属性,并为所有四个属性触发 PropertyChanged
事件:
public class HslColorViewModel : INotifyPropertyChanged
{
Color color;
string name;
public event PropertyChangedEventHandler PropertyChanged;
public double Hue
{
set
{
if (color.Hue != value)
{
Color = Color.FromHsla(value, color.Saturation, color.Luminosity);
}
}
get
{
return color.Hue;
}
}
public double Saturation
{
set
{
if (color.Saturation != value)
{
Color = Color.FromHsla(color.Hue, value, color.Luminosity);
}
}
get
{
return color.Saturation;
}
}
public double Luminosity
{
set
{
if (color.Luminosity != value)
{
Color = Color.FromHsla(color.Hue, color.Saturation, value);
}
}
get
{
return color.Luminosity;
}
}
public Color Color
{
set
{
if (color != value)
{
color = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Hue"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Saturation"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Luminosity"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Color"));
Name = NamedColor.GetNearestColorName(color);
}
}
get
{
return color;
}
}
public string Name
{
private set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
get
{
return name;
}
}
}
当 Color
属性更改时,NamedColor
类中的静态 GetNearestColorName
方法(也包含在 DataBindingDemos 解决方案中)获取命名最类似的颜色并设置 Name
属性。 此 Name
属性具有专用 set
访问器,因此无法从类之外进行设置。
将 ViewModel 设置为绑定源时,绑定基础结构会将处理程序附加到 PropertyChanged
事件。 通过这种方式,绑定可收到属性的更改通知,然后可以根据更改的值设置目标属性。
但是,当目标属性(或目标属性的 Binding
定义)的 BindingMode
为 OneTime
时,绑定基础结构不必在 PropertyChanged
事件上附加处理程序。 目标属性仅在 BindingContext
更改时更新,而不是在源属性本身更改时更新。
“简易颜色选择器”XAML 文件实例化页面资源字典中的 HslColorViewModel
并初始化 Color
属性。 Grid
的 BindingContext
属性被设置为 StaticResource
绑定扩展以引用该资源:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.SimpleColorSelectorPage">
<ContentPage.Resources>
<ResourceDictionary>
<local:HslColorViewModel x:Key="viewModel"
Color="MediumTurquoise" />
<Style TargetType="Slider">
<Setter Property="VerticalOptions" Value="CenterAndExpand" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<Grid BindingContext="{StaticResource viewModel}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<BoxView Color="{Binding Color}"
Grid.Row="0" />
<StackLayout Grid.Row="1"
Margin="10, 0">
<Label Text="{Binding Name}"
HorizontalTextAlignment="Center" />
<Slider Value="{Binding Hue}" />
<Slider Value="{Binding Saturation}" />
<Slider Value="{Binding Luminosity}" />
</StackLayout>
</Grid>
</ContentPage>
BoxView
、Label
和三个 Slider
视图从 Grid
继承绑定上下文。 这些视图都是引用 ViewModel 中的源属性的绑定目标。 对于 BoxView
的 Color
属性和 Label
的 Text
属性,数据绑定为 OneWay
:视图中的属性根据 ViewModel 中的属性进行设置。
但是,Slider
的 Value
属性为 TwoWay
。 此模式允许从 ViewModel 设置每个 Slider
,也允许从每个 Slider
设置 ViewModel。
首次运行程序时,BoxView
、Label
和三个 Slider
元素均根据实例化 ViewModel 时设置的初始 Color
属性集从 ViewModel 中进行设置。 如左侧 iOS 屏幕截图中所示:
操纵滑块时,BoxView
和 Label
会相应地更新,如 Android 屏幕截图所示。
在资源字典中实例化 ViewModel 是一种常见的方法。 也可以在 BindingContext
属性的属性元素标记内实例化 ViewModel。 在“简易颜色选择器”XAML 文件中,尝试从资源字典中删除 HslColorViewModel
并将其设置为 Grid
的 BindingContext
属性,如下所示:
<Grid>
<Grid.BindingContext>
<local:HslColorViewModel Color="MediumTurquoise" />
</Grid.BindingContext>
···
</Grid>
可以采用多种方式设置绑定上下文。 有时,代码隐藏文件会实例化 ViewModel 并将其设置为页面的 BindingContext
属性。 这些都是有效的方法。
替代绑定模式
如果目标属性的默认绑定模式不适合特定的数据绑定,则可以通过将 Binding
的 Mode
属性(或 Binding
标记扩展的 Mode
属性)设置为 BindingMode
枚举的其中一个成员来替代它。
但是,将 Mode
属性设置为 TwoWay
并不总是像你预期的那样有效。 例如,尝试修改“替代 XAML 绑定”XAML 文件,以在绑定定义中包含 TwoWay
:
<Label Text="TEXT"
FontSize="40"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
Scale="{Binding Source={x:Reference slider},
Path=Value,
Mode=TwoWay}" />
这预计会将 Slider
初始化为 Scale
属性的初始值(即 1),但情况并不是这样。 初始化 TwoWay
绑定时,首先从源设置目标,这意味着会将 Scale
属性设置为 Slider
默认值 0。 对 Slider
设置 TwoWay
绑定时,最初会从源设置 Slider
。
可以在“替代 XAML 绑定”示例中将绑定模式设置为 OneWayToSource
:
<Label Text="TEXT"
FontSize="40"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
Scale="{Binding Source={x:Reference slider},
Path=Value,
Mode=OneWayToSource}" />
现在,Slider
被初始化为 1(Scale
的默认值),但操作 Slider
并不会影响 Scale
属性,因此这不是很有用。
注意
VisualElement
类还定义了 ScaleX
和 ScaleY
属性,这些属性可以在水平和垂直方向上以不同方式缩放 VisualElement
。
使用 TwoWay
替代默认绑定模式的一种非常有用的应用程序涉及 ListView
的 SelectedItem
属性。 默认绑定模式为 OneWayToSource
。 如果对 SelectedItem
属性设置数据绑定以引用 ViewModel 中的源属性,则从 ListView
选择设置该源属性。 但是,在某些情况下,可能还需要从 ViewModel 初始化 ListView
。
“示例设置”页面演示了此方法。 此页面表示应用程序设置的简单实现,这些设置通常在 ViewModel 中定义,例如此 SampleSettingsViewModel
文件:
public class SampleSettingsViewModel : INotifyPropertyChanged
{
string name;
DateTime birthDate;
bool codesInCSharp;
double numberOfCopies;
NamedColor backgroundNamedColor;
public event PropertyChangedEventHandler PropertyChanged;
public SampleSettingsViewModel(IDictionary<string, object> dictionary)
{
Name = GetDictionaryEntry<string>(dictionary, "Name");
BirthDate = GetDictionaryEntry(dictionary, "BirthDate", new DateTime(1980, 1, 1));
CodesInCSharp = GetDictionaryEntry<bool>(dictionary, "CodesInCSharp");
NumberOfCopies = GetDictionaryEntry(dictionary, "NumberOfCopies", 1.0);
BackgroundNamedColor = NamedColor.Find(GetDictionaryEntry(dictionary, "BackgroundNamedColor", "White"));
}
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public DateTime BirthDate
{
set { SetProperty(ref birthDate, value); }
get { return birthDate; }
}
public bool CodesInCSharp
{
set { SetProperty(ref codesInCSharp, value); }
get { return codesInCSharp; }
}
public double NumberOfCopies
{
set { SetProperty(ref numberOfCopies, value); }
get { return numberOfCopies; }
}
public NamedColor BackgroundNamedColor
{
set
{
if (SetProperty(ref backgroundNamedColor, value))
{
OnPropertyChanged("BackgroundColor");
}
}
get { return backgroundNamedColor; }
}
public Color BackgroundColor
{
get { return BackgroundNamedColor?.Color ?? Color.White; }
}
public void SaveState(IDictionary<string, object> dictionary)
{
dictionary["Name"] = Name;
dictionary["BirthDate"] = BirthDate;
dictionary["CodesInCSharp"] = CodesInCSharp;
dictionary["NumberOfCopies"] = NumberOfCopies;
dictionary["BackgroundNamedColor"] = BackgroundNamedColor.Name;
}
T GetDictionaryEntry<T>(IDictionary<string, object> dictionary, string key, T defaultValue = default(T))
{
return dictionary.ContainsKey(key) ? (T)dictionary[key] : defaultValue;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
每个应用程序设置都是一个属性,保存在名为 SaveState
的方法中的 Xamarin.Forms 属性字典中,并从该字典加载到构造函数中。 类的底部有两种方法可以帮助简化 ViewModel,并使它们不易出错。 底部的 OnPropertyChanged
方法有一个设置为调用属性的可选参数。 这可以避免在将属性的名称指定为字符串时出现拼写错误。
类中的 SetProperty
方法要更胜一筹:它将设置的属性值与存储为字段的值进行比较,并且仅当两个值不相等时才调用 OnPropertyChanged
。
SampleSettingsViewModel
类为背景色定义了两个属性:BackgroundNamedColor
属性属于类型 NamedColor
,这也是 DataBindingDemos 解决方案中包含的一个类。 BackgroundColor
属性属于类型 Color
,并且该属性获取自 NamedColor
对象的 Color
属性。
NamedColor
类使用 .NET 反射来枚举 Xamarin.FormsColor
结构中的所有静态公共字段,并将它们以及它们的名称存储在可从静态 All
属性访问的集合中:
public class NamedColor : IEquatable<NamedColor>, IComparable<NamedColor>
{
// Instance members
private NamedColor()
{
}
public string Name { private set; get; }
public string FriendlyName { private set; get; }
public Color Color { private set; get; }
public string RgbDisplay { private set; get; }
public bool Equals(NamedColor other)
{
return Name.Equals(other.Name);
}
public int CompareTo(NamedColor other)
{
return Name.CompareTo(other.Name);
}
// Static members
static NamedColor()
{
List<NamedColor> all = new List<NamedColor>();
StringBuilder stringBuilder = new StringBuilder();
// Loop through the public static fields of the Color structure.
foreach (FieldInfo fieldInfo in typeof(Color).GetRuntimeFields())
{
if (fieldInfo.IsPublic &&
fieldInfo.IsStatic &&
fieldInfo.FieldType == typeof(Color))
{
// Convert the name to a friendly name.
string name = fieldInfo.Name;
stringBuilder.Clear();
int index = 0;
foreach (char ch in name)
{
if (index != 0 && Char.IsUpper(ch))
{
stringBuilder.Append(' ');
}
stringBuilder.Append(ch);
index++;
}
// Instantiate a NamedColor object.
Color color = (Color)fieldInfo.GetValue(null);
NamedColor namedColor = new NamedColor
{
Name = name,
FriendlyName = stringBuilder.ToString(),
Color = color,
RgbDisplay = String.Format("{0:X2}-{1:X2}-{2:X2}",
(int)(255 * color.R),
(int)(255 * color.G),
(int)(255 * color.B))
};
// Add it to the collection.
all.Add(namedColor);
}
}
all.TrimExcess();
all.Sort();
All = all;
}
public static IList<NamedColor> All { private set; get; }
public static NamedColor Find(string name)
{
return ((List<NamedColor>)All).Find(nc => nc.Name == name);
}
public static string GetNearestColorName(Color color)
{
double shortestDistance = 1000;
NamedColor closestColor = null;
foreach (NamedColor namedColor in NamedColor.All)
{
double distance = Math.Sqrt(Math.Pow(color.R - namedColor.Color.R, 2) +
Math.Pow(color.G - namedColor.Color.G, 2) +
Math.Pow(color.B - namedColor.Color.B, 2));
if (distance < shortestDistance)
{
shortestDistance = distance;
closestColor = namedColor;
}
}
return closestColor.Name;
}
}
DataBindingDemos 项目中的 App
类定义类型为 SampleSettingsViewModel
且名为 Settings
的属性。 在实例化 App
类时初始化此属性,并在调用 OnSleep
方法时调用 SaveState
方法:
public partial class App : Application
{
public App()
{
InitializeComponent();
Settings = new SampleSettingsViewModel(Current.Properties);
MainPage = new NavigationPage(new MainPage());
}
public SampleSettingsViewModel Settings { private set; get; }
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Handle when your app sleeps
Settings.SaveState(Current.Properties);
}
protected override void OnResume()
{
// Handle when your app resumes
}
}
有关应用程序生命周期方法的详细信息,请参阅应用生命周期一文。
几乎所有其他内容都在 SampleSettingsPage.xaml 文件中进行处理。 使用 Binding
标记扩展来设置页面的 BindingContext
:绑定源是静态 Application.Current
属性,它是项目中 App
类的实例,而 Path
设置为 Settings
属性,它是 SampleSettingsViewModel
对象:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.SampleSettingsPage"
Title="Sample Settings"
BindingContext="{Binding Source={x:Static Application.Current},
Path=Settings}">
<StackLayout BackgroundColor="{Binding BackgroundColor}"
Padding="10"
Spacing="10">
<StackLayout Orientation="Horizontal">
<Label Text="Name: "
VerticalOptions="Center" />
<Entry Text="{Binding Name}"
Placeholder="your name"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="Birth Date: "
VerticalOptions="Center" />
<DatePicker Date="{Binding BirthDate}"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="Do you code in C#? "
VerticalOptions="Center" />
<Switch IsToggled="{Binding CodesInCSharp}"
VerticalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="Number of Copies: "
VerticalOptions="Center" />
<Stepper Value="{Binding NumberOfCopies}"
VerticalOptions="Center" />
<Label Text="{Binding NumberOfCopies}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Background Color:" />
<ListView x:Name="colorListView"
ItemsSource="{x:Static local:NamedColor.All}"
SelectedItem="{Binding BackgroundNamedColor, Mode=TwoWay}"
VerticalOptions="FillAndExpand"
RowHeight="40">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<BoxView Color="{Binding Color}"
HeightRequest="32"
WidthRequest="32"
VerticalOptions="Center" />
<Label Text="{Binding FriendlyName}"
FontSize="24"
VerticalOptions="Center" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
页面的所有子项均继承绑定上下文。 此页面上的大多数其他绑定都是 SampleSettingsViewModel
中的属性。 BackgroundColor
属性用于设置 StackLayout
的 BackgroundColor
属性,而 Entry
、DatePicker
、Switch
和 Stepper
属性均被绑定到 ViewModel 中的其他属性。
ListView
的 ItemsSource
属性被设置为静态 NamedColor.All
属性。 这会使用所有 NamedColor
实例来填充 ListView
。 对于 ListView
中的每个项,项的绑定上下文都被设置为 NamedColor
对象。 ViewCell
中的 BoxView
和 Label
被绑定到 NamedColor
中的属性。
ListView
的 SelectedItem
属性属于类型 NamedColor
,并被绑定到 SampleSettingsViewModel
的 BackgroundNamedColor
属性:
SelectedItem="{Binding BackgroundNamedColor, Mode=TwoWay}"
SelectedItem
的默认绑定模式为 OneWayToSource
,它从所选项目中设置 ViewModel 属性。 TwoWay
模式允许从 ViewModel 中初始化 SelectedItem
。
但是,以这种方式设置 SelectedItem
时,ListView
不会自动滚动,因此无法显示所选项目。 代码隐藏文件中有必要存在一些代码:
public partial class SampleSettingsPage : ContentPage
{
public SampleSettingsPage()
{
InitializeComponent();
if (colorListView.SelectedItem != null)
{
colorListView.ScrollTo(colorListView.SelectedItem,
ScrollToPosition.MakeVisible,
false);
}
}
}
左侧的 iOS 屏幕截图显示了程序首次运行时的情况。 SampleSettingsViewModel
中的构造函数将背景色初始化为白色,即 ListView
中选择的颜色:
其他屏幕截图显示已更改的设置。 尝试使用此页面时,请记住将程序置于休眠状态,或在程序运行的设备或模拟器上终止程序。 从 Visual Studio 调试器终止程序不会导致调用 App
类中的 OnSleep
替代。
在下一篇文章中,可了解如何指定对 Label
的 Text
属性设置的数据绑定的字符串格式。