跳到主要内容

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;它保存输入并提供一个派生状态,暂不引入工具包。

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 的入口,不承担业务校验。

MainWindow.xaml.cs(完整文件)
using System.Windows;

namespace WpfBasics;

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}

将窗口原有内容替换为下列布局,保留根元素的命名空间声明和 x:Class="WpfBasics.MainWindow"。输入会即时进入 ViewModel,派生属性的通知让界面一起更新。

MainWindow.xaml:Window 内的内容
<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 与命令状态。