3.1 DataContext 与数据绑定
学习目标
本节把“读取输入框文本”改成“编辑普通 C# 对象”。你将认识绑定源、路径和目标,理解 DataContext 在对象树中的继承规则,并能根据需求选择绑定方向与源更新时机。先掌握这些概念,再进入属性通知和 MVVM。
一条绑定连接了什么
绑定至少涉及三件事:源对象提供数据,路径指出源中的属性,目标是控件上的依赖属性。例如 Text="{Binding Title}" 将源对象的 Title 属性连接到 TextBox.Text。它不是运行 C# 代码的模板插值,而是创建一个持续工作的绑定表达式。
没有显式指定源时,绑定通常使用控件的 DataContext。窗口设置了上下文,内部布局容器和控件一般会继承它,因此不必为每一个输入框重复赋值。某个子容器重新设置上下文后,它的后代会使用新的对象。DataContext 属性由 WPF 属性系统提供,但它承载的值可以是普通对象,不要求继承某个 WPF 基类,也不是依赖注入容器。
和 ASP.NET Core 的模型绑定相比,WPF 绑定的生命周期更长:窗口持续持有数据对象,用户每次编辑都可能改变它。它不会在每次点击时自动重新创建模型,也不会自动调用 Controller 或服务端校验。
一个没有通知机制的绑定示例
沿用基础项目,保留模板的 App 文件。新增 TaskDraft.cs,并替换两个 MainWindow 文件。三份代码都是完整文件,故意使用普通自动属性,让你先观察绑定本身。
namespace WpfBasics;
public sealed class TaskDraft
{
public string Title { get; set; } = "学习 WPF 绑定";
public string Note { get; set; } = "先理解更新时机";
public string OwnerName { get; set; } = "后端开发者";
}
<Window x:Class="WpfBasics.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="编辑任务草稿" Width="600" Height="360">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="标题:离开输入框后更新源" Margin="0,0,0,8" />
<TextBox x:Name="TitleInput" Grid.Row="1"
Text="{Binding Title, Mode=TwoWay}" Margin="0,0,0,12" />
<TextBlock Grid.Row="2" Text="备注:每次文本变化都更新源"
Margin="0,0,0,8" />
<TextBox Grid.Row="3"
Text="{Binding Note, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,12" />
<TextBlock Grid.Row="4" Text="{Binding OwnerName, Mode=OneWay}"
Margin="0,0,0,12" />
<Button Grid.Row="5" Content="读取草稿对象" Padding="16,6"
HorizontalAlignment="Left" Click="OnReadDraftClick" />
</Grid>
</Window>
using System.Windows;
namespace WpfBasics;
public partial class MainWindow : Window
{
public TaskDraft Draft { get; } = new();
public MainWindow()
{
InitializeComponent();
DataContext = Draft;
}
private void OnReadDraftClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(this, $"标题:{Draft.Title}\n备注:{Draft.Note}", "源对象状态");
}
}
构造函数将 Draft 赋给窗口上下文,控件上的 Title、Note、OwnerName 都相对于这个对象解析。绑定源不是窗口本身,所以不需要写 Draft.Title。在调试器中观察 Draft,输入标题但保持输入焦点时,它仍可能保留旧值;输入备注时则立即变化。
点击“读取草稿对象”会让输入框失去焦点,因此通常能读到最新标题。这不能证明标题在每次键入时就已经更新,它正好体现了默认的失焦更新行为。
方向与时机分别控制什么
| 选项 | 控制内容 | 适用场景 |
|---|---|---|
OneWay | 源到目标 | 只读展示 |
TwoWay | 源与目标双向更新 | 可编辑表单 |
OneTime | 初始化时读取源 | 后续无需更新的内容 |
OneWayToSource | 目标到源 | 特殊的状态回传 |
默认模式由目标依赖属性的元数据决定,不能认为所有控件都是双向绑定。TextBox.Text 通常默认双向,而 TextBlock.Text 通常是单向。示例显式写模式,方便学习时确认意图。
UpdateSourceTrigger 控制何时把目标的新值写回源,与绑定方向是两个维度。TextBox.Text 默认是 LostFocus;写成 PropertyChanged 就在文本变化时写回;Explicit 则要求代码调用绑定表达式的 UpdateSource()。单向展示不涉及写回,因此源更新时机对它没有对应意义。
普通自动属性可以接收输入框写回的数据,但在程序里执行 Draft.Title = "新标题" 时,不会可靠地通知所有绑定目标刷新。需要通知机制才有完整的“状态变化推动 UI”效果;下一节会增加它。
不依赖 DataContext 的绑定
当控件之间需要连接时,可以显式指定源。例如下面是放在同一窗口内的控件片段,它直接观察已经命名的输入框,适合显示 UI 层的即时输入预览。
<TextBlock Text="{Binding ElementName=TitleInput, Path=Text}" />
ElementName 使用同一名称作用域中的元素;RelativeSource 可以寻找祖先或当前元素;Source 可以显式指定资源对象。它们与 DataContext 是不同的源选择方式,一条绑定不要同时设置多个来源。数据模板还会把每一项的数据对象设为模板内部的上下文,后续阅读集合视图时要特别留意这一点。
常见错误
- 没有文本但应用也不崩溃:绑定路径拼错或上下文类型不符时,常见结果是显示空值,并在调试输出中留下绑定错误。先确认实际
DataContext的类型。 - 绑定了私有字段或方法:入门示例使用公开属性;
{Binding GetTitle()}不会像 C# 那样调用方法。 - 输入看起来成功,业务对象还是旧值:检查焦点和
UpdateSourceTrigger,不要把目标显示的文本等同于源对象状态。 - 给子控件设置自身上下文后外部绑定失效:它改变了默认来源。自定义控件应优先通过显式来源暴露属性,详见依赖属性。
动手练习与验收
把标题改为即时写回,并增加一个绑定到 TitleInput.Text 的预览文本。然后把标题模式暂时改为 OneWay,比较输入框内容、预览和 Draft.Title 的差异。
验收标准:双向即时写回时,调试器里的源属性随键入变化;单向时,用户仍可编辑输入框,但源属性不接收这些修改;预览跟随输入框的目标值变化。最后恢复双向,并能用一句话分别解释 Mode 和 UpdateSourceTrigger。