ViewModel时钟
假设您正在编写需要访问当前日期和时间的程序,并且您希望通过数据绑定来使用该信息。 .NET基类库通过DateTime结构提供日期和时间信息。要获取当前日期和时间,只需访问DateTime.Now属性。这是编写时钟应用程序的习惯方式。
但是出于数据绑定的目的,DateTime有一个严重的缺陷:它只提供静态信息,在日期或时间发生变化时没有通知。
在MVVM的上下文中,DateTime结构可能有资格作为模型,因为DateTime提供了我们需要的所有数据,但没有提供有利于数据绑定的形式。编写一个使用DateTime但在日期或时间发生变化时提供通知的ViewModel是必要的。
Xamarin.FormsBook.Toolkit库包含如下所示的DateTimeViewModel类。该类只有一个属性,名为DateTime,类型为DateTime,但由于在Device.StartTimer回调中频繁调用DateTime.Now,此属性会动态更改。
请注意,DateTimeViewModel类基于INotifyPropertyChanged接口,并包含用于定义此接口的System.ComponentModel命名空间的using指令。要实现此接口,该类定义名为PropertyChanged的公共事件。
注意:在类中定义PropertyChanged事件非常容易,而没有明确指定该类实现INotifyPropertyChanged!如果您没有明确指定该类基于INotifyPropertyChanged接口,则将忽略通知:
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace Xamarin.FormsBook.Toolkit
{
public class DateTimeViewModel : INotifyPropertyChanged
{
DateTime dateTime = DateTime.Now;
public event PropertyChangedEventHandler PropertyChanged;
public DateTimeViewModel()
{
Device.StartTimer(TimeSpan.FromMilliseconds(15), OnTimerTick);
}
bool OnTimerTick()
{
DateTime = DateTime.Now;
return true;
}
public DateTime DateTime
{
private set
{
if (dateTime != value)
{
dateTime = value;
// Fire the event.
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
此类中唯一的公共属性称为DateTime类型的DateTime,它与名为dateTime的专用支持字段相关联。 ViewModels中的公共属性通常具有私有支持字段。 DateTime属性的set访问器对于类是私有的,并且它从计时器回调每15毫秒更新一次。
除此之外,set访问器以非常标准的方式构造用于ViewModel:它首先检查设置为属性的值是否与dateTime支持字段不同。 如果没有,它将从传入值设置该支持字段,并使用属性的名称触发PropertyChanged处理程序。 如果仅将属性设置为其现有值,则触发PropertyChanged处理程序被认为是非常糟糕的做法,甚至可能导致在双向绑定中涉及无限循环的递归属性设置的问题。
这是触发事件的set访问器中的代码:
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs("DateTime"));
}
这种形式比这样的代码更好,它不会将处理程序保存在单独的变量中:
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
在多线程环境中,PropertyChanged处理程序可能在检查空值的if语句和事件的实际触发之间分离。 将处理程序保存在单独的变量中可防止导致问题,因此即使您尚未在多线程环境中工作,也应采用这种习惯。
get访问器只返回dateTime支持字段。
MvvmClock程序演示了DateTimeViewModel类如何通过数据绑定向用户界面提供更新的日期和时间信息:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="MvvmClock.MvvmClockPage">
<ContentPage.Resources>
<ResourceDictionary>
<toolkit:DateTimeViewModel x:Key="dateTimeViewModel" />
<Style TargetType="Label">
<Setter Property="FontSize" Value="Large" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout VerticalOptions="Center">
<Label Text="{Binding Source={x:Static sys:DateTime.Now},
StringFormat='This program started at {0:F}'}" />
<Label Text="But now..." />
<Label Text="{Binding Source={StaticResource dateTimeViewModel},
Path=DateTime.Hour,
StringFormat='The hour is {0}'}" />
<Label Text="{Binding Source={StaticResource dateTimeViewModel},
Path=DateTime.Minute,
StringFormat='The minute is {0}'}" />
<Label Text="{Binding Source={StaticResource dateTimeViewModel},
Path=DateTime.Second,
StringFormat='The seconds are {0}'}" />
<Label Text="{Binding Source={StaticResource dateTimeViewModel},
Path=DateTime.Millisecond,
StringFormat='The milliseconds are {0}'}" />
</StackLayout>
</ContentPage>
页面的Resources部分实例化DateTimeViewModel,还为Label定义了一个隐式Style。
六个Label元素中的第一个将其Text属性设置为涉及实际.NET DateTime结构的Binding对象。该绑定的Source属性是一个x:Static标记扩展,它引用静态DateTime.Now属性以获取程序首次开始运行时的日期和时间。此绑定不需要路径。 “F”格式规范适用于完整日期/时间模式,具有日期和时间字符串的长版本。虽然此Label显示程序启动的日期和时间,但它永远不会更新。
最后的四个数据绑定将被更新。在这些数据绑定中,Source属性设置为引用DateTimeViewModel对象的StaticResource标记扩展。 Path设置为该ViewModel的DateTime属性的各种子属性。在幕后,绑定基础结构在DateTimeViewModel中的PropertyChanged事件上附加处理程序。此处理程序检查DateTime属性中的更改,并在该属性更改时更新Label的Text属性。
除了InitializeComponent调用之外,代码隐藏文件是空的。最后四个标签的数据绑定显示更新时间,其更新速度与视频刷新率一样快:
通过将StackLayout的BindingContext属性设置为引用ViewModel的StaticResource标记扩展,可以简化此XAML文件中的标记。 BindingContext通过可视树传播,以便您可以删除最后四个Label元素上的Source设置:
<StackLayout VerticalOptions="Center"
BindingContext="{StaticResource dateTimeViewModel}">
<Label Text="{Binding Source={x:Static sys:DateTime.Now},
StringFormat='This program started at {0:F}'}" />
<Label Text="But now..." />
<Label Text="{Binding Path=DateTime.Hour,
StringFormat='The hour is {0}'}" />
<Label Text="{Binding Path=DateTime.Minute,
StringFormat='The minute is {0}'}" />
<Label Text="{Binding Path=DateTime.Second,
StringFormat='The seconds are {0}'}" />
<Label Text="{Binding Path=DateTime.Millisecond,
StringFormat='The milliseconds are {0}'}" />
</StackLayout>
第一个Label上的Binding覆盖了BindingContext及其自己的Source设置。
您甚至可以从ResourceDictionary中删除DateTimeViewModel项,并在BindingContext属性元素标记之间的StackLayout中实例化它:
<StackLayout VerticalOptions="Center">
<StackLayout.BindingContext>
<toolkit:DateTimeViewModel />
</StackLayout.BindingContext>
<Label Text="{Binding Source={x:Static sys:DateTime.Now},
StringFormat='This program started at {0:F}'}" />
<Label Text="But now..." />
<Label Text="{Binding Path=DateTime.Hour,
StringFormat='The hour is {0}'}" />
<Label Text="{Binding Path=DateTime.Minute,
StringFormat='The minute is {0}'}" />
<Label Text="{Binding Path=DateTime.Second,
StringFormat='The seconds are {0}'}" />
<Label Text="{Binding Path=DateTime.Millisecond,
StringFormat='The milliseconds are {0}'}" />
</StackLayout>
或者,您可以将StackLayout的BindingContext属性设置为包含的Binding
DateTime属性。 然后BindingContext成为DateTime值,它允许单独的绑定简单地引用.NET DateTime结构的属性:
<StackLayout VerticalOptions="Center"
BindingContext="{Binding Source={StaticResource dateTimeViewModel},
Path=DateTime}">
<Label Text="{Binding Source={x:Static sys:DateTime.Now},
StringFormat='This program started at {0:F}'}" />
<Label Text="But now..." />
<Label Text="{Binding Path=Hour,
StringFormat='The hour is {0}'}" />
<Label Text="{Binding Path=Minute,
StringFormat='The minute is {0}'}" />
<Label Text="{Binding Path=Second,
StringFormat='The seconds are {0}'}" />
<Label Text="{Binding Path=Millisecond,
StringFormat='The milliseconds are {0}'}" />
</StackLayout>
你可能会怀疑这会起作用! 在幕后,数据绑定通常会安装PropertyChanged事件处理程序并监视正在更改的特定属性,但在这种情况下它不能,因为数据绑定的源是DateTime值,而DateTime不实现INotifyPropertyChanged。 但是,这些Label元素的BindingContext随着ViewModel中DateTime属性的每次更改而更改,因此绑定基础结构此时会访问这些属性的新值。
由于Text属性上的单个绑定在长度和复杂性方面都有所减少,因此您可以删除Path属性名称并将所有内容放在一行上,并且不会混淆任何内容:
<StackLayout VerticalOptions="Center">
<StackLayout.BindingContext>
<Binding Path="DateTime">
<Binding.Source>
<toolkit:DateTimeViewModel />
</Binding.Source>
</Binding>
</StackLayout.BindingContext>
<Label Text="{Binding Source={x:Static sys:DateTime.Now},
StringFormat='This program started at {0:F}'}" />
<Label Text="But now..." />
<Label Text="{Binding Hour, StringFormat='The hour is {0}'}" />
<Label Text="{Binding Minute, StringFormat='The minute is {0}'}" />
<Label Text="{Binding Second, StringFormat='The seconds are {0}'}" />
<Label Text="{Binding Millisecond, StringFormat='The milliseconds are {0}'}" />
</StackLayout>
在本书的未来计划中,个人绑定将尽可能简短和优雅。