WPF数据绑定学习笔记
面向已经了解 WPF 基本控件和依赖属性,准备系统学习数据绑定的读者。
推荐环境:.NET 10 LTS + WPF。随附示例使用net9.0-windows,以便在当前安装了 .NET 9 SDK 的电脑上直接编译。数据绑定机制在 .NET 9 和 .NET 10 中没有变化。
最后更新:2026-10-19
目录
写在前面
这份笔记怎么用
这份笔记有三种阅读方式:
| 路线 | 做法 | 适合情况 |
|---|---|---|
| 直接运行 | 打开随附示例,逐个切换六个页签 | 想先看到效果 |
| 逐步跟做 | 从第 1 章开始,在同一个 WPF 项目中添加代码 | 想亲手敲一遍 |
| 按需查阅 | 直接阅读对应来源类型的章节 | 已经遇到具体绑定问题 |
建议至少完成第 1 章,因为后面的所有示例都建立在同一套绑定语法上。
示例项目位置
1 | C:\Users\North\Desktop\WPF学习笔记\示例代码\WpfDataBindingDemo |
运行方式:
1 | cd "C:\Users\North\Desktop\WPF学习笔记\示例代码\WpfDataBindingDemo" |
如果使用 Visual Studio:
- 打开项目。
- 确认启动项目是
WpfDataBindingDemo。 - 按
F5以调试方式启动。 - 打开“输出”窗口,把显示来源切换为“调试”。
2026 年的目标框架
新项目如果安装了 .NET 10 SDK,推荐:
1 | <TargetFramework>net10.0-windows</TargetFramework> |
如果电脑上只有 .NET 9 SDK,就使用:
1 | <TargetFramework>net9.0-windows</TargetFramework> |
本篇示例已经在 .NET 9 上编译并启动验证。把目标框架改成 .NET 10 后,数据绑定代码不需要修改。
先建立直觉:数据绑定到底是什么
数据绑定是在“目标属性”和“来源属性”之间建立一条同步通道:
1 | 来源对象的值 |
例如:
1 | <TextBlock Text="{Binding EmployeeName}" /> |
这段 XAML 的含义不是“把 EmployeeName 复制成一段固定文本”,而是:
1 | 每当 EmployeeName 变化 |
目标、来源、路径
学习数据绑定时,先分清三个词:
| 名称 | 含义 | 示例 |
|---|---|---|
| 目标 | 接收值的控件属性 | TextBlock.Text |
| 来源 | 提供值的对象 | MainViewModel |
| 路径 | 来源对象上的属性 | EmployeeName |
对应写法:
1 | Text="{Binding EmployeeName}" |

图 1:目标属性、来源属性、绑定表达式和数据流向的关系。
可以读成:
1 | 把来源对象上的 EmployeeName |
来源对象从哪里来?如果没有显式指定 Source,通常来自 DataContext。
DataContext 是什么
DataContext 是 WPF 中非常重要的概念。它是一个可继承的依赖属性,用来给绑定提供默认来源。
示例在窗口上设置了:
1 | <Window.DataContext> |
之后子元素的绑定:
1 | <TextBlock Text="{Binding Title}" /> |
会去当前 DataContext 中查找 Title 属性。
可以用下面的关系理解:
1 | Window.DataContext = MainViewModel |
一个绑定表达式的关键信息
完整的绑定通常包含:
| 设置 | 作用 |
|---|---|
Source |
指定来源对象 |
ElementName |
指定另一个 XAML 元素作为来源 |
RelativeSource |
相对于目标元素查找来源 |
Path |
指定来源上的属性 |
Mode |
指定数据流向 |
UpdateSourceTrigger |
指定什么时候写回来源 |
Converter |
在来源值和目标值之间转换 |
StringFormat |
格式化显示文本 |
后面六章会反复使用这些设置。

