1. 从一堆回调地狱到 MVVM:这个模式到底在治什么病
大概七八年前,我接手过一个用 WinForms 写的中型桌面项目。每个窗体的代码文件普遍两千行往上,界面逻辑和业务逻辑绞成一团,改一个需求就像从毛线团里抽线头——这大概就是很多后来学 MVVM 的人最初的起点。
那时候打开 Form1.cs,满屏都是 button_Click、textBox_TextChanged、comboBox_SelectedIndexChanged。保存按钮的点击事件里,十几个控件逐个取值、判空、拼 SQL,成功之后再反过来设置另外七八个控件的显示状态。产品经理说“保存前先判断年龄是否满 18 岁”,我在事件里加判断;过两周说“这个判断下单页面也要用”,我只能复制粘贴;再过一个月说“改成输入完自动校验、不点按钮也能提示”,我发现自己已经完全改不动了——校验逻辑散落在十个事件函数里,动一处崩三处。
这种状态有个很形象的名字:意大利面条式代码。界面逻辑和业务逻辑你中有我、我中有你,任何一根面条被拽一下,整盘都会变形。
1.1 界面代码为什么会越写越烂
很多人一开始写界面的时候并没有恶意,只是顺着最直觉的方式走:界面上有个按钮,按钮被点击,那我就写“点击后干什么”。问题在于,界面操作天然是碎片化的——一次业务操作会被拆进无数个 UI 事件里。今天验证、明天存数据、后天跳转,全都塞进这些事件回调,文件的膨胀速度远超你的预期。
而且 UI 事件是特别容易“顺手牵羊”的地方。文本框值变了,顺手更新一下另一个标签;勾选框勾了,顺手禁用一下某个按钮。这些“顺手”在一开始毫无问题,但一旦页面有几十个控件,事件之间的依赖就会变成一张没人敢动的网。
我后来总结过,界面代码烂掉的三个信号:大量空行夹杂的复制粘贴代码、方法名里带数字后缀(SaveData1、SaveData2)、以及“改一个需求需要同时改七八个事件方法”。
1.2 MVVM 怎么切断界面与业务之间的“你中有我”
MVVM 的全称是 Model-View-ViewModel,2005 年由微软 WPF 架构师 John Gossman 提出,本质上是 MVC 模式在 UI 开发场景下的一次演化。它做的事情概括起来就一句话:把“界面长什么样”“界面需要什么数据”“业务规则是什么”三件事彻底分开,然后通过数据绑定重新把它们粘合起来。
我当时第一次认真读这个概念,是在 Josh Smith 那篇著名的 MSDN 文章《WPF Apps With The Model-View-ViewModel Design Pattern》里,后来又翻了 Raffaele Garofalo 的《Building Enterprise Applications with WPF and the MVVM Pattern》。这两篇材料放到今天看也不过时,原因在于 MVVM 解决的根本问题——UI 代码和业务代码的耦合——是所有桌面端、移动端、甚至前端项目都会遇到的。
如果你今天写界面代码时还在顺手查数据、顺手写逻辑、顺手改其他控件,那这篇文章就是为你写的。下面我会把 MVVM 的三个角色、数据绑定的底层原理、不同平台的落地方式逐一拆开,最后用一个最小可运行案例带你把链路完整跑一遍。
2. 三个角色各管各的活:Model、View、ViewModel 的职责边界
MVVM 最难的地方不在于理解“分层”,而在于理解每层到底该管什么、不该管什么。很多初学者画了三个框,写上几个类名,觉得这就是 MVVM 了,结果职责全面错位,代码反而比不用模式时更绕。
2.1 Model:纯粹的数据与业务规则,和界面一毛钱关系都没有
Model 层装的是“业务世界里的实体和规则”。比如订单系统里的 Order 类,有 OrderId、TotalAmount、Status 这些属性;用户管理模块里的 User 类,有 Name、Age、Role。Model 层还可以包含业务校验规则、计算逻辑、对数据库或网络接口的访问——这些都是纯粹的“业务事实”,不依赖任何 UI 框架。
判断一个类属不属于 Model,有个很简单的办法:把它丢到一个完全没有界面的控制台程序里,它还能不能正常工作?如果能,它就是 Model;如果它里面出现了 Button、TextBox、UIElement 这类东西,那它就是混进了 View 的基因。
我知道有人会觉得,那我直接在 Model 里加一个“显示成什么颜色”的属性不就行了吗?不行。显示相关的字段——Foreground、IsVisible、DisplayName——通通应该放到 ViewModel 里。Model 层一旦被 UI 污染,就等于告诉所有界面“你必须迁就我的显示方式”。等哪天多个页面想用不同方式展示同一个数据时,你就等着拆墙吧。
2.2 View:只管长得怎么样,不管业务怎么跑
View 就是用户看到的界面。在 WPF 里是 XAML,在 Android 里是 XML 布局或 Compose,在 Qt 里是 QML,在 WinForms 里是 Designer 生成的窗体。View 的职责只有两项:展示 ViewModel 暴露出来的数据,把用户的操作转发给 ViewModel。
“仅此而已”说起来容易做起来难。很多人写着写着就在 View 的后置代码里加了这么一段:
private void SaveButton_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(txtName.Text)) { MessageBox.Show("请输入姓名"); return; } var person = new Person { Name = txtName.Text }; _repository.Save(person); txtResult.Text = "保存成功"; }这段代码犯了三个忌:直接在 View 里做校验、直接访问数据仓库、直接操作其他控件。正确做法是 View 只声明“我要绑定这些数据”和“我要绑定哪些命令”,剩下的事全部交给 ViewModel。
至于 View 的后置代码是不是要写零行,我的经验是:不是绝对禁止,但应该收敛到纯 UI 操作上。比如处理动画、处理自定义控件的拖拽、调用一些 ViewModel 难以抽象的系统交互(文件对话框)。判断标准很简单:这段代码跟“业务规则”有关吗?有关就扔给 ViewModel;纯属于界面行为,放在 View 可以接受。
2.3 ViewModel:界面要的不是数据的本来样子,而是数据“能怎么被使用”
ViewModel 是 MVVM 里最核心、也最容易写歪的一层。它的定义可以拆成三句话:
- 它是 View 的“数据状态模型”:View 需要显示哪些字段、哪些按钮可点、列表是否为空,这些状态都由 ViewModel 暴露。
- 它是 View 的“行为模型”:View 的用户操作被抽象成命令(Command),命令的执行逻辑和可执行条件定义在 ViewModel 里。
- 它是 Model 的“适配器”:Model 里的原始数据往往不适合直接展示(比如日期格式、金额单位、状态枚举),ViewModel 负责把它们转换成 View 友好的形态。
拿一个用户详情页举例。Model 里的 User 只有 BirthDate(DateTime)和 Status(enum),但界面要显示“出生年份”、要显示“状态对应的中文标签”、要有一个“是否成年”的判断来决定某个按钮可不可点。这些计算和映射,就是 ViewModel 干的活。
ViewModel 还有一个很重要的身份:它完全不知道 View 的存在,也完全不引用任何 UI 控件类型。它只暴露属性、命令和事件(比如 INotifyPropertyChanged 的 PropertyChanged 事件),至于这些属性和命令怎么被展示,是 View 层绑定引擎的事。这一点是 MVVM 与 MVC、MVP 最大的不同——MVP 里 Presenter 直接通过接口操作 View,而 MVVM 里 ViewModel 对 View 一无所知,靠的是绑定机制这个“隐形翻译官”。
3. 数据绑定背后的三件套:INotifyPropertyChanged、命令和绑定方向
MVVM 能不能跑起来,全看数据绑定靠不靠谱。很多文章一上来就扔代码,读者照着抄能跑,但一旦绑定不更新、命令不触发,就完全不知道从哪排查。所以这一节我仔细讲讲绑定背后的三个核心机制。
3.1 PropertyChanged 事件:界面凭什么知道“数据变了”
在 WPF 里,ViewModel 的类要实现 INotifyPropertyChanged 接口。这个接口只有一个事件 PropertyChanged,语义是:“我的某个属性变化了,请感兴趣的订阅者(也就是绑定引擎)去重新读一下这个属性。”
public class UserViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _name; public string Name { get => _name; set { if (_name == value) return; _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } }为什么 ViewModel 要在属性 setter 里手动触发事件,而不是系统自动检测?因为 .NET 并没有“每次属性赋值自动通知”这种魔法(除非你用依赖属性或者某种 AOP 类库)。所以你必须养成两个习惯:setter 里先判断值是否真的变了(防止无意义刷新),再触发 PropertyChanged 并带上属性名。
Android 端对应的机制是 LiveData 或 StateFlow。LiveData 做的事情和 PropertyChanged 几乎一一对应:ViewModel 里暴露一个 LiveData ,View 订阅它,数据变化时观察者收到回调。区别在于 LiveData 还有生命周期感知能力,Activity 销毁时自动取消订阅,这在移动端是一个很实用的福利。
如果你听到有人说“数据绑定库帮我搞定一切”,别全信。绑定引擎只负责把 PropertyChanged 路由给界面,属性值的变更还是得 ViewModel 自己触发。这点不搞明白,你写一百个绑定,就有一百个“界面不刷新”的坑等着你。
3.2 ICommand:把“用户点了按钮”变成“用户想做什么”
光有数据绑定还不够——用户的点击、输入、选择这些行为总得有个去处。MVVM 的标准做法是用命令(Command)取代事件处理器。
WPF 里的命令接口是 ICommand,定义了三件事:Execute(执行)、CanExecute(能否执行)、CanExecuteChanged(执行条件变化时触发)。我日常最常用的封装是 RelayCommand,本质就是把 Execute 和 CanExecute 包成两个委托:
public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute ?? (() => true); } public bool CanExecute(object parameter) => _canExecute(); public void Execute(object parameter) => _execute(); public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } }这里有个细节值得注意:CanExecuteChanged 挂在 CommandManager.RequerySuggested 上,意味着 WPF 会在一些交互节点(鼠标点击、键盘输入后)自动重新查询 CanExecute,按钮的可用状态就能自动跟着变化。如果你手写事件订阅,很可能出现“按钮灰着,条件其实已经满足,但一直不恢复可点”的怪现象。
从这个角度看,点击一个按钮命令,本质上是告诉 ViewModel:“用户想保存。”至于校验有没有通过、数据写入成功没有、接下来状态怎么变,都是 ViewModel 自己的事。
3.3 绑定的方向:单向、双向、一次性到底怎么选
绑定不是只有一种。WPF 和 Android 数据绑定都支持多种模式,选错会直接导致界面不刷新或者数据不同步。
| 绑定模式 | 数据流向 | 典型场景 |
|---|---|---|
| OneWay 单向 | 源变化推给目标,目标改了不影响源 | 展示文本、状态标签 |
| TwoWay 双向 | 源和目标互相影响 | 输入框、勾选框、滑块 |
| OneTime 一次性 | 只在启动时绑定一次,之后不再监听 | 完全不变的静态数据 |
在 WPF 里,TextBox.Text 的默认绑定模式是 TwoWay,而 TextBlock.Text 默认是 OneWay。很多新手在这个地方翻车:明明 ViewModel 里值变了,TextBlock 就是不更新,多半是绑定模式没设对,或者忘了触发 PropertyChanged。Android 的 DataBinding 里同样有 @{}(单向)和 @={}(双向)的语法区别。
记住一个原则:凡是用户可编辑的输入类控件,用双向绑定;凡是展示类控件,用单向绑定。别图省事全部设成双向,那会引入大量无谓的通知和偶发 bug。
4. 同一个 MVVM,不同平台的口音:WPF、Android、Qt、WinForms 怎么落地
MVVM 是思想,不是包治百病的银弹。每个平台对它的支持程度天差地别,我这些年分别用过 WPF、Android、Qt,还有被调侃“老掉牙”的 WinForms,说说真实体会。
4.1 WPF:出身最正,绑定引擎就是为它量身定做
WPF 是 MVVM 的“原生老家”。XAML 天生支持绑定表达式,DependencyProperty 这个机制本身就是为绑定设计的,再加上 ICommand、附加行为、DataTemplate,整套体系严丝合缝。Josh Smith 那篇经典文章之所以能影响一代 WPF 开发者,就是因为 WPF 的绑定能力让 MVVM 从“理论”变成了“每天都在用的东西”。
在企业级项目里,WPF + MVVM 几乎成了标准答案。Raffaele Garofalo 那本《Building Enterprise Applications with WPF and the MVVM Pattern》虽然书名老旧,但里面的分层思想——Presentation 层、Domain 层、Infrastructure 层——今天依然很能打,它把 MVVM 从“三个文件怎么放”提升到了“整个解决方案怎么组织”的高度。
我自己在 WPF 项目里最常用的框架是 Prism,它解决了纯手工 MVVM 的两大痛点:导航和模块化。但如果你只是做一个中小型工具,我建议别急着上框架。先把手动版的 RelayCommand 和数据绑定玩顺,否则框架的抽象反而会成为你排查问题的障碍。
4.2 Android 的 MVVM 是“Jetpack 风味”,不是原版那套
Android 的 MVVM 路线比较曲折。早期是 Activity 里写一切的 MVC,后来流行过一阵 MVP。MVP 在 Android 上最大的问题是 Presenter 持有 View 接口引用,配置变更(比如转屏)时 Activity 销毁重建,很容易造成引用悬空或者说崩溃。
Google 后来推出的 Jetpack 组件——ViewModel、LiveData/StateFlow、DataBinding——把 MVVM 在 Android 上落了地,但玩法跟 WPF 有本质区别。Android 的 ViewModel 解决的头号问题是“配置变更下数据存活”:系统在转屏时帮你缓存 ViewModel 实例,Activity 重建后拿到的还是同一个 ViewModel,这是 WPF 的 ViewModel 没有的烦恼,因为桌面端不存在“屏幕转一下整个窗体销毁重建”这种事。
典型的 Android MVVM 分层是:Activity/Fragment 充当 View,ViewModel 暴露 LiveData 或 StateFlow,仓库层(Repository)封装数据来源,数据变化通过观察者通知 UI。注意,Android 开发者说的 MVVM 很多并不强制用 DataBinding,用 ViewBinding + LiveData 观察也完全成立,核心是不在 View 里直接查数据、不持有 Activity 引用。
4.3 Qt 的信号槽与 MVVM:QML 和 C++ 怎么搭档
Qt 这套跟 WPF、Android 长得都不一样。Qt 的经典写法是信号槽(Signal/Slot),本质上也是一种观察者模式:对象 A 发出信号,对象 B 的槽函数被调用。如果你理解 PropertyChanged 和 Submit,就能秒懂信号槽——它就是连接“变化”与“反应”的管道。
Qt 做 MVVM 有两个主流场景。纯 Widgets(QWidget)这一套,严格说没有 WPF 那种原生绑定引擎,你需要手动用 Q_PROPERTY + NOTIFY 信号来模拟属性通知,再用信号槽把 View 操作转给 ViewModel,绑定要靠手写 mapping,体验远不如 WPF。而 Qt Quick/QML 这条路就顺多了:QML 里声明式绑定是原生的,C++ 侧的类通过 Q_PROPERTY 暴露属性,变化时发出通知信号,QML 自动刷新——这就是标准的 MVVM 形态,也是现在 Qt 官方主推的方向。
所以你看,搜索词里出现“qt mvvm框架”,本质上是在问“怎么把 MVVM 这套思想套到 Qt 上”。我的建议是:如果你在写 QML 应用,直接把 MVVM 当默认写法;如果还在维护老的 QWidget 界面,别硬上 MVVM,用 MVC 或者干脆保持信号槽直连,效率更高。
4.4 WinForms:老框架硬上 MVVM 值不值
WinForms 是被问得最多、也最尴尬的一个。它没有 XAML 绑定表达式、没有依赖属性、没有 ICommand,只有最原始的 DataBindings 和事件。从机制上讲,WinForms 离 MVVM 最远。
但我还是见过有人(包括我自己)硬在 WinForms 里做 MVVM,答案通常很现实:不是项目不够老,而是“换框架”的成本够高。WinForms 里做 MVVM 有三条路线:
- 纯手工:实现 INotifyPropertyChanged,用 BindingSource + DataBindings 做属性绑定,用按钮的 Click 事件手动调用 ViewModel 方法,命令层完全省略。这套能做,但绑定能力弱,很多时候还得回到事件里写“转手代码”。
- 引入轻量框架:MVVM Light、Caliburn.Micro 早期都支持 WinForms,但作者维护力度有限,生态一般。
- 换 UI 框架重构:WPF、Avalonia,或者跨平台的 .NET MAUI。
我个人的态度是这样:如果你维护的是一个成熟 WinForms 产品,别为了“跟上潮流”强行 MVVM,优先保证现有架构不被破坏,在新页面里做“瘦界面 + 业务逻辑后置”就已经能改善很多;如果你是从零开始一个新的桌面项目,我建议直接考虑 WPF 或 Avalonia,别拿 WinForms 练 MVVM 的手感,纯属自虐。
5. 手把手搭一个最小可运行的 MVVM 案例:从数据到界面全链路跑通
讲了这么多原理,我们来看点能直接跑的东西。我用 WPF 做一个最简单的用户信息页面:输入姓名和年龄,下方显示问候语和一个“是否成年”的判定。
第一步是 Model,一个纯粹的 User 类:
public class User { public string Name { get; set; } public int Age { get; set; } }第二步是 ViewModel。它暴露三个属性:Name(双向绑定到输入框)、Age(双向绑定到输入框)、Greeting(单向绑定到文本块,根据 Name 计算)。再加一个 ClearCommand,点按钮清空输入。
public class UserViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _name; public string Name { get => _name; set { if (_name == value) return; _name = value; OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Greeting)); } } private int _age; public int Age { get => _age; set { if (_age == value) return; _age = value; OnPropertyChanged(nameof(Age)); OnPropertyChanged(nameof(IsAdult)); } } public string Greeting => string.IsNullOrEmpty(Name) ? "请输入姓名" : $"你好,{Name}"; public bool IsAdult => Age >= 18; public ICommand ClearCommand { get; } public UserViewModel() { ClearCommand = new RelayCommand(Clear); } private void Clear() { Name = string.Empty; Age = 0; } private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }注意两个细节:Name 变化时要同时通知 Greeting,因为 Greeting 依赖 Name;Age 变化时要通知 IsAdult。这种“级联通知”是 MVVM 里最容易漏掉的东西,漏了就会出现界面显示旧值的问题。
第三步是 View,纯 XAML 绑定:
<StackPanel Margin="20" Width="300"> <TextBlock Text="姓名:" /> <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="年龄:" Margin="0,10,0,0" /> <TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="{Binding Greeting}" Margin="0,20,0,0" FontSize="18" /> <TextBlock Text="{Binding IsAdult, StringFormat=成年状态:{0}}" /> <Button Content="清空" Command="{Binding ClearCommand}" Margin="0,20,0,0" /> </StackPanel>UpdateSourceTrigger=PropertyChanged 表示用户每敲一个字就立即把输入同步给 ViewModel,而不是等输入框失焦。这个属性在表单即时校验场景下几乎是必需的,但在大文本或性能敏感场景,用默认的失焦再更新更合理。
第四步,在窗口的后置代码里把 DataContext 设上:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new UserViewModel(); } }到这里,整个链路就跑通了:用户敲名字,Name 的 setter 触发 PropertyChanged,绑定引擎读取 Greeting 并更新文本块;年龄改成 22,IsAdult 变 true,下方状态文本自动变成“成年状态:True”;点清空按钮,命令执行,三块 UI 一起刷新。整个过程中,View 的后置代码里没有一行业务逻辑,不需要手动调用任何控件的赋值方法。
这就是 MVVM 最直观的价值:界面永远是“声明的、自动的”,而不是“命令式的、手动的”。
6. 实战多年后才想明白的坑:事件泄漏、臃肿的 ViewModel 和强行架构
模式学起来快,用起来慢。我把自己在真实项目里踩过的坑整理成三类,希望你绕开。
6.1 事件订阅导致的内存泄漏
MVVM 会引入一个非常隐蔽的泄漏来源:事件订阅。在 WinForms 或 MVC 时代,界面和逻辑互相持有引用,窗体关了引用链自然断掉;但在 MVVM 里,如果 ViewModel 订阅了某个全局服务的事件(比如“数据推送到达”),而这个全局服务被静态持有,那么 ViewModel 就永远不会被回收。更常见的场景是,View 订阅了 ViewModel 的事件但没有在关闭时退订。
我踩过最狠的一次:一个长驻后台的消息服务,MainViewModel 订阅了它的 MessageArrived 事件,用户关闭窗口后理论上 MainViewModel 该被 GC 回收,但因为订阅链还在,整个 ViewModel 连同它引用的几 MB 缓存数据一直在内存里躺着。肉眼可见的表现是:程序用一天,内存从 100MB 涨到 800MB。
解法有三个:
- 短命订阅及时退订,在 OnClosed 或 Dispose 里把 -= 写干净;
- 用弱事件模式(WeakEventManager)或 WeakReference 包装订阅者;
- 全局级服务尽量用“弱事件”,不要让长生命周期对象直接持有短生命周期对象的引用。
想测有没有泄漏,最简单的办法是反复打开关闭页面,观察内存是否稳定回落到基线。不回落,就是有东西“拽住”了页面。
6.2 ViewModel 膨胀成“上帝对象”的征兆与止损
第二个坑是 ViewModel 越写越肥。刚开始写得挺清爽,后来需求叠加:要显示登录用户、要显示待办列表、要显示通知、要校验表单、要控制按钮可见性……三个月后,这个 ViewModel 有两千行、二十来个属性、十来个命令,成了新的“超级类”。
这时候 MVVM 已经从帮手变成了包袱。我的止损手段是:
- 按功能切分 ViewModel。一个页面可以有多个特性 ViewModel,View 通过多个 DataContext 或组合方式把它们暴露出来。
- 把可复用的逻辑下沉为服务类(比如 AuthenticationService、OrderValidator),ViewModel 只做编排,不写具体实现。
- 如果多个 ViewModel 共享同一份状态,考虑引入共享的状态容器,而不是互相引用。
记住,ViewModel 的本质是“协调者”,不是“装下一切的口袋”。它把职责定义清楚之后还继续膨胀,说明职责切分出了问题。
6.3 不是所有页面都该上 MVVM:三种“别硬上”的场景
我见过团队为了统一架构,把登录页、导出配置页这种只有三五个控件的简单界面也套了完整的 Model、ViewModel、命令、数据绑定四件套。结果一个半小时能写完的页面,要写三个文件加一堆通知逻辑,还要维护命令的 CanExecute 状态。这就是过度设计。
什么时候别硬上 MVVM:
- 一次性原型、演示 demo、临时工具页面,直接用 Code-Behind 最快;
- 纯静态展示页(没有交互、没有数据变化),MVVM 带不来任何收益;
- View 与业务耦合极浅的场景(比如只是调一个 API 显示结果),用一个轻量 Presenter 或事件回调比完整 MVVM 更省事。
架构的价值在于长期维护。一个页面写完就不动了,那它用什么模式都不重要;一个页面会被十几个需求反复摩擦,那 MVVM 省下的钱才是真金白银。
7. 回到原点:MVVM 真正值钱的地方不在模式本身
写到这里,我想说点可能不太符合“技术文章预期”的话。MVVM 这三个字母,说穿了就是一份职责分工合同:数据归 Model、长相归 View、中间翻译归 ViewModel。它真正值钱的地方,不是那几个类的写法,而是逼着你在写代码前想清楚“这行逻辑到底属于哪一层”。想清楚了,即便你用的是 WinForms、即便你没有完整的绑定引擎,代码也不会坏到哪里去。
如果你现在正在准备技术面试聊 MVVM,或者要在团队里推行它,我建议你先亲手把一个无框架的 WPF/Avalonia 小例子写顺——自己写一次 RelayCommand、自己实现一次 INotifyPropertyChanged、自己踩一次“界面不刷新”的坑,比背十遍概念都有用。等你真能在某个页面里做到代码后台几乎不写业务逻辑,你就理解这套东西为什么能活这么多年。
最后分享我个人的一个小习惯:每写一个 ViewModel,提交代码前我都问自己三个问题——它的属性里有没有不该出现的 UI 类型?它调用的方法里有没有本该属于 Model 的逻辑?View 那边的绑定还有没有哪处是手动的?三问过关,这个模式才算真正用到位了。