数据绑定简介
本课介绍如何创建可显示当前时间的应用。 本课介绍以下基础知识:数据绑定、将数据从代码获取到应用 UI,以及刷新为更新 UI 上的时钟显示。 本课程会为你打下基础,以便执行后面几课中更复杂的数据绑定任务。 那么,现在就开始吧!
1.创建项目
打开 Visual Studio(如果还没有运行的话)。 使用“空白应用(通用 Windows)”模板新建 C# Windows 通用项目。 将项目命名为“DatabindingSample”。 这个项目是你在整个“UI 和数据”模块学习期间要处理的项目。
在你单击“确定”后,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 个单位的边距。 接下来,让我们处理 TextBlock
的 Text
。
Text={x:Bind CurrentTime}
部分是你遇到的首个数据绑定部分。 x:Bind
是 XAML 标记扩展,它与应用的其余部分一起编译为 C# 代码。 随后,它将 TextBlock
的 Text
属性连接到 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 键或选择菜单中的“调试”/“开始调试”命令),便会编译和运行应用。 更棒的是,应用似乎能按预期运行! 当前时间显示在右上角。
但是,有些内容不正确,因为时钟不会更新。 它停滞在应用首次启动的时间。 应用如何知道何时刷新 TextBlock
中的值? 必须指示 UWP 运行时,每秒更新一次时钟。
5.指定绑定模式
{x:Bind}
绑定更适合提升性能。 也就是说,它们不会执行开发人员没有明确要求的任何操作。 所以,默认情况下,{x:Bind}
绑定只评估绑定源(在此示例中为 CurrentTime
属性)一次。 这种类型的绑定称为 OneTime
绑定。 若要让 UWP 框架继续更新 UI,必须显式指定另一种绑定模式:OneWay
或 TwoWay
。
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
属性更改,并在 TextBlock
的 Text
中反映它。 此附加基础结构占用很少的内存和 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 框架。
1.创建项目
打开 Visual Studio(如果还没有运行的话)。 使用 WPF 应用程序模板创建新的 C# WPF 项目。 将项目命名为“DatabindingSampleWPF”,再选择“确定”。 这个项目是你在整个“UI 和数据”模块学习期间要处理的项目。
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 标记扩展。 随后,它将 TextBlock
的 Text
属性连接到 CurrentTime
属性。但是哪个对象的 CurrentTime
属性呢?
数据绑定引用的对象是在 TextBlock
的 DataContext
中实例化。 因此,上面的 XAML 代码不仅创建 TextBlock
控件,还实例化 Clock
对象。 此外,上面的代码还将 TextBlock
的 Text
属性绑定到它创建的 Clock
对象的 CurrentTime
属性。 CurrentTime
属性称为“绑定源”,Text
属性称为“绑定目标”。
4.运行应用
如果现在启动应用(按 F5 键或选择菜单中的“调试”/“开始调试”命令),便会编译和运行应用。 更棒的是,应用似乎能按预期运行! 当前时间显示在右上角。
但是,有些内容不正确,因为时钟不会更新。 它停滞在应用首次启动的时间。 应用如何知道何时刷新 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 框架。