数据绑定简介

已完成

Tech logo of U W P and W P F.

本课介绍如何创建可显示当前时间的应用。 本课介绍以下基础知识:数据绑定、将数据从代码获取到应用 UI,以及刷新为更新 UI 上的时钟显示。 本课程会为你打下基础,以便执行后面几课中更复杂的数据绑定任务。 那么,现在就开始吧!

Tech logo of U W P and W P F. W P F appears dimmed.

1.创建项目

打开 Visual Studio(如果还没有运行的话)。 使用“空白应用(通用 Windows)”模板新建 C# Windows 通用项目。 将项目命名为“DatabindingSample”。 这个项目是你在整个“UI 和数据”模块学习期间要处理的项目。

Screenshot of the Visual Studio Create a new project dialog box.

在你单击“确定”后,Visual Studio 会提示你输入目标和最低 Windows 版本。 本项目只是一个练习项目,而且你也不打算将它部署到运行旧版 Windows 的计算机。 因此,可以选择 Windows 最新版本作为最低版本和目标版本,再单击“确定”。

2.添加用于显示时钟的 TextBlock

在项目完全初始化并加载后,双击“解决方案资源管理器”中的“MainPage.xaml”,以打开它。

提示

如果屏幕空间不足,请使用左上角的下拉菜单来切换编辑器,以模拟分辨率较低的屏幕。 建议对本模块使用“13.3 英寸桌面(1280x720) 100% 缩放”,但要选择感觉最舒适的一个。

Grid 元素的开始标记和结束标记之间添加以下代码行。

<TextBlock HorizontalAlignment="Right" 
           Margin="10" 
           Text="{x:Bind CurrentTime}" />

这会在窗口右上角新建一个 TextBlock,与窗口边缘保持 10 个单位的边距。 接下来,让我们处理 TextBlockText

Text={x:Bind CurrentTime} 部分是你遇到的首个数据绑定部分。 x:Bind 是 XAML 标记扩展,它与应用的其余部分一起编译为 C# 代码。 随后,它将 TextBlockText 属性连接到 CurrentTime 属性。 如果现在尝试编译项目,会看到以下错误消息:

XamlCompiler 错误 WMC1110:绑定路径 "CurrentTime" 无效:在类型 "MainPage" 上找不到属性 "CurrentTime"

这表明编译器在 MainPage 中缺少 CurrentTime 属性。 一旦创建此属性,它的内容就会显示在右上角的 TextBlock 中。

备注

UWP 还支持如下旧数据绑定方法:Text={Bind CurrentTime}。 此旧方法的工作原理与 {x:Bind} 有点不同。 最值得注意的区别是,它不会在有拼写错误时提供编译时错误。 本模块以新绑定方式 {x:Bind} 为重点,这种方式提供编译时错误检查。 不过,{x:Bind} 不如 {Bind} 成熟,并且有新功能推出。正因为此,我们在创建项目时选择使用最新版 Windows。

3.创建 CurrentTime 属性

打开 MainPage.xaml.cs,并向 MainPage 类添加以下属性定义。

public string CurrentTime => DateTime.Now.ToLongTimeString();

如果不熟悉上面的语法,请注意它是“表达式体成员”。 它是在 C# 6.0 中引入的,并且是以下代码的简写:

public string CurrentTime 
{
    get { return DateTime.Now.ToLongTimeString(); }
}

4.运行应用

如果现在启动应用(按 F5 键或选择菜单中的“调试”/“开始调试”命令),便会编译和运行应用。 更棒的是,应用似乎能按预期运行! 当前时间显示在右上角。

Screenshot of the running app with the clock.

但是,有些内容不正确,因为时钟不会更新。 它停滞在应用首次启动的时间。 应用如何知道何时刷新 TextBlock 中的值? 必须指示 UWP 运行时,每秒更新一次时钟。

5.指定绑定模式

{x:Bind} 绑定更适合提升性能。 也就是说,它们不会执行开发人员没有明确要求的任何操作。 所以,默认情况下,{x:Bind} 绑定只评估绑定源(在此示例中为 CurrentTime 属性)一次。 这种类型的绑定称为 OneTime 绑定。 若要让 UWP 框架继续更新 UI,必须显式指定另一种绑定模式:OneWayTwoWay

TwoWay 绑定模式表明,在 C# 代码(逻辑)和 UI 之间进行双向绑定。 稍后,若要绑定到用户可以操作的控件,这种类型的绑定将很有用。 但对于 TextBlock,采用 OneWay 绑定是首选的做法,因为数据更改只源于代码,绝不会源于 UI。

若要为 TextBlock 指定 OneWay 绑定模式,请将 {x:Bind CurrentTime} 更改为 {x:Bind CurrentTime, Mode=OneWay}Grid 中的整个 TextBlock 标记现在应该看起来像这样的标记。

