4、MVVM模式深度实践:MVVM各层职责、数据绑定机制、命令模式、View与ViewModel的分离技巧

好,咱们今天聊聊MVVM。说实话,这个模式在企业级桌面应用里,几乎成了标配。WPF用它,UWP用它,现在很多跨平台框架也在用。但你真的用对了吗?

我见过不少团队,项目一开始就上了MVVM,结果写着写着,ViewModel里塞满了UI逻辑,View里又偷偷写了业务代码。最后代码比没分层还乱。嗯,这里要注意,MVVM不是银弹,但用好了,确实能让你的代码清爽很多。

4.1 MVVM各层职责——别让它们“串门”

先说说各层该干什么。说白了,MVVM就是把一个界面拆成三块:

  • View:只管界面长什么样。按钮在哪,颜色是什么,动画怎么动。它不关心数据从哪来,也不关心点击后要做什么。
  • ViewModel:这是大脑。它负责把Model的数据转换成View能用的样子,同时处理用户的交互逻辑。比如用户点了“保存”,ViewModel就去调用Model的保存方法。
  • Model:纯粹的数据和业务规则。它不知道有View存在,也不知道有ViewModel。它只知道自己是个“用户”对象,有姓名、年龄,以及一个“保存到数据库”的方法。

核心原则:View只依赖ViewModel,ViewModel只依赖Model。Model谁都不依赖。千万别让View直接操作Model,也别让ViewModel知道View上有哪些控件。

我在项目中遇到过一种情况:有个同事为了省事,直接在View的代码后台里写了数据校验。结果后来需求变了,校验规则要改,他得同时改View和ViewModel两处。你说这多折腾?

4.2 数据绑定机制——ViewModel和View的“桥梁”

数据绑定是MVVM的灵魂。没有它,你就得手动写一堆代码去同步界面和数据。有了它,你只需要告诉框架:“这个文本框绑定到ViewModel的Name属性”,剩下的它自己搞定。

但这里有个坑:绑定模式。WPF里默认是双向绑定,但有些场景其实只需要单向。比如一个只显示用户名的标签,你用双向绑定,不仅浪费性能,还可能引发意外更新。

绑定模式 适用场景 注意事项
OneWay 只显示数据,不修改 性能最好,适合只读控件
TwoWay 需要编辑数据 注意避免循环更新
OneTime 数据初始化后不再变化 性能最优,适合静态数据
OneWayToSource 只写不读,比如日志 用得少,但某些场景很实用

我个人习惯是:能用OneWay绝不用TwoWay。你想想看,一个列表里几百条数据,每条都双向绑定,那性能开销可不小。

4.3 命令模式——把“点击”变成“命令”

在MVVM里,按钮点击不能直接写事件处理了。你得用命令。说白了,就是把“用户点击了按钮”这个动作,抽象成一个命令对象,然后绑定到ViewModel的一个属性上。

WPF里有个ICommand接口,你需要自己实现它。不过别担心,我一般会写一个通用的RelayCommand类,省得每个命令都写一遍。

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

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

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

    public void Execute(object parameter) => _execute(parameter);

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

你看,这个类把执行逻辑和“能否执行”的逻辑分开了。比如一个“保存”按钮,当数据没变化时,CanExecute返回false,按钮自动变灰。这比你在代码后台手动设置IsEnabled优雅多了。

小技巧:我习惯把命令定义成ViewModel的属性,比如SaveCommandDeleteCommand。这样View里直接绑定,清晰明了。千万别把命令逻辑写在View里,那又回到老路上了。

4.4 View与ViewModel的分离技巧——别让它们“藕断丝连”

这是MVVM里最容易被忽视的地方。很多人觉得,只要把代码写在ViewModel里,就算分离了。其实不然。

我曾经见过一个项目,ViewModel里居然引用了System.Windows.Controls命名空间。为什么?因为他在ViewModel里直接操作了某个控件的属性。这其实就破坏了分离原则。

要做到真正的分离,记住几点:

  • ViewModel不要引用任何UI相关的程序集。比如WPF的System.Windows、WinForms的System.Windows.Forms。ViewModel应该可以在纯控制台项目里编译通过。
  • View里不要写业务逻辑。比如“用户点击后,先校验再保存”,这个流程应该在ViewModel的命令里完成。View只负责调用命令。
  • 用数据模板来展示复杂数据。比如一个列表里每项都有不同的样式,别在ViewModel里拼字符串,用DataTemplate搞定。

避坑指南:我曾经犯过一个错——在ViewModel里暴露了一个Visibility属性,类型是System.Windows.Visibility。结果这个ViewModel只能在WPF里用,换到其他平台就废了。正确的做法是暴露一个bool属性,然后在View里用BooleanToVisibilityConverter转换。

说白了,ViewModel应该是一个纯粹的C#类,不依赖任何UI框架。这样你以后想换UI框架,或者做单元测试,都会轻松很多。

4.5 实践中的一些建议

最后,分享几个我在实际项目里总结的经验:

  • ViewModel不要太大。如果一个ViewModel里有超过20个属性或10个命令,考虑拆分成多个小的ViewModel。比如一个“用户编辑”页面,可以拆成“基本信息ViewModel”和“权限设置ViewModel”。
  • 用依赖注入管理ViewModel。别在View里new ViewModel。用DI容器统一管理,这样ViewModel可以方便地获取服务,也方便测试。
  • 注意内存泄漏。数据绑定如果没处理好,容易造成ViewModel无法被垃圾回收。比如事件订阅后没有取消订阅。我习惯在ViewModel里实现IDisposable,在View关闭时清理绑定。

嗯,MVVM其实不难,难的是坚持原则。你想想看,如果每个团队成员都严格遵守这些约定,代码会变得多干净?维护起来会多省心?

好了,这一章就到这里。下一章咱们聊聊依赖注入,那也是个好东西。