4.1 MVVM 的职责与分层
学习目标
学完本节,你应能把“显示什么”“如何交互”“如何存取数据”分别放到合适的对象中,解释为什么 ViewModel 既不是窗口,也不是 Controller。先读过 属性通知与集合,再动手拆分界面。
三种职责与后端对照
Model 表达业务数据和规则,例如任务的编号、标题、完成状态,以及禁止空标题的规则。View 是窗口、页面、控件和 XAML,负责布局、颜色、焦点与动画。ViewModel 为某个界面提供可绑定的状态、命令以及数据投影,例如当前输入、任务列表、是否正在加载、错误提示。
| 后端经验 | 在 WPF 中如何使用 |
|---|---|
| DTO、领域模型 | 仍可表示 API 数据和业务规则 |
| 应用服务、Repository | 仍可封装用例和数据访问 |
| Controller | 通常没有一一对应的桌面对象 |
| HTTP 请求中的局部变量 | 可能变成持续存在的界面状态 |
| 请求返回错误状态码 | 转成用户能看见并处理的错误状态 |
一次 HTTP 请求结束后,Controller 实例通常不再承载用户的编辑状态;桌面页面却可能打开数小时。ViewModel 要保存未提交的输入、选择项和加载状态,也要考虑关闭页面后取消工作。不要让一个应用级 ViewModel 永久持有所有窗口、所有列表和所有历史结果。
MVVM 的价值是让业务交互能脱离真实窗口验证,而非让代码后置文件必须为空。设置初始 DataContext、处理焦点、拖放和纯视觉事件,放在 View 中很自然。直接在 ViewModel 创建 MessageBox、访问 TextBox.Text 或读取按钮实例,会把它与具体界面绑在一起;可把这些需求转成状态或抽象的界面服务。
一个可运行的最小 ViewModel
前提:已有名为 WpfBasics 的 WPF 项目,主窗口为 MainWindow。以下是完整的 MainViewModel.cs;它保存输入并提供一个派生状态,暂不引入工具包。
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBasics;
public sealed class MainViewModel : INotifyPropertyChanged
{
private string _title = "学习 WPF";
public string Title
{
get => _title;
set
{
if (_title == value) return;
_title = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsTitleValid));
}
}
public bool IsTitleValid => !string.IsNullOrWhiteSpace(Title);
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
将窗口代码后置替换为下面的完整文件,删除上一章示例的旧字段和事件处理器。旧处理器可能引用已经从 XAML 中移除的具名控件,保留它们会产生编译错误。这是 View 连接 ViewModel 的入口,不承担业务校验。
using System.Windows;
namespace WpfBasics;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
将窗口原有内容替换为下列布局,保留根元素的命名空间声明和 x:Class="WpfBasics.MainWindow"。输入会即时进入 ViewModel,派生属性的通知让界面一起更新。
<StackPanel Margin="24">
<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Margin="0,12,0,0"
Text="{Binding IsTitleValid, StringFormat='标题有效:{0}'}" />
</StackPanel>
以后加入 API 时,让 ViewModel 依赖 ITodoService,服务返回 DTO,ViewModel 再整理成界面需要的状态。服务无需引用 WPF;ViewModel 也不应拼接接口地址或解析 HTTP 状态码。具体的组装方式见 依赖注入与配置。
常见错误
只把点击事件改名为 ViewModel 方法,方法里仍直接操作窗口,不会形成职责边界。另一种错误是把 DTO 的每个字段都塞到全局共享对象里:两个窗口会互相覆盖正在编辑的内容。先确定状态属于应用、窗口还是页面,再选择生命周期。派生属性也需要通知;Title 变化并不会自动使 IsTitleValid 的绑定重新取值。
练习与验收
增加只读属性 Preview,返回去除首尾空格后的标题,并在窗口中绑定它。验收:输入内容时预览即时变化;清空或只输入空格时有效状态为 False;ViewModel 文件没有引用 System.Windows。接着写下“标题不能为空”在哪层执行:客户端为交互体验校验,服务端仍必须再次校验。
官方资料
下一节:ICommand 与命令状态。