<TextBlock HorizontalAlignment="Right" 
           Margin="10" 
           Text="{x:Bind CurrentTime, Mode=OneWay}" />

绑定会指示 UWP 运行时,生成必要的基础结构来跟踪 CurrentTime 属性更改,并在 TextBlockText 中反映它。 此附加基础结构占用很少的内存和 CPU 周期。正因为此,它不会默认生成。

如果现在运行应用,应该会发现时钟仍不更新。 需要通知系统,CurrentTime 属性已更改。

6.实现 INotifyPropertyChanged 接口

此通知通过 INotifyPropertyChanged 接口发出。 它是一个简单的接口,包含一个事件。

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

任何使用简单 C# 属性作为数据绑定源的类都必须实现 INotifyPropertyChanged 接口。 此类必须在应更新 UI 时触发 PropertyChanged 事件。 接下来,向 MainPage 类声明添加此接口。

public sealed partial class MainPage : Page, INotifyPropertyChanged

还必须向类添加 PropertyChanged 事件,从而实现此接口。

public event PropertyChangedEventHandler PropertyChanged;

7.每秒调用一次 PropertyChanged 事件

剩下的全部操作就是,每当要更新时钟(即每秒),调用一次 PropertyChanged 事件。 首先,在 MainPage 类中声明 DispatcherTimer 对象。

private DispatcherTimer _timer;

现在,在构造函数中将它设置为(在 InitializeComponent 调用之后),每秒触发一次。

_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

_timer.Tick += (sender, o) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

_timer.Start();

上面的第一行代码创建时间间隔为 1 秒的计时器,最后一行代码启动计时器。 接下来看看在计时器触发后会发生什么(即第二行代码执行的操作)。

PropertyChanged?.Invoke 是简写,用于检查事件是否为 NULL;如果不为 NULL,便会调用事件。 与大多数事件一样,第一个参数是 sender (this)。 PropertyChanged 事件的第二个参数是新建的 PropertyChangedEventArgs 对象,其中包含需要将字符串用作属性名的构造函数。 因此,PropertyChanged 事件订阅者(在此示例中为 UWP 系统)会收到更新后的属性的属性名,并能执行相应操作。

提示

不要对属性名使用字符串文本(如 "CurrentTime")。 使用字符串本身很容易出现拼写错误,这可能会导致在没有更新 UI 时出现无法调试的问题。 此外,如果字符串常量未更新,正常重命名属性也可能会引入错误。 最好始终使用 nameof 表达式,它不受拼写错误的影响,并能执行重命名。

整个 MainPage.xaml.cs 应如下所示:

namespace DatabindingSample
{
    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        private DispatcherTimer _timer;

        public MainPage()
        {
            this.InitializeComponent();
            _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

            _timer.Tick += (sender, o) =>
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

            _timer.Start();
        }

