MVVM 解决的真正问题

代码后置(code-behind)在界面简单时很好用:按钮点击直接写逻辑,一目了然。但随着业务变复杂,事件处理器会迅速膨胀成上千行,界面元素与业务规则缠在一起,既无法单元测试,也不敢轻易改动。MVVM 的做法是把「视图状态与行为」抽到视图模型里,视图只负责把状态画出来、把操作转发出去。

MVVM 的价值不在分层好看,而在于让业务逻辑摆脱界面框架——视图模型可以像普通类一样被构造、被断言。
三个角色的职责
  • Model数据与业务规则,不依赖任何界面类型。
  • ViewModel暴露可绑定属性与命令,承载页面的状态与行为。
  • ViewXAML 声明式描述结构,只在必要时写极少量代码后置。
WPF MVVM 架构配图:三层平行悬浮的抽象圆角层级与双向箭头
三层各司其职,绑定与命令是两层之间唯一的通道。

数据绑定与通知机制

绑定把界面属性与视图模型属性连起来,源发生变化时目标自动更新。要让单向绑定真正生效,视图模型必须实现属性变更通知接口;需要双向同步的输入控件再额外支持目标到源的更新。集合则要使用可观察类型,增删元素时界面才会跟随变化。

public sealed class ArticleViewModel : INotifyPropertyChanged
{
    private string _keyword = string.Empty;

    public string Keyword
    {
        get => _keyword;
        set
        {
            if (_keyword == value) return;
            _keyword = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(Filtered));
        }
    }

    public IEnumerable<Article> Filtered =>
        All.Where(a => a.Title.Contains(Keyword, StringComparison.Ordinal));

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

这段代码的关键点是:设置属性时先比较再赋值,避免无意义的通知造成界面反复刷新;派生属性在依赖项变化时手动通知,否则筛选结果不会更新。这两处细节往往就是「界面卡」与「数据不刷新」两类问题在代码里的根源。

命令:把用户操作变成可测逻辑

命令把「点击按钮」抽象为「执行一个操作」,并附带「当前能否执行」的状态。视图绑定命令后,按钮的可用状态由命令自动驱动,不需要在业务代码里逐个去改控件的启用属性。命令的另一个价值是可测试:它只是一个方法,在单元测试里直接调用即可验证行为。

public sealed class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool>? _canExecute;

    public RelayCommand(Action execute, Func<bool>? canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
    public void Execute(object? parameter) => _execute();

    public event EventHandler? CanExecuteChanged;
    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

异步操作要额外注意:命令在执行期间应当把可执行状态置为 false,防止用户连点造成重复提交;同时要在内部捕获异常并反馈到界面,因为命令执行路径上的异常不会被自动呈现给用户。

控件模板与样式复用

WPF 的样式系统分离了「外观」与「结构」:样式设置属性,控件模板重写视觉树,数据模板决定数据如何呈现。这种分离让同一个控件可以在不同场景换一套外观而不改逻辑,也是做出与系统控件不同视觉风格的正规途径,比继承控件重写绘制要轻量得多。

资源组织建议
  • 层级分明基础色与尺寸放最底层,控件样式次之,页面样式最后覆盖。
  • 动态资源主题相关的画笔用动态资源引用,支持运行时换肤。
  • 模板精简自定义模板要保留触发器与状态视觉,否则控件会失去悬停反馈。
  • 可访问性保留键盘焦点视觉,不要为了美观把焦点框去掉。

依赖注入与模块划分

视图模型之间会互相协作:列表页需要打开详情页,主窗口需要协调多个页面的状态。把服务与视图模型注册进依赖注入容器,由容器负责创建与生命周期管理,可以避免到处写单例与静态变量——后者在测试时几乎无法替换,长期会让代码变得难以验证。

模块划分建议按业务域而不是按类型:把订单相关的模型、服务与视图模型放在同一目录,删功能时整块移除;公共能力如日志、配置、网络客户端放在基础设施目录并被业务模块依赖,反向依赖要坚决禁止,否则模块会逐渐退化成一张互相引用的网。