第十八章:MVVM(四)
ViewModel中的交互式属性
ViewModel的第二个例子做了一些非常基本的事情,你永远不会为此目的编写ViewModel。 SimpleMultiplierViewModel类简单地将两个数字相乘。 但它是一个很好的例子,用于演示具有多个交互属性的ViewModel的开销和机制。 (虽然你永远不会编写一个ViewModel来将两个数字相乘,但你可以编写一个ViewModel来解决二次方程或更复杂的东西。)
SimpleMultiplierViewModel类是SimpleMultiplier项目的一部分:
using System;
using System.ComponentModel;
namespace SimpleMultiplier
{
class SimpleMultiplierViewModel : INotifyPropertyChanged
{
double multiplicand, multiplier, product;
public event PropertyChangedEventHandler PropertyChanged;
public double Multiplicand
{
set
{
if (multiplicand != value)
{
multiplicand = value;
OnPropertyChanged("Multiplicand");
UpdateProduct();
}
}
get
{
return multiplicand;
}
}
public double Multiplier
{
set
{
if (multiplier != value)
{
multiplier = value;
OnPropertyChanged("Multiplier");
UpdateProduct();
}
}
get
{
return multiplier;
}
}
public double Product
{
protected set
{
if (product != value)
{
product = value;
OnPropertyChanged("Product");
}
}
get
{
return product;
}
}
void UpdateProduct()
{
Product = Multiplicand * Multiplier;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
该类定义了double类型的三个公共属性,名为Multiplicand,Multiplier和Product。每个属性都由私有字段支持。前两个属性的set和get访问器是公共的,但Product属性的set访问器受到保护,以防止它在类外部设置,同时仍允许后代类更改它。
每个属性的set访问器首先检查属性值是否实际更改,如果是,则将支持字段设置为该值,并使用该属性名称调用名为OnPropertyChanged的方法。
INotifyPropertyChanged接口不需要OnPropertyChanged方法,但ViewModel类通常包含一个用于减少代码重复的方法。它通常被定义为受保护,以防您需要从另一个ViewModel派生一个ViewModel并在派生类中触发该事件。在本章的后面,您将看到在INotifyPropertyChanged类中减少代码重复的技术。
Multiplicand和Multiplier属性的set访问器通过调用UpdateProduct方法结束。这是执行将两个属性的值相乘并为Product属性设置新值的方法,然后触发其自己的PropertyChanged事件。
这是使用此ViewModel的XAML文件:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SimpleMultiplier"
x:Class="SimpleMultiplier.SimpleMultiplierPage"
Padding="10, 0">
<ContentPage.Resources>
<ResourceDictionary>
<local:SimpleMultiplierViewModel x:Key="viewModel" />
<Style TargetType="Label">
<Setter Property="FontSize" Value="Large" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout BindingContext="{StaticResource viewModel}">
<StackLayout VerticalOptions="CenterAndExpand">
<Slider Value="{Binding Multiplicand}" />
<Slider Value="{Binding Multiplier}" />
</StackLayout>
<StackLayout Orientation="Horizontal"
Spacing="0"
VerticalOptions="CenterAndExpand"
HorizontalOptions="Center">
<Label Text="{Binding Multiplicand, StringFormat='{0:F3}'}" />
<Label Text="{Binding Multiplier, StringFormat=' x {0:F3}'}" />
<Label Text="{Binding Product, StringFormat=' = {0:F3}'}" />
</StackLayout>
</StackLayout>
</ContentPage>
SimpleMultiplierViewModel在Resources字典中实例化,并使用StaticResource标记扩展设置为StackLayout的BindingContext属性。 BindingContext由StackLayout的所有子孙继承,其中包括两个Slider和三个Label元素。 BindingContext的使用允许这些绑定尽可能简单。
Slider的Value属性的默认绑定模式是TwoWay。 每个Slider的Value属性的更改会导致ViewModel的属性发生更改。
三个Label元素显示ViewModel的所有三个属性的值,其中一些格式插入时间,并且等号与数字相符:
<Label Text="{Binding Multiplicand, StringFormat='{0:F3}'}" />
<Label Text="{Binding Multiplier, StringFormat=' x {0:F3}'}" />
<Label Text="{Binding Product, StringFormat=' = {0:F3}'}" />
对于前两个,您可以将Label元素的Text属性直接绑定到相应Slider的Value属性,但这需要为每个Slider指定一个带有x的名称:Name并在Source参数中引用该名称 使用x:Reference标记扩展。 此程序中使用的方法更加清晰,并验证数据是否正在通过ViewModel从每个Slider到每个Label进行完整的旅行。
除了在构造函数中调用InitializeComponent之外,代码隐藏文件中没有任何内容。 所有业务逻辑都在ViewModel中,整个用户界面在XAML中定义:
如果您愿意,可以在资源字典中实例化时初始化ViewModel:
<local:SimpleMultiplierViewModel x:Key="viewModel"
Multiplicand="0.5"
Multiplier="0.5" />
作为双向绑定的结果,Slider元素将获得这些初始值。
当您想要稍微改变用户界面时,将用户界面与底层业务逻辑分离的好处变得很明显,可能通过将Stepper替换为Slider中的一个或两个数字:
<StackLayout VerticalOptions="CenterAndExpand">
<Slider Value="{Binding Multiplicand}" />
<Stepper Value="{Binding Multiplier}" />
</StackLayout>
除了两个元素的不同范围外,功能相同:
您也可以替换条目:
<StackLayout VerticalOptions="CenterAndExpand">
<Slider Value="{Binding Multiplicand}" />
<Entry Text="{Binding Multiplier}" />
</StackLayout>
Entry的Text属性的默认绑定模式也是TwoWay,因此您需要担心的是source属性double和target属性字符串之间的转换。 幸运的是,此转换由绑定基础结构自动处理:
如果键入一系列无法转换为double的字符,则绑定将保留最后一个有效值。如果您需要更复杂的验证,则必须实现自己的验证(例如使用触发器,将在第23章中讨论)。
一个有趣的实验是键入1E-1,这是可转换为双精度的科学记数法。你会看到它在条目中立即变为“0.1”。这是TwoWay绑定的效果:Multiplier属性从Entry设置为1E-1,但绑定基础结构在值返回Entry时调用的ToString方法返回文本“0.1”。因为这与现有的Entry文本,新文本已设置。为防止这种情况发生,您可以将绑定模式设置为OneWayToSource:
<StackLayout VerticalOptions="CenterAndExpand">
<Slider Value="{Binding Multiplicand}" />
<Entry Text="{Binding Multiplier, Mode=OneWayToSource}" />
</StackLayout>
现在,ViewModel的Multiplier属性是从Entry的Text属性设置的,而不是相反。如果您不需要从ViewModel更新这两个视图,则可以将它们都设置为OneWayToSource。但通常你会希望MVVM绑定是TwoWay。
您是否应该担心双向绑定中的无限循环?通常不会,因为PropertyChanged事件仅在属性实际更改时触发,而不是仅在它设置为相同值时触发。通常,源和目标将在反弹或反弹后停止相互更新。但是,可以编写一个“病态”值转换器,它不提供往返转换,并且确实可以在双向绑定中导致无限更新周期。