        public string CurrentTime => DateTime.Now.ToLongTimeString();
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

8.运行应用

如果现在运行应用,应该会发现时钟更新了。 恭喜!你已创建首个数据绑定!

9.总结

至此,已了解如何使用 {x:Bind},创建自动快速将数据从代码获取到 UWP 应用 UI 的方法。 此方法在编译时接受检查。 此外,你也对 INotifyPropertyChanged 接口有所熟悉。 此接口可便于应用在数据绑定属性更改且应更新 UI 时通知 UWP 框架。

Tech logo of U W P and W P F. U W P appears dimmed.

1.创建项目

打开 Visual Studio(如果还没有运行的话)。 使用 WPF 应用程序模板创建新的 C# WPF 项目。 将项目命名为“DatabindingSampleWPF”,再选择“确定”。 这个项目是你在整个“UI 和数据”模块学习期间要处理的项目。

Screenshot of the Visual Studio Create a new WPF project dialog box.

2.创建 Clock 类

由于任务是显示当前时间,因此最好先创建 Clock 类。 右键单击“解决方案资源管理器”中的“DatabindingSampleWPF”项目,选择“添加”/“类”,再输入“Clock”作为类名。

将下面的代码复制到新建的文件中:

using System;

namespace DatabindingSampleWPF
{
    public class Clock
    {
        public string CurrentTime => DateTime.Now.ToLongTimeString();
    }
}

如果不熟悉上面 CurrentTime 属性的语法,请注意它是“表达式体成员”。 它是在 C# 6.0 中引入的,并且是以下代码的简写:

public string CurrentTime 
{
    get { return DateTime.Now.ToLongTimeString(); }
}

可以看到,到目前为止,Clock 类只是以长时间格式返回当前时间的简单 string 属性。 下一步是在应用本身中显示时间。

3.添加用于显示时钟的 TextBlock

如果已在 Visual Studio 中打开 MainWindow.xaml,请选择它的选项卡。否则,可以在“解决方案资源管理器”中双击它。

Grid 元素的开始标记和结束标记之间添加以下代码行。

<TextBlock HorizontalAlignment="Right" 
           VerticalAlignment="Top"
           Margin="10" 
           Text="{Binding CurrentTime}">
    <TextBlock.DataContext>
        <local:Clock/>
    </TextBlock.DataContext>
</TextBlock>

此标记会在窗口右上角新建一个 TextBlock,与窗口边缘保持 10 个单位的边距。

Text="{Binding CurrentTime}" 部分是你遇到的首个数据绑定部分。 {Binding} 是 XAML 标记扩展。 随后,它将 TextBlockText 属性连接到 CurrentTime 属性。但是哪个对象的 CurrentTime 属性呢?

数据绑定引用的对象是在 TextBlockDataContext 中实例化。 因此,上面的 XAML 代码不仅创建 TextBlock 控件,还实例化 Clock 对象。 此外,上面的代码还将 TextBlockText 属性绑定到它创建的 Clock 对象的 CurrentTime 属性。 CurrentTime 属性称为“绑定源”,Text 属性称为“绑定目标”。

4.运行应用

如果现在启动应用(按 F5 键或选择菜单中的“调试”/“开始调试”命令),便会编译和运行应用。 更棒的是,应用似乎能按预期运行! 当前时间显示在右上角。

Screenshot of the running app with the clock.

但是,有些内容不正确,因为时钟不会更新。 它停滞在应用首次启动的时间。 应用如何知道何时刷新 TextBlock 中的值? 必须指示 WPF 运行时,每秒更新一次时钟。

也就是说,需要通知系统,CurrentTime 属性已更改。

5.实现 INotifyPropertyChanged 接口

此通知通过 INotifyPropertyChanged 接口发出。 它是一个简单的接口,包含一个事件。

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

任何使用简单 C# 属性作为数据绑定源的类都必须实现 INotifyPropertyChanged 接口。 此类必须在应更新 UI 时触发 PropertyChanged 事件。 接下来,向 Clock 类声明添加此接口。

using System.ComponentModel;

public class Clock : INotifyPropertyChanged
{

还必须向类添加 PropertyChanged 事件,从而实现此接口。

public event PropertyChangedEventHandler? PropertyChanged;

6.每秒调用一次 PropertyChanged 事件

剩下的全部操作就是,每当要更新时钟(即每秒),调用一次 PropertyChanged 事件。 首先,向 using 添加 System.Windows.Threading 命名空间,并在 Clock 类中声明 DispatcherTimer 对象。

private DispatcherTimer _timer;

现在,在构造函数中将它设置为,每秒触发一次。

public Clock()
{
    _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

    _timer.Tick += (sender, o) => PropertyChanged?.Invoke(this,
            new PropertyChangedEventArgs(nameof(CurrentTime)));

    _timer.Start();
}

构造函数中的第一行代码创建时间间隔为 1 秒的计时器,最后一行代码启动计时器。 接下来看看在计时器触发后会发生什么(即第二行代码执行的操作)。

PropertyChanged?.Invoke 是简写,用于检查事件是否为 NULL;如果不为 NULL,便会调用事件。 与大多数事件一样,第一个参数是 sender (this)。 PropertyChanged 事件的第二个参数是新建的 PropertyChangedEventArgs 对象,其中包含需要将字符串用作属性名的构造函数。 因此,PropertyChanged 事件订阅者(在此示例中为 WPF 系统)会收到更新后的属性的属性名,并能执行相应操作。

提示

不要对属性名使用字符串文本(如 "CurrentTime")。 使用字符串本身很容易出现拼写错误,这可能会导致在没有更新 UI 时出现无法调试的问题。 此外,如果字符串常量未更新,正常重命名属性也可能会引入错误。 最好始终使用 nameof 表达式,它不受拼写错误的影响,并能执行重命名操作。

整个 Clock.cs 应如下所示:

namespace DatabindingSampleWPF
{
    using System;
    using System.ComponentModel;
    using System.Windows.Threading;

    public class Clock : INotifyPropertyChanged
    {
        private DispatcherTimer _timer;

        public string CurrentTime => DateTime.Now.ToLongTimeString();

        public event PropertyChangedEventHandler PropertyChanged;

        public Clock()
        {
            // setup _timer to refresh CurrentTime
            _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
            _timer.Tick += (sender, o) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));
            _timer.Start();
        }
    }
}

7.运行应用

如果现在运行应用,应该会发现时钟更新了。 你已创建首个数据绑定!

8.总结

至此,已了解如何使用 {Binding},创建自动快速将数据从代码获取到 WPF 应用 UI 的方法。 此外,你也对 INotifyPropertyChanged 接口有所熟悉。 此接口可便于应用在数据绑定属性更改且应更新 UI 时通知 WPF 框架。