图 2:六类常见来源及其通知方式和适用场景。
绑定的三种常见流向
| 模式 | 方向 | 典型场景 |
|---|---|---|
OneWay |
来源到目标 | 只读展示 |
TwoWay |
来源和目标双向同步 | 文本框编辑 |
OneTime |
只读取一次 | 静态配置 |
还有两种不常用但需要知道的方向:
| 模式 | 方向 |
|---|---|
OneWayToSource |
目标到来源 |
Default |
使用目标属性的默认模式 |
1. 数据绑定的基本关系
本节目标
学完本节,你应该能够:
- 写出一个最小绑定表达式。
- 说清目标、来源和路径分别是什么。
- 使用
ElementName把两个控件绑定起来。 - 解释
OneWay、TwoWay和OneTime的区别。 - 知道
UpdateSourceTrigger控制什么。
1.1 最小示例:滑块和文本框
在 XAML 中放入一个 Slider 和一个 TextBox:
1 | <StackPanel Margin="30"> |
运行后:
- 拖动滑块,文本框中的数字会变化。
- 在文本框中输入数字,滑块位置也会变化。
这就是最基本的双向绑定。
1.2 拆解绑定表达式
1 | Text="{Binding ElementName=ValueSlider, |
逐项解释:
| 设置 | 值 | 含义 |
|---|---|---|
| 目标属性 | Text |
文本框中显示的文字 |
ElementName |
ValueSlider |
来源是名为 ValueSlider 的元素 |
Path |
Value |
来源属性是 Slider.Value |
Mode |
TwoWay |
双向同步 |
UpdateSourceTrigger |
PropertyChanged |
目标变化时立即写回来源 |
1.3 为什么这里不用 DataContext
因为来源已经通过 ElementName 明确指定了:
1 | ElementName=ValueSlider |
只要绑定中有明确的来源,就不需要再依赖 DataContext。
常见来源写法:
| 写法 | 来源 |
|---|---|
{Binding Name} |
当前 DataContext 的 Name |
{Binding ElementName=Other, Path=Text} |
名为 Other 的元素 |
{Binding Source={StaticResource Data}, Path=Name} |
资源对象 |
{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth} |
目标元素自身 |
1.4 目标属性必须是依赖属性
绑定目标必须是依赖属性。例如:
1 | Text="{Binding EmployeeName}" |
TextBlock.Text 是依赖属性,所以可以绑定。
如果把绑定写到普通 CLR 属性上,通常不会得到 WPF 绑定系统的支持。
可以记成:
1 | 目标端:依赖属性 |
来源端可以是普通 CLR 属性,这是 WPF 数据绑定非常常见的设计。
1.5 OneWay:只从来源流向目标
1 | <TextBlock Text="{Binding Title, Mode=OneWay}" /> |
含义:
1 | Title 改变 -> TextBlock.Text 改变 |
只读显示通常使用 OneWay。
很多目标属性的默认模式就是 OneWay,所以有时可以省略。
1.6 TwoWay:双向同步
1 | <TextBox Text="{Binding Title, Mode=TwoWay}" /> |
含义:
1 | Title 改变 -> TextBox.Text 改变 |
编辑类控件经常使用双向绑定,例如:
TextBox.TextCheckBox.IsCheckedSlider.ValueComboBox.SelectedItem
1.7 OneTime:只读取一次
1 | <TextBlock Text="{Binding Title, Mode=OneTime}" /> |
绑定只在初始化时读取一次来源值。之后来源变化不会继续更新目标。
适合:
- 启动后不会变化的配置
- 一次性标题
- 性能敏感且确定不会变化的静态内容
不要把需要实时更新的内容写成 OneTime,否则很容易出现“值明明变了,界面不动”的问题。
1.8 UpdateSourceTrigger 控制写回时机
对于双向绑定,目标变化后什么时候写回来源,由 UpdateSourceTrigger 控制:
| 值 | 含义 | 适合场景 |
|---|---|---|
PropertyChanged |
目标属性每次变化就写回 | 实时搜索、即时预览 |
LostFocus |
目标失去焦点时写回 | 普通表单 |
Explicit |
调用 UpdateSource() 时写回 |
需要手动提交 |
Default |
使用目标属性的默认行为 | 通常不用手动指定 |
例如:
1 | <TextBox Text="{Binding Title, |
每输入一个字符,来源属性就会更新。
如果使用 LostFocus,只有离开文本框后才更新。
1.9 BindingExpression 是什么
同一个 Binding 对象可以被多个目标共享,但每个目标与来源之间的实际连接叫 BindingExpression。
可以简单理解为:
1 | Binding:描述“怎样绑定” |
通常写界面时不需要直接操作它。排查绑定错误或手动调用 UpdateSource 时可能会用到。
1.10 绑定失败时怎么办
绑定路径写错时,WPF 通常不会让程序崩溃,而是:
- 在调试输出中记录绑定错误。
- 目标属性可能保持默认值或显示为空。
例如:
1 | <TextBlock Text="{Binding EmplyeeName}" /> |
这里把 EmployeeName 拼错了。程序可能照常运行,但文本为空。
排查顺序:
- 查看输出窗口的
System.Windows.Data Error。 - 检查
Path拼写。 - 检查
DataContext是否正确。 - 检查属性是否是
public。 - 检查来源对象是否真的包含这个属性。
1.11 本节小结
| 问题 | 答案 |
|---|---|
| 绑定由什么组成 | 目标、来源、路径 |
| 目标属性要求 | 依赖属性 |
| 来源属性要求 | 依赖属性或普通属性 |
| 双向绑定怎么写 | Mode=TwoWay |
| 即时写回怎么写 | UpdateSourceTrigger=PropertyChanged |
| 路径写错会崩溃吗 | 通常不会,但绑定会失败 |
小练习
- 把滑块示例改成
OneWay,观察从文本框输入时滑块是否变化。 - 把
UpdateSourceTrigger改成LostFocus,观察输入过程中和离开文本框后的区别。 - 故意把
Path=Value改成Path=Valu,查看输出窗口的错误信息。
2. 基于依赖对象的数据绑定
本节目标
理解依赖对象作为绑定来源时的行为,并能够:
- 定义一个
DependencyObject来源类。 - 用依赖属性保存绑定来源值。
- 在 XAML 中把控件绑定到依赖对象。
- 解释为什么依赖属性天然适合作为绑定来源。
- 知道依赖对象来源适合什么场景。
2.1 什么是依赖对象
DependencyObject 是 WPF 属性系统的基础类型。依赖属性必须注册在依赖对象上。
常见依赖对象包括:
WindowGridTextBoxButton- 自定义控件
也可以自己定义:
1 | public class BindingSourceObject : DependencyObject |
2.2 依赖属性为什么适合做来源
依赖属性由 WPF 属性系统管理,变化通知是属性系统能力的一部分。绑定读取依赖属性时,可以直接接入这套通知机制。
对比普通属性:
| 来源属性类型 | 变化通知 |
|---|---|
| 依赖属性 | 属性系统原生支持 |
| 普通属性 | 通常要实现 INotifyPropertyChanged |
这就是为什么控件属性作为绑定来源时通常不需要额外写通知代码。
2.3 定义绑定来源对象
新建 DependencyObjects/BindingSourceObject.cs:
1 | using System.Windows; |
这里有两个依赖属性:
| 属性 | 类型 | 用途 |
|---|---|---|
DisplayName |
string |
显示名称 |
AccentBrush |
Brush |
强调色 |
2.4 在窗口代码中创建来源对象
在 MainWindow.xaml.cs 中:
1 | public partial class MainWindow : Window |
这里把 BindingSourcePreview 的 DataContext 设置为来源对象。
2.5 在 XAML 中绑定到依赖对象
先在窗口资源中声明转换器:
1 | <Window.Resources> |
然后写:
1 | <TextBlock Text="{Binding ElementName=BindingSource, |
但 BindingSource 是窗口的普通 CLR 属性,不是 XAML 命名元素。要把它作为 ElementName 来源,需要先把它暴露成一个命名元素,或者在代码中设置 DataContext。
示例采用后者:
1 | <Border x:Name="BindingSourcePreview" |
并在构造函数中:
1 | BindingSourcePreview.DataContext = BindingSource; |
这样内部绑定会从 BindingSource 读取属性。
2.6 完整示例:依赖对象页签
示例中的 XAML 使用:
1 | <Border x:Name="BindingSourcePreview" |
当代码改变来源对象的依赖属性:
1 | BindingSource.DisplayName = "新的标题"; |
绑定目标会自动刷新。
2.7 依赖对象来源的优点
| 优点 | 说明 |
|---|---|
| 原生变化通知 | 依赖属性由 WPF 管理 |
| 支持样式和动画 | 依赖属性可以参与这些机制 |
| 支持属性值继承 | 可以设置 Inherits |
| 与 XAML 配合自然 | 控件本身就是依赖对象 |
2.8 依赖对象来源的限制
依赖对象适合界面属性,但不是什么数据都应该做成依赖对象。
| 限制 | 说明 |
|---|---|
必须继承 DependencyObject |
普通 POCO 不能直接使用 |
| 依赖属性写法较冗长 | 注册、包装器、元数据 |
| 不适合业务实体 | 业务对象通常使用普通 C# 类和通知接口 |
| 需要理解属性系统 | 调试比普通属性复杂 |
实际项目中:
- 控件、视觉属性:依赖对象或依赖属性
- ViewModel、业务实体:普通类和
INotifyPropertyChanged
2.9 依赖对象和普通对象的对比
| 对比项 | 依赖对象 | 普通对象 |
|---|---|---|
| 基类 | DependencyObject |
任意类 |
| 属性系统 | WPF 依赖属性 | CLR 属性 |
| 变化通知 | 原生支持 | 通常实现 INotifyPropertyChanged |
| 样式和动画 | 原生支持 | 不直接支持 |
| 适合 | 控件、视觉对象 | ViewModel、业务对象 |
2.10 本节小结
| 问题 | 答案 |
|---|---|
| 依赖对象可以作为来源吗 | 可以 |
| 依赖属性需要额外通知吗 | 通常不需要 |
| 怎样把依赖对象设为来源 | 设置目标元素的 DataContext 或使用 Source |
| 适合业务实体吗 | 通常不适合 |
| 适合控件属性吗 | 非常适合 |
小练习
- 给
BindingSourceObject增加一个Description依赖属性,并绑定到TextBlock。 - 在代码中修改
DisplayName,观察界面是否自动更新。 - 尝试用
Source属性而不是DataContext来绑定同一个对象。
3. 基于普通数据类型的数据绑定
本节目标
理解普通 CLR 属性作为绑定来源时的通知机制,并能够:
- 在 ViewModel 中实现
INotifyPropertyChanged。 - 绑定
string、int、decimal等普通类型。 - 使用
CallerMemberName减少字符串拼写错误。 - 区分“值类型”和“普通属性”这两个概念。
- 知道什么时候不需要通知。
3.1 普通类型也能绑定
绑定来源不要求是依赖对象。下面这些都可以:
1 | public string Title { get; set; } |
但有一个关键条件:
如果希望来源值变化后界面自动更新,普通属性通常需要通知机制。
3.2 没有通知会发生什么
假设 ViewModel 写成:
1 | public class MainViewModel |
XAML:
1 | <TextBlock Text="{Binding Title}" /> |
第一次显示时会读取“初始标题”。
如果代码执行:
1 | viewModel.Title = "新标题"; |
界面不一定会自动更新,因为没有通知告诉绑定系统“Title 变了”。
3.3 实现 INotifyPropertyChanged
1 | using System.ComponentModel; |
当 Title 改变时,绑定系统收到通知,然后重新读取新值。
3.4 CallerMemberName 有什么好处
OnPropertyChanged() 没有传参数,为什么也能知道属性名?
因为参数上有:
1 | [] |
编译器会自动把调用者的成员名传进去。上面的调用等价于:
1 | OnPropertyChanged(nameof(Title)); |
这样可以避免手写字符串:
1 | // 容易拼错,而且重命名后可能忘记改 |
3.5 在 XAML 中绑定普通属性
窗口设置 DataContext:
1 | <Window.DataContext> |
绑定:
1 | <TextBox Text="{Binding Title, |
效果:
1 | 在 TextBox 输入 |
3.6 值类型和引用类型都能绑定
普通数据类型不只指 int、decimal。字符串、布尔值、枚举、日期都可以绑定。
示例:
1 | public bool IsEnabled { get; set; } |
对应 XAML:
1 | <CheckBox IsChecked="{Binding IsEnabled}" /> |
是否显示得好,取决于目标属性能否理解来源值。不能直接转换时,可以使用 StringFormat 或转换器。
3.7 StringFormat 格式化
金额:
1 | <TextBlock Text="{Binding Salary, |
日期:
1 | <TextBlock Text="{Binding CreatedAt, |
注意 StringFormat 前面的 {}:
1 | StringFormat={}{0:C0} |
它用来告诉 XAML 解析器:后面的花括号不是标记扩展。
3.8 什么时候不需要 INotifyPropertyChanged
如果属性只读取一次,之后不会变化,可以不实现通知:
1 | public string ProductCode { get; } = "P-1001"; |
如果使用:
1 | <TextBlock Text="{Binding ProductCode, Mode=OneTime}" /> |
一次读取即可。
但表单编辑、实时状态、动态计算结果通常需要通知。
3.9 普通属性绑定的常见错误
| 错误 | 结果 | 修正 |
|---|---|---|
| 没有实现通知 | 改值后界面不更新 | 实现 INotifyPropertyChanged |
| 通知了错误的属性名 | 目标不刷新 | 使用 CallerMemberName |
属性不是 public |
绑定找不到 | 改为 public |
DataContext 为空 |
绑定无来源 | 设置 DataContext |
| 双向绑定目标不支持写回 | 只显示不更新来源 | 检查目标属性是否可写 |
使用 OneTime |
后续变化不更新 | 改为 OneWay 或 TwoWay |
3.10 本节小结
| 问题 | 答案 |
|---|---|
| 普通类型能绑定吗 | 能 |
| 为什么需要通知 | 让界面知道来源变了 |
| 最常用通知接口 | INotifyPropertyChanged |
| 怎样减少字符串错误 | 使用 [CallerMemberName] |
| 字符串能绑定吗 | 能 |
| 数字能格式化吗 | 能,使用 StringFormat |
小练习
- 给
MainViewModel增加Age属性,并绑定到TextBlock。 - 把
Age改成只读属性,观察是否还能作为绑定来源。 - 使用
StringFormat把工资显示成带货币符号的格式。 - 移除
PropertyChanged通知,观察输入后另一个文本是否还更新。
4. 基于对象的数据绑定
本节目标
理解对象作为绑定来源时的常见模式,并能够:
- 把对象属性绑定到控件。
- 使用
SelectedItem绑定当前选中对象。 - 使用
ObservableCollection<T>显示动态列表。 - 解释对象属性变化和集合变化是两种不同通知。
- 知道什么时候需要
INotifyPropertyChanged,什么时候需要ObservableCollection<T>。
4.1 一个对象可以包含多个属性
先定义:
1 | public class Employee |
ViewModel:
1 | public Employee SelectedEmployee { get; set; } |
绑定:
1 | <TextBlock Text="{Binding SelectedEmployee.Name}" /> |
这叫“路径绑定”。Path 可以逐级访问对象属性。
4.2 对象属性变化通知
如果只显示对象,SelectedEmployee.Name 可以正常读取。
但如果用户在文本框中修改姓名,并希望其他位置同步更新,对象属性也需要通知。
因此 Employee 实现:
1 | public class Employee : INotifyPropertyChanged |
这样:
1 | TextBox 修改 Name |
4.3 集合绑定
单个对象适合详情展示,多个对象通常需要集合。
示例:
1 | public ObservableCollection<Employee> Employees { get; } = new() |
绑定:
1 | <ListBox ItemsSource="{Binding Employees}" |
ObservableCollection<T> 会在添加、删除、移动元素时通知界面。
4.4 ObservableCollection 和 List 的区别
| 集合 | 集合变化通知 | 适合 |
|---|---|---|
List<T> |
没有 | 静态数据、查询结果 |
ObservableCollection<T> |
有 | 可增删改的界面列表 |
如果列表内容会动态变化,优先使用 ObservableCollection<T>。
注意:
1 | ObservableCollection<T> 通知“集合变了” |
这两个职责不同。
4.5 SelectedItem 绑定
列表通常还需要绑定“当前选中项”:
1 | <ListBox ItemsSource="{Binding Employees}" |
SelectedEmployee 需要通知:
1 | private Employee? _selectedEmployee; |
否则列表选择变化后,其他绑定到 SelectedEmployee 的控件不会更新。
4.6 详情面板怎样绑定
详情面板可以设置:
1 | <StackPanel DataContext="{Binding SelectedEmployee}"> |
这时内部绑定路径从:
1 | SelectedEmployee.Name |
变成了:
1 | Name |
因为 DataContext 已经切换到选中的员工对象。
4.7 完整对象模型
示例中的 Employee:
1 | public class Employee : INotifyPropertyChanged |
4.8 对象绑定和列表绑定的配合
完整关系:
1 | MainViewModel |
界面:
1 | ListBox -> Employees |
4.9 添加新对象
示例按钮:
1 | private void AddEmployee_Click(object sender, RoutedEventArgs e) |
点击后:
ObservableCollection发出集合变化通知。ListBox增加新项。SelectedEmployee指向新对象。- 详情区切换到新对象。
4.10 本节小结
| 问题 | 答案 |
|---|---|
| 对象属性怎样绑定 | {Binding Object.Property} |
| 对象属性变化怎样通知 | INotifyPropertyChanged |
| 集合变化怎样通知 | ObservableCollection<T> |
| 选中项怎样绑定 | SelectedItem |
| 详情面板怎样简化路径 | 设置 DataContext 为选中对象 |
| 两种通知一样吗 | 不一样,一个管属性,一个管集合 |
小练习
- 给
Employee增加Email属性,并绑定到详情面板。 - 把
ObservableCollection换成List,点击添加按钮观察列表是否更新。 - 给列表增加删除按钮,删除后观察选中项怎样变化。
5. 基于ADO.NET的数据绑定
本节目标
理解 ADO.NET 数据对象怎样参与 WPF 绑定,并能够:
- 创建内存中的
DataTable。 - 使用
DataView进行筛选和排序。 - 把
DataView绑定到DataGrid。 - 理解
DataRowView和列绑定。 - 知道 ADO.NET 绑定适合什么场景。
5.1 ADO.NET 在 WPF 中的角色
WPF 不要求一定使用 Entity Framework 或 ORM。ADO.NET 的 DataTable、DataView 和 DataSet 仍然可以参与绑定。
典型流程:
1 | 数据库查询 |
本篇示例不连接真实数据库,而是在内存中创建 DataTable,这样可以专注于绑定机制。
5.2 创建 DataTable
1 | private static DataTable CreateEmployeeDataTable() |
这段代码做了三件事:
- 创建表结构。
- 定义列和列类型。
- 插入示例行。
5.3 DataTable 和 DataView 的区别
| 对象 | 作用 |
|---|---|
DataTable |
保存行和列 |
DataView |
对表提供排序、筛选和当前视图 |
可以把它理解为:
1 | DataTable = 原始数据 |
同一个 DataTable 可以有多个 DataView,每个视图使用不同的筛选或排序。
5.4 创建 DataView
1 | private readonly DataView _employeeView; |
DefaultView 返回表的默认视图。
5.5 绑定到 DataGrid
1 | <DataGrid x:Name="EmployeeDataGrid" |
这里的 Binding="{Binding Name}" 不是绑定到某个 ViewModel 属性,而是绑定到当前行对象上的 Name 列。
5.6 DataRowView 是什么
DataView 中的每一行在绑定时通常表现为 DataRowView。
可以把它理解为“视图中的一行”。它把列名暴露成可绑定属性,所以:
1 | Binding="{Binding Name}" |
可以读到列 Name。
如果列名写成:
1 | table.Columns.Add("EmployeeName", typeof(string)); |
绑定就要改成:
1 | Binding="{Binding EmployeeName}" |
列名和绑定路径必须一致。
5.7 使用 RowFilter 筛选
示例:
1 | private void FilterDevelopment_Click(object sender, RoutedEventArgs e) |
RowFilter 使用类似 SQL 的表达式语法,但它是在内存中的 DataView 上执行,不是发给数据库。
运行后点击“只看研发部”,表格会只显示研发部员工。
5.8 使用 Sort 排序
1 | private void SortBySalary_Click(object sender, RoutedEventArgs e) |
DESC 表示降序,ASC 表示升序。
可以组合多列:
1 | _employeeView.Sort = "Department ASC, Salary DESC"; |
5.9 ADO.NET 绑定的优点
| 优点 | 说明 |
|---|---|
| 内置表格结构 | 列名、类型和行集合 |
| 支持筛选排序 | DataView 提供现成能力 |
| 与 DataGrid 配合好 | 行列绑定直观 |
| 兼容传统数据库代码 | 已有 ADO.NET 项目容易接入 |
5.10 ADO.NET 绑定的限制
| 限制 | 说明 |
|---|---|
| 数据模型偏表格 | 复杂对象关系不够自然 |
| 强类型较弱 | 列名通常以字符串表示 |
| 业务逻辑容易分散 | 不要把大量业务规则塞进 DataTable |
| 现代项目常用 ORM | Entity Framework、Dapper 等更常见 |
学习绑定机制时,DataTable 很直观;新项目选择数据访问方案时,要根据项目规模和团队习惯决定。
5.11 真实数据库怎样接入
如果使用 SQL Server,可以大致写成:
1 | using var connection = new SqlConnection(connectionString); |
本篇不执行真实数据库连接,是为了让示例在任何电脑上都能直接运行。
5.12 本节小结
| 问题 | 答案 |
|---|---|
DataTable 是什么 |
内存中的表 |
DataView 是什么 |
表的筛选排序视图 |
| 怎样绑定表格 | 绑定 DataView |
| 行绑定写什么 | Binding="{Binding 列名}" |
| 怎样筛选 | 设置 RowFilter |
| 怎样排序 | 设置 Sort |
小练习
- 增加一个“只看工资大于 10000”的筛选按钮。
- 把
Sort改成按部门升序、工资降序。 - 给
DataTable增加一个Email列,并在DataGrid中显示。 - 修改列名,观察绑定路径是否必须同步变化。
6. 基于Linq的数据绑定
本节目标
理解 LINQ 查询结果怎样参与 WPF 绑定,并能够:
- 用 LINQ 过滤、排序和投影对象集合。
- 把查询结果赋给
ItemsSource。 - 理解 LINQ 查询结果和可观察集合的区别。
- 知道什么时候需要重新执行查询。
- 知道匿名类型绑定的注意事项。
6.1 LINQ 不是一种控件
LINQ 是 C# 中的查询能力。它本身不是 WPF 绑定机制,但它的结果可以作为绑定来源。
典型流程:
1 | 原始集合 |
6.2 先准备对象集合
沿用前面的 Employee:
1 | public ObservableCollection<Employee> Employees { get; } |
示例数据:
1 | Employees = new ObservableCollection<Employee> |
6.3 基本 LINQ 查询
目标:找出工资不低于 10000 的员工,并按工资降序排列。
1 | var result = _viewModel.Employees |
逐段解释:
| 方法 | 作用 |
|---|---|
Where |
过滤 |
OrderByDescending |
按工资降序 |
ToList |
立即执行并生成列表 |
如果没有 ToList,得到的是延迟执行的查询。把它直接赋给 ItemsSource 时,行为可能和预期不同,排查也更困难。初学阶段建议先物化结果。
6.4 绑定查询结果
1 | LinqDataGrid.ItemsSource = result; |
这里的 result 是 List<Employee>。
DataGrid 会为每个 Employee 创建一行,列绑定读取对象属性:
1 | <DataGridTextColumn Header="姓名" |
6.5 投影成匿名类型
示例使用:
1 | var result = _viewModel.Employees |
Select 生成了匿名类型对象。
匿名类型适合:
- 查询结果只用于当前界面
- 不需要修改对象
- 不需要在其他层之间传递
如果结果需要长期维护、编辑或跨层传递,建议定义明确的 DTO 或 ViewModel 类型。
6.6 匿名类型能双向绑定吗
匿名类型的属性通常是只读的。因此:
1 | <DataGrid IsReadOnly="True" |
更合适。
如果需要编辑,使用可写的实体类型,而不是匿名类型。
6.7 LINQ 结果不会自动跟随原集合变化
这一点非常重要:
1 | var result = Employees |
之后即使 Employees 新增了一个工资 12000 的员工,result 也不会自动增加这一行。
原因:
1 | ToList 已经生成了一个固定列表 |
需要重新执行查询并重新赋值:
1 | LinqDataGrid.ItemsSource = Employees |
6.8 怎样实现“实时查询”
如果希望输入条件变化后立即刷新,可以:
- 监听输入变化。
- 重新执行 LINQ 查询。
- 把新结果赋给
ItemsSource。
示例按钮:
1 | private void RunLinqQuery_Click(object sender, RoutedEventArgs e) |
6.9 LINQ 和 ObservableCollection 的关系
两者可以配合,但职责不同:
| 对象 | 作用 |
|---|---|
ObservableCollection<T> |
保存可变集合,通知增删 |
| LINQ | 从集合中查询、筛选、排序、投影 |
常见模式:
1 | 原始集合:ObservableCollection<Employee> |
6.10 LINQ 的延迟执行
下面两种写法值得区分:
1 | var query = Employees.Where(employee => employee.Salary >= 10000); |
这是延迟执行的查询。真正遍历时才执行。
1 | var list = Employees |
这是立即执行,结果已经固定为列表。
绑定前建议明确:
1 | 我希望结果固定下来,还是每次访问都重新查询? |
6.11 LINQ 查询的常见错误
| 错误 | 结果 | 修正 |
|---|---|---|
忘记 ToList |
结果延迟执行,行为不直观 | 绑定前物化 |
| 查询后不重新赋值 | 界面不显示新结果 | 重新执行并赋给 ItemsSource |
| 匿名类型尝试编辑 | 属性只读 | 使用可写实体类型 |
| 在查询里做耗时操作 | 界面卡顿 | 把耗时查询移出 UI 线程 |
| 过滤条件解析失败 | 得到默认值 | 校验输入 |
6.12 本节小结
| 问题 | 答案 |
|---|---|
| LINQ 能作为绑定来源吗 | 查询结果可以 |
| 结果怎样绑定 | 赋给 ItemsSource |
需要 ToList 吗 |
建议绑定前物化 |
| 原集合变化会自动刷新吗 | 不会 |
| 匿名类型适合编辑吗 | 不适合 |
| 怎样刷新查询 | 重新执行并重新赋值 |
小练习
- 修改 LINQ 查询,只显示研发部员工。
- 按部门分组,显示每个部门的平均工资。
- 把匿名类型换成
Employee对象,观察是否可编辑。 - 给查询增加“姓名包含关键字”的条件。
数据绑定语法速查
| 写法 | 含义 |
|---|---|
{Binding Name} |
绑定当前 DataContext 的 Name |
{Binding Path=Name} |
显式写路径 |
{Binding ElementName=Other, Path=Text} |
绑定另一个元素 |
{Binding Source={StaticResource Data}, Path=Name} |
绑定资源对象 |
{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth} |
绑定目标自身 |
{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title} |
绑定祖先窗口 |
{Binding Employees.Count} |
路径访问集合属性 |
{Binding SelectedEmployee.Name} |
路径访问对象属性 |
绑定模式速查
| 模式 | 方向 | 典型场景 |
|---|---|---|
OneWay |
来源到目标 | 只读文本 |
TwoWay |
双向 | 文本框、复选框 |
OneTime |
只读一次 | 静态配置 |
OneWayToSource |
目标到来源 | 特殊场景 |
Default |
目标默认 | 通常省略 |
更新时机速查
| 值 | 什么时候写回来源 | 适合 |
|---|---|---|
PropertyChanged |
目标属性每次变化 | 实时预览 |
LostFocus |
目标失去焦点 | 普通表单 |
Explicit |
手动调用 UpdateSource |
手动提交 |
Default |
目标属性默认规则 | 一般不用写 |
数据来源对照
| 来源类型 | 典型对象 | 通知方式 | 适合 |
|---|---|---|---|
| 依赖对象 | DependencyObject |
依赖属性系统 | 控件、视觉对象 |
| 普通数据 | string、int、decimal |
INotifyPropertyChanged |
ViewModel 属性 |
| 对象 | 实体类 | INotifyPropertyChanged |
详情、选中项 |
| 集合 | ObservableCollection<T> |
集合变化通知 | 动态列表 |
| ADO.NET | DataTable、DataView |
数据视图通知 | 表格数据 |
| LINQ | List<T>、匿名类型 |
重新赋值 | 查询结果 |
常见错误与排查
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| 文本为空 | 路径拼错 | 查看输出窗口绑定错误 |
| 修改来源界面不动 | 没有通知 | 实现 INotifyPropertyChanged |
| 修改界面来源不动 | 没有双向绑定 | 设置 Mode=TwoWay |
| 输入过程中不更新 | 写回时机不对 | 使用 PropertyChanged |
| 列表不刷新 | 使用普通 List |
改用 ObservableCollection |
| 选中项不同步 | 没有绑定 SelectedItem |
设置双向绑定 |
| DataGrid 没有数据 | 列名与绑定路径不一致 | 检查列名 |
| LINQ 结果不更新 | 查询结果已经物化 | 重新执行查询 |
| 绑定没有来源 | DataContext 为空 |
设置 DataContext |
| 转换失败 | 类型不匹配 | 使用转换器或格式化 |
调试绑定的三个步骤
- 先看输出窗口有没有
System.Windows.Data Error。 - 再确认
DataContext和Path是否指向同一个对象。 - 最后检查通知和更新时机。
大多数绑定问题不是 WPF 本身出错,而是来源、路径、通知三者之一没有对上。
完整示例代码
文件结构
1 | WpfDataBindingDemo/ |
WpfDataBindingDemo.csproj
1 | <Project Sdk="Microsoft.NET.Sdk"> |
Employee.cs
1 | using System.ComponentModel; |
MainViewModel.cs
1 | using System.Collections.ObjectModel; |
BindingSourceObject.cs
1 | using System.Windows; |
BrushConverter.cs
1 | using System.Globalization; |
MainWindow.xaml
1 | <Window x:Class="WpfDataBindingDemo.MainWindow" |
MainWindow.xaml.cs
1 | using System.Data; |
运行检查表
| 检查项 | 预期结果 |
|---|---|
执行 dotnet build |
显示 0 个错误 |
| 启动程序 | 出现六个页签 |
| 页签 1 拖动滑块 | 文本框数字变化 |
| 页签 1 修改文本框 | 滑块位置变化 |
| 页签 2 修改 DisplayName | 预览文本变化 |
| 页签 2 选择颜色 | 预览边框和文字颜色变化 |
| 页签 3 修改标题 | 下方标题同步变化 |
| 页签 4 选择员工 | 详情区切换 |
| 页签 4 修改姓名 | 列表显示同步更新 |
| 页签 4 点击添加员工 | 列表新增一行 |
| 页签 5 点击筛选 | 只显示研发部 |
| 页签 5 点击排序 | 工资按降序排列 |
| 页签 6 输入最低工资 | 查询结果按条件过滤 |
最后三个综合练习
练习一:做一个可编辑的员工表单
要求:
- 使用
Employee对象作为来源。 - 姓名、部门、工资都使用双向绑定。
- 修改表单时,列表中的对应项同步更新。
- 工资使用
StringFormat格式化。
这个练习可以检验“对象属性通知 + 双向绑定”。
练习二:给 DataView 增加组合筛选
要求:
- 增加部门筛选。
- 增加最低工资筛选。
- 两个条件可以同时生效。
- 点击“清除筛选”后恢复全部数据。
提示:RowFilter 可以使用类似 Department = '研发部' AND Salary >= 10000 的表达式。
练习三:做一个实时 LINQ 查询
要求:
- 文本框中输入关键字。
- 每次输入后重新执行 LINQ 查询。
- 查询结果显示在
DataGrid。 - 输入为空时显示全部员工。
这个练习可以帮助你理解“LINQ 查询结果需要重新赋值”这一行为。
结语
数据绑定看起来有很多写法,但可以先抓住固定关系:
1 | 目标属性是谁? |
六种来源可以这样记:
| 来源 | 核心通知机制 |
|---|---|
| 依赖对象 | 依赖属性系统 |
| 普通数据 | INotifyPropertyChanged |
| 对象 | 对象属性通知 |
| 集合 | ObservableCollection<T> |
| ADO.NET | DataView 的行集视图 |
| LINQ | 查询后重新赋值 |
先把最小的 ElementName 绑定跑通,再依次尝试普通属性、对象、集合、DataView 和 LINQ。每加一种来源,都先观察界面变化,再看通知和路径,理解会比单纯背语法更牢固。





