第十八章:MVVM(三)

简介:

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调用之外,代码隐藏文件是空的。最后四个标签的数据绑定显示更新时间,其更新速度与视频刷新率一样快:
2018_10_10_130902
通过将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>

在本书的未来计划中,个人绑定将尽可能简短和优雅。

目录
相关文章
|
存储 SQL 前端开发
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
借一个项目谈Android应用软件架构,你还在套用MVP 或MVVM吗
|
前端开发 Java 程序员
iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)
iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)
471 0
|
前端开发
[译] 实用的 MVVM 和 RxSwift
今天我们将使用 RxSwift 实现 MVVM 设计模式。对于那些刚接触 RxSwift 的人,我 在这里 专门做了一个部分来介绍。
1372 0
|
前端开发 JavaScript Android开发
|
前端开发 JavaScript Android开发
|
前端开发 JavaScript Android开发
|
前端开发 Android开发 Windows
|
前端开发 JavaScript Android开发
|
前端开发 JavaScript Android开发
|
JavaScript 前端开发 Android开发