Introduction
MVVM(Model-View-ViewModel)是一种常用于构建用户界面的软件架构模式,特别适用于支持数据绑定的框架,如 WPF、Xamarin、WinUI、MAUI 等。MVVM 通过分离界面和逻辑,提高了代码的可维护性、可测试性和可重用性。
历史发展
MVVM(Model-View-ViewModel)是在微软推出 WPF 时,为了解决 UI 与逻辑强耦合问题,借鉴 MVC/MVP 等模式而诞生的架构模式。 早期桌面开发:UI 和逻辑混在一起(如 WinForms)。在 WinForms / VB6 / MFC 时代:
- 所有事件处理都写在按钮点击、窗体加载等事件里
- UI 和逻辑紧耦合,维护困难,复用性差
- 你改 UI,可能要改一堆逻辑;你改逻辑,可能要重新布控件
btnSave.Click += (s, e) => { var name = txtName.Text; SaveToDatabase(name); MessageBox.Show("保存成功"); };
MVC/MVP 出现:开始尝试分离逻辑和 UI
| 模式 | 说明 | | —————————— | ———————————— | | MVC(Model-View-Controller) | 控制器处理用户输入,更新 Model 和 View(如 ASP.NET) | | MVP(Model-View-Presenter) | View 是被动的,由 Presenter 来驱动 | 这些适合 Web,但在桌面(特别是数据双向绑定)上仍然繁琐:
- Presenter 仍然需要写很多 UI 更新代码(
textBox.Text = model.Name
)
WPF 2006年发布,催生 MVVM。MVVM 是微软为配合 WPF 的数据绑定系统而设计的架构模式
| 特点 | 原因 | | ———————- | ———— | | WPF 支持强大的数据绑定系统 | 不再需要手动操作控件 | | 控件属性可直接绑定 ViewModel 属性 | 需要一套更清晰的分层设计 | MVVM 的结构 #”.net”
| 层 | 职责 | | ————- | ————————————- | | Model | 数据结构、业务规则 | | View | 纯 UI,使用 XAML | | ViewModel | 暴露属性/命令供 View 绑定,处理 UI 逻辑,不直接操作控件 | WPF使用内建的绑定系统来完成绑定,自动调用,自动更新
MVVM的发展(跨平台 + 框架)
| 阶段 | 演进 | | ——————————- | —————————————- | | .NET MVVM 初期 | 手写 INotifyPropertyChanged、RelayCommand | | MVVM Light、Caliburn.Micro、Prism | 提供简化 ViewModel 编写的工具和导航、消息通信 | | Xamarin.Forms / MAUI | 将 MVVM 用于跨平台移动开发 | | .NET Community Toolkit MVVM | 微软官方支持的现代 MVVM 框架,轻量、现代、Source Generator |
我的总结, chatgpt认可
MVVM是一套框架设计模式。他的设计目标是,实现UI和ViewModel的解耦,而且还能实现两者的双向同步,提升开发效率和可维护性。
MVVM改进了MVC, MVP框架,去除了 Controller/Presenter对UI的直接操作,利用绑定系统和通知机制自动同步状态。他综合了多个设计模式,比如观察者模式(属性通知),命令模式(行为封装),工厂模式等设计出来的。
WPF是MVVM的实现,提供了内建的一系列机制来实现这个目标。
Binding 系统(数据绑定) ICommand(命令绑定) DependencyProperty(支持变更通知),就是写一个类,封装了某一个数据的值和操作方法,然后把这个类注册到WPF框架 DataTemplate - 模板化的view
View通过绑定机制,将属性和事件绑定到ViewModel。
绑定属性:Text=”{Binding Name}” → 自动连接 ViewModel 的属性。 绑定命令:Command=”{Binding SaveCommand}” → 自动连接 ViewModel 的命令逻辑。
View通过命令模式解耦,在WPF框架帮助下执行业务逻辑。
View 不再直接调用方法,而是绑定到实现了 ICommand 的对象(命令模式)。WPF 框架负责帮你在点击按钮时自动调用 Command.Execute()。
ViewModel通过delegate事件机制,将数据变化通知给WPF框架,由WPF绑定系统负责自动更新UI,实现与UI的解耦。
View知道ViewModel。但ViewModel不知道UI。
View 通过 DataContext = new ViewModel() 明确引用 ViewModel ViewModel 完全不知道 UI 的存在 —— 这样就能进行 单元测试、重用、抽离逻辑
基本做法:
| 组件 | 说明 |
| ————————– | ——————————- |
| EditCategoryViewModel
| 绑定字段、命令(SaveCommand)、加载逻辑 |
| EditCategoryView.xaml
| 使用 TextBox
绑定字段,按钮绑定命令 |
| EditCategoryView.xaml.cs
| 只负责注入依赖,传递参数,调用 ViewModel 的加载方法 |
MVVM 的三大组成部分
| 角色 | 作用说明 |
| ————- | —————————————————————————————————– |
| Model | 表示应用程序的数据和业务逻辑。通常是实体类、服务类、数据库访问、API 调用等。不依赖 UI,纯粹面向业务。 |
| View | 表示界面(UI),通常是 XAML 文件(如 MainWindow.xaml
)。通过数据绑定展示 ViewModel 提供的数据。不包含业务逻辑。 |
| ViewModel | 是 View 和 Model 的桥梁。负责数据转换、命令实现、通知 View 更新。ViewModel 不依赖 View,也不知道具体哪个View,他提供供 View 绑定的属性和命令。 |
| | |
MVVM 的优点
✅ 清晰分离 UI 和逻辑,降低耦合
✅ 提高测试性(可为 ViewModel 编写单元测试)
✅ 便于多人协作(UI 和逻辑可以分工)
✅ 支持 UI 重用和主题切换
WPF 的三大核心特性决定了MVVM
1. 数据绑定(Data Binding)
- WPF View(XAML)可直接绑定 ViewModel 的属性。
- 不需要 Controller 手动操作 UI。
✅ 2. 命令系统(ICommand)
- WPF 提供
ICommand
接口,代替传统的事件处理器。 - ViewModel 可定义命令,View 绑定即可。
✅ 3. 通知机制(INotifyPropertyChanged)
- 支持 UI 随数据变化自动更新。
主要概念
数据绑定(Data Binding)
View 通过绑定 ViewModel 的属性(如 Text="{Binding UserName}"
)自动显示数据。
^wpf-databinding
所谓绑定就是把自己注册到订阅列表里
属性通知(INotifyPropertyChanged)
这是一个接口,表示“我会在属性变更时通过事件通知外部”
ViewModel 实现 INotifyPropertyChanged
接口,属性值变更时通知 UI 自动更新。
https://github.com/microsoft/referencesource/blob/main/System/compmod/system/componentmodel/INotifyPropertyChanged.cs
public interface INotifyPropertyChanged
{
/// <devdoc>
/// </devdoc>
event PropertyChangedEventHandler PropertyChanged;
}
https://github.com/microsoft/referencesource/blob/main/System/compmod/system/componentmodel/PropertyChangedEventHandler.cs
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
例如:
public class PersonViewModel : INotifyPropertyChanged
{
private string _name;
public string Name {
get => _name;
set {
if (_name != value) {
_name = value;
// 事件发生,调用内部方法进行处理
OnPropertyChanged(nameof(Name));
}
}
}
// 定义一个delegate引用,还没有赋值。由外部+或-来订阅
public event PropertyChangedEventHandler? PropertyChanged;
// 在Windows form中,control本身包括UI和代码2部分,事件由代码处理并负责手动更新UI
// 在wpf中,代码负责处理事件,并依次调用外部subscribers注册过的方法。代码不负责更新UI
protected void OnPropertyChanged(string propName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
WPF 的事件绑定 vs WinForms 的区别
|项目|WPF / MVVM|Windows Forms|
|—|—|—|
|数据绑定方式|通过 INotifyPropertyChanged
和 Binding
实现属性与 UI 的同步|通常需要手动设置控件值,如 textBox1.Text = person.Name;
|
|使用 delegate 的方式|事件用于通知绑定系统更新 UI|事件用于响应 UI 行为(如 Click)|
|事件机制|event
+ delegate
+ INotifyPropertyChanged
|event
+ delegate
直接用于事件回调|
|控件绑定|支持双向自动绑定,如 {Binding Name}
|需要自己写事件处理器来同步数据|
|用途|逻辑层 ViewModel -> View 的通知机制|交互层 View -> Logic 的事件处理机制|
绑定系统 (Binding system)
绑定系统(Binding System) 是 WPF 框架内部的“监听+同步”引擎: 它负责:
- 查找 Binding 的数据源(DataContext)
- 创建
BindingExpression
- 订阅
INotifyPropertyChanged
事件 - 当你调用
OnPropertyChanged(...)
时,自动更新 UI 控件
你在 ViewModel 里定义的这个事件:
public event PropertyChangedEventHandler? PropertyChanged;
WPF 内部的绑定系统会做一系列事情。
包括绑定属性值,自动注册一个处理函数到PropertyChanged
事件上(+=
)等等
当你调用 .Invoke()
触发事件时,它就会运行那个处理函数,告诉系统:“这个属性值变了!”
然后 WPF 框架就去更新 UI 了
绑定系统(Binding System)是 MVVM 的核心
没有“绑定系统”,就没有 MVVM。
| 原因 | 说明 |
| —————– | ——————————————————————– |
| 🔗 解耦 UI 与逻辑 | View 不再手动调用 ViewModel,只需绑定表达式 |
| 🔄 自动同步状态 | 属性值一变,UI 自动更新;UI 输入,数据自动同步 |
| 📉 替代事件、赋值、回调 | View不需要写 Button.Click += ...
后台代码不需要写 textBox.Text = vm.Name
|
| 🤖 命令绑定代替逻辑控制 | 不再在 code-behind 写事件,统一用命令触发 |
绑定系统的核心技术包括
- delegate
ICommand
(行为命令接口)反射
(访问属性值) 绑定系统通过反射找 ViewModel 中的属性,比如Name
、SaveCommand
依赖属性
(DependencyProperty,View 控件层面的核心)所有 WPF 控件的属性(如 TextBox.Text)都必须是依赖属性,才能绑定和响应变化- BindingExpression (内部对象管理源、路径、模式(OneWay/TwoWay)、更新方向等)
- 值转换器(IValueConverter) 在绑定路径中进行格式转换
- UpdateSourceTrigger 控制何时触发数据源更新(LostFocus、PropertyChanged)
| 技术 | 是否绑定系统核心? | 作用 |
| ——————– | ————- | ——————————- |
| ✅ delegate | ✅ 是(事件通知机制核心) | 通知属性值变化(INotifyPropertyChanged) |
| ✅ 反射 | ✅ 是 | 用反射定位属性路径
找到属性 getter/setter |
| ✅ DependencyProperty | ✅ 是 | 控件能被绑定的基础 |
| ✅ ICommand | ✅ 是 | 控件行为与逻辑解耦 |
| ✅ BindingExpression | ✅ 是 | 绑定表达式对象,管理绑定行为 |
完整绑定触发链(双向绑定示例)
<TextBox Text="{Binding Name, Mode=TwoWay}" />
→ UI 输入数据:
- 用户输入 TextBox
- 触发 DependencyProperty 变更 → 调用
BindingExpression.UpdateSource()
- 通过反射写入ViewModel的
Name
属性 → 同时触发ViewModel的 PropertyChanged 事件 - 若绑定了多个控件,会触发其它控件同步更新
→ ViewModel 改数据:
- 设置
Name
属性并触发PropertyChanged
- 绑定系统收到事件 → 调用
BindingExpression.UpdateTarget()
- 将新值写入 TextBox
绑定系统负责维护其中的复杂关系
具体绑定流程
开发者写: `<TextBox Text="{Binding Name}" />` ↓ Binding 对象(绑定描述) ↓ BindingExpression(绑定执行对象) ↓ 绑定系统监听 ViewModel 的 Name 属性 ↓ ViewModel 属性发生变化 ↓ 触发 PropertyChanged 事件 ↓ BindingExpression 捕捉事件并更新 UI
Binding 系统组件总览
组件 | 说明 |
---|---|
Binding | 表示绑定描述(路径、源、模式等)。配置谁绑定谁。 相当于快递单,描述 |
SetBinding() | 注册绑定关系到依赖属性。 相当于注册快递单 |
BindingExpression | 表示“某一个绑定关系”的执行体。执行绑定,双向监听变更,负责更新。 相当于快递员,具体送货 |
DataBindEngine | 内部绑定系统的总控中心(单例),负责所有绑定操作的调度。 相当于物流公司 |
PropertyPathWorker | 解析绑定路径如 "User.Address.City" |
INotifyPropertyChanged | ViewModel 触发属性变更事件,驱动更新。ViewModel 通知绑定系统,数据已更改 |
PropertyChanged | ViewModel的属性变更事件。 相当于电话通知(货到了) |
DependencyProperty | 控件属性系统,用于接收绑定值,支持绑定和回调。 相当于收件地址(控件属性) |
下面逐句分析
<TextBox Text="{Binding Name}" />
WPF 运行时等价于执行:
var binding = new Binding("Name");
textBox.SetBinding(TextBox.TextProperty, binding);
WPF 中的 Binding
是一个类:System.Windows.Data.Binding
它表示“控件的某个属性”如何与“数据源的某个属性”建立关系。
FrameworkElement.SetBinding(…) 传入控件本身、目标依赖属性、Binding
描述对象
public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
{
return BindingOperations.SetBinding(this, dp, binding);
}
BindingOperations.SetBinding(…)
public static BindingExpressionBase SetBinding(
DependencyObject target, DependencyProperty dp, BindingBase binding)
{
return binding.ProvideValueInternal(...); // 简化解释
}
这里调用了 Binding.ProvideValue(...)
,最终会创建一个 BindingExpression对象
BindingExpression
是绑定关系的执行者,负责监听源、更新目标。
BindingExpression.Attach(…) 执行以下事情:
- 确定绑定源(默认是
DataContext
) - 用
PropertyPathWorker
解析路径(比如User.Name
) - 注册到源对象的
PropertyChanged
事件上 - 将源值写入目标
DependencyProperty
注意BindingExpression
实现了一个 PropertyChanged
监听器,负责监听属性变化
public class BindingExpression : BindingExpressionBase
{
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == Path) {
UpdateTarget();
}
}
}
当你在 ViewModel 中这样写:
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name));
如果有属性变化,BindingExpression
被触发,调用 UpdateTarget()
把新值写到 UI 控件的 TextProperty
上。
UI 更新:DependencyProperty 系统 控件属性不是普通字段,而是:
public static readonly DependencyProperty TextProperty = ...
这些 DependencyProperty
提供能力:
- 能被绑定
- 能被监听
- 能在值改变时通知控件刷新
Binding 的核心类原理剖析
Binding 它只负责“描述绑定”,不负责执行。
public class Binding : BindingBase { public string Path { get; set; } // 要绑定的属性路径 public object Source { get; set; } // 显式设置的数据源(可选) public BindingMode Mode { get; set; } // OneWay、TwoWay 等 public UpdateSourceTrigger UpdateSourceTrigger { get; set; } // 更新时机 ... }
BindingExpression 是绑定系统的“执行体”,负责监听、更新、转换。
public class BindingExpression : BindingExpressionBase { private object _dataItem; // 数据源对象 private PropertyPathWorker _pathWorker; public override void UpdateTarget() { ... } public override void UpdateSource() { ... } }
DataBindEngine
是绑定系统的总控中心:
- 负责调度表达式
- 管理所有的活动绑定(弱引用方式)
- 执行绑定验证、回调、延迟更新等任务
命令绑定(ICommand)
用于绑定按钮等 UI 事件到 ViewModel 中的方法。 命令绑定是一种机制,允许你在 XAML 中把按钮等控件的操作(如点击)绑定到 ViewModel 中的“命令对象”,而不需要写事件处理器。 命令绑定把解决了UI控件和逻辑的强耦合:btnSayHello += ….。把命令剥离出来写成单独类,实现了复用,而且也使得UI 和 ViewModel能分别进行测试。
ICommand 接口定义
```csharp public interface ICommand { bool CanExecute(object? parameter); // 是否可以执行,如按钮是否启用 void Execute(object? parameter); // 执行命令 event EventHandler? CanExecuteChanged; // 可执行状态发生变化时通知(delegate) }
这就是命令模式的典型接口,三个要点:
1. **Execute**:真正执行的动作
2. **CanExecute**:判断是否允许执行(比如表单未填完整 → 按钮禁用)
3. **CanExecuteChanged**:通知系统重新评估 `CanExecute`(触发按钮启用/禁用变化)
它用了什么设计模式?
|名称|是否体现|原因|
|---|---|---|
|✅ 命令模式|✔|将“动作”封装为对象(如 SaveCommand),可绑定到 UI|
|✅ 委托(delegate)|✔|用事件 `CanExecuteChanged` 通知控件状态更新|
|✅ 观察者模式|✔|控件监听命令的 `CanExecuteChanged`,当命令状态变化就更新|
### 命令绑定(ICommand)的核心意义
| 功能 | 实现方式 |
| ----------- | ------------------------------- |
| 把操作抽象成对象 | 用 ICommand 接口 |
| 控件自动绑定并调用 | Command="{Binding SaveCommand}" |
| 控制按钮启用/禁用 | 实现 CanExecute() |
| 解耦 View 和逻辑 | 逻辑完全放在 ViewModel 中 |
| 支持 MVVM | 是 WPF 支持 MVVM 的关键机制之一 |
### 流程
+---------+ +------------------+
| Button |-----> | ICommand | <-------+
| (Invoker)| | + Execute() | |
+---------+ | + CanExecute() | |
+------------------+ |
^ |
+---------------+ |
| RelayCommand | |
+---------------+ |
^ |
+----------------+ |
| ViewModel |--------+
+----------------+
例如
> 这里,View背后绑定到了ViewModel中的SayHelloCommand属性
> View 只绑定 `Command="{Binding SaveCommand}"`
> 不需要知道 SayHelloCommand 背后执行了什么
创建Command对象
```csharp
public class RelayCommand : ICommand
{
private readonly Action _execute;
public RelayCommand(Action execute) => _execute = execute;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged;
}
ViewModel 中使用:
public ICommand SayHelloCommand { get; }
public MyViewModel()
{
SayHelloCommand = new RelayCommand(() => MessageBox.Show("Hello!"));
}
当你点击按钮时,WPF 会自动执行
SayHelloCommand.Execute()
,不需要你在Button_Click
里写代码。
ICommand命令绑定 vs Data Binding数据绑定
ICommand
命令绑定 和 WPF 的 数据绑定系统 都是通过“绑定表达式(BindingExpression)”结合在一起的。
数据绑定只用到了delegate和观察者模式,命名绑定又加了命令模式,显得复杂了些。
命令绑定(Command=”{Binding XXXCommand}”)也是使用 WPF 的数据绑定系统来连接控件与 ViewModel 的 ICommand 对象,只不过绑定目标是 ICommand 类型而不是普通属性。
WPF 框架系统在合适的时间自动调用命令绑定的 CanExecute()
和 Execute()
方法,不需要自己调用。
|类比角色|普通属性绑定|命令绑定|
|—|—|—|
|UI控件属性|TextBox.Text
|Button.Command
|
|数据源属性|string Name
|ICommand SaveCommand
|
|Binding系统|Binding("Name")
|Binding("SaveCommand")
|
|控件行为|自动更新 Text|自动调用 Execute()|
命令绑定 ≈ 数据绑定的一种特殊形式
普通数据绑定 绑的是属性
<TextBox Text="{Binding Name}" />
- 绑定目标是
DependencyProperty
:Text - 绑定源是 ViewModel 的属性:Name(string) 命令绑定 绑的是行为 ```
- 绑定目标是 `DependencyProperty`:Button.Command(类型为 `ICommand`)
- 绑定源是 ViewModel 的属性:SaveCommand(类型为 `ICommand`)
- ✅ 本质上,`Command="{Binding SaveCommand}"` 也是一个 `BindingExpression`!
### 命令绑定的执行过程
1. XAML 中写下 `Command="{Binding SaveCommand}"`
2. WPF 调用 `SetBinding(Button.CommandProperty, binding)
3. 绑定系统创建 `BindingExpression`,找到了 ViewModel 的 `SaveCommand` 属性
4. `Button.CommandProperty` ← 绑定了这个 `ICommand` 对象(比如 `RelayCommand` 实例)
5. Button 控件内部:
- 在加载和状态变化时调用 `command.CanExecute()` 决定是否启用
- 在用户点击时调用 `command.Execute()`
## 命令绑定 vs 属性通知
命令绑定实现了命令设计模式。属性通知实现了观察者模式
| 项目 | 命令模式(Command) | 观察者模式(Observer) |
| ------------ | -------------------------- | ------------------------------------------- |
| 用于控制行为 | ✅ 是 | ❌ 否 |
| 用于同步数据 | ❌ 否 | ✅ 是 |
| MVVM 中的实现 | `ICommand`, `RelayCommand` | `INotifyPropertyChanged`, `PropertyChanged` |
| 控件角色 | 调用命令 | 观察 ViewModel 数据变化 |
| ViewModel 作用 | 暴露命令供绑定 | 实现通知机制供绑定 |
2个模式用来解决不同问题
|模式|解决的问题|MVVM 中的应用|
|---|---|---|
|✅ **命令模式**(Command Pattern)|**将请求封装成对象,解耦请求发送者与执行者**解决“按钮点击时要执行什么操作”的问题|WPF 中的 `ICommand` 和 `RelayCommand`比如:`Button.Command="{Binding SaveCommand}"`控件不需要知道逻辑怎么写|
|✅ **观察者模式**(Observer Pattern)|**当一个对象状态改变时,自动通知依赖它的多个对象**解决“属性值变了,UI 如何自动更新”的问题|ViewModel 实现 `INotifyPropertyChanged`WPF 绑定系统监听这个事件比如:`OnPropertyChanged("Name")` 通知 UI 刷新绑定的 `TextBox.Text`|
换个角度
| MVVM 层 | 使用的设计模式 | 用途 |
| ---------------- | ------- | ---------------- |
| ViewModel → View | 观察者模式 | 数据变 → 通知 UI 自动更新 |
| View → ViewModel | 命令模式 | 用户操作 → 调用命令方法 |
# 支持库
| 框架/库名 | 发布年份 | 作者/主导 | 适用平台 | 特点关键词 | 当前状态 |
| ----------------------------------------- | ----- | --------------- | ----------------------- | -------------------------- | ------------------ |
| **Prism** | ~2008 | 微软 → 社区 | WPF, UWP, Xamarin, MAUI | 模块化、导航、事件聚合、DI | ✅ 活跃维护 |
| **MVVM Light** | ~2009 | Laurent Bugnion | WPF, UWP, Xamarin | 轻量、Messenger、RelayCommand | ❌ 停止维护(作者加入微软) |
| **Caliburn.Micro** | ~2010 | Rob Eisenberg | WPF, UWP | 约定优于配置、自动绑定、Action 调用 | ⚠️ 基本停滞(维护慢) |
| **ReactiveUI** | ~2010 | 社区 | 所有 .NET UI 平台 | 响应式、Rx.NET、双向绑定 | ✅ 活跃维护(偏 Rx 思维) |
| **CommunityToolkit.Mvvm**(原 MVVM Toolkit) | 2020 | 微软官方 | WPF, WinUI, MAUI, Uno | Source Generator、属性注解、命令注解 | ✅ 官方推荐 |
| **FreshMvvm** | ~2016 | Michael Ridland | Xamarin.Forms | Page 自动注入、导航集成 | ⚠️ Xamarin 退场,维护有限 |
| **Catel** | ~2012 | 社区 | WPF, Xamarin | MVVM + DI + Validation | ⚠️ 小众,维护减缓 |
| **Template10** | ~2015 | Jerry Nixon(微软) | UWP | UWP 快速开发框架 | ❌ 已弃用(微软也不再推荐) |
| **Stylet** | ~2017 | 个人/小团队 | WPF | 类似 Caliburn.Micro,精简、可测试 | ⚠️ 小众,文档少 |
Prism vs ReactiveUI vs MVVM Toolkit 对比表
| 比较维度 | **Prism** | **ReactiveUI** | **MVVM Toolkit** (CommunityToolkit.Mvvm) |
| ------------------------ | --------------------------------- | ------------------------------------------------- | -------------------------------------------- |
| 🔰 发布年份 | ~2008 | ~2010 | 2020(.NET 统一平台后) |
| 🧠 核心理念 | 企业级模块化、导航、DI、事件聚合 | 响应式编程、数据流驱动 | 极简、自动生成样板代码 |
| 🧩 是否完整框架 | ✅ 是,功能齐全 | ❌ 否,偏工具库(基于 Rx.NET) | ❌ 否,仅提供属性和命令的精简实现 |
| 💡 是否官方支持 | 最初微软,后转社区维护 | ❌ 社区主导 | ✅ 微软官方维护 |
| 🛠️ 平台支持 | ✅ WPF, UWP, Xamarin, MAUI | ✅ 全平台(WPF, WinUI, MAUI, Console) | ✅ WPF, WinUI, MAUI, Uno |
| 🪝 依赖注入(DI)支持 | ✅ 内建支持(Unity, DryIoc 等) | ❌ 需自行接入 | ❌ 手动注入 |
| 🧭 导航系统支持 | ✅ Prism Regions | ❌ 无导航支持 | ❌ 无导航系统 |
| 📣 消息通信系统 | ✅ EventAggregator | ✅ MessageBus | ✅ WeakReferenceMessenger |
| 🔗 命令支持 | `DelegateCommand` | `ReactiveCommand` | `[RelayCommand]` 注解生成 |
| 🧮 属性绑定支持 | `BindableBase.SetProperty()` | `this.RaiseAndSetIfChanged()` | `[ObservableProperty]` 注解生成 |
| ⚡ 异步命令 | ✅ 支持 Task 封装命令 | ✅ 原生支持异步命令 | ✅ 支持 async 方法自动生成命令 |
| 🧪 测试友好度 | ✅ 强(ViewModel 易 Mock) | ✅ 极强(Rx 可完全测试) | ✅ 强(自动生成命令和属性可测试) |
| 🧶 使用复杂度 | ⚠️ 中等偏高(依赖结构和导航配置) | ⚠️ 高(需熟悉 Rx 思维) | ✅ 低(非常简单直观) |
| 🧰 命令/属性语法 | 手动 `new DelegateCommand()` / 手动通知 | `ReactiveCommand.Create()` / RaiseAndSetIfChanged | 纯注解 `[RelayCommand]`, `[ObservableProperty]` |
| 🧬 是否使用 Source Generator | ❌ 否 | ❌ 否 | ✅ 是(属性/命令全自动生成) |
| ✨ ViewModel 基类 | `BindableBase` | `ReactiveObject` | `ObservableObject` |
| 📦 NuGet 包名 | `Prism.*` | `ReactiveUI.*` | `CommunityToolkit.Mvvm` |
| 📚 学习曲线 | 中等偏高(结构繁杂) | 高(响应式概念较陡峭) | 低(面向初学者友好) |
| 🎯 推荐适用场景 | 企业级、模块化应用、WPF/MAUI 大项目 | 高响应性、复杂交互、函数式编程爱好者 | 现代 .NET 项目、WPF/MAUI 普通 MVVM 项目 |
# Develop
`INotifyCollectionChanged`接口:集合的话需要实现INotifyCollectionChanged 接口 ,会一个事件叫:NotifyCollectionChangedEventHandler
这个事件作用是:是当集合改变时会发生响应,从而会提供一个`ObservableCollection<T>` 动态数据集合
属性的话是需要继承一个INotifyPropertyChanged接口,会提供一个事件叫:PropertyChangedEventHandler
这个事件作用是:在更改属性值时会发生响应
同时页面需要在绑定字段的时候设置监听属性:UpdateSourceTrigger=PropertyChanged mode=TwoWay 获取或设置一个值,该值指示绑定的数据流方向。
# FAQ
## MVVM vs MVC vs MVP
如果没有绑定系统,MVVM 退化成 MVP 或 MVC,你就得写:
textBox.Text = viewModel.Name; viewModel.Name = textBox.Text; button.Click += (_, __) => viewModel.Save();
这让 View 直接控制逻辑,完全违背 MVVM 的目标(分离 UI 和逻辑)。
|比较项|MVC 特点|WPF 需求|冲突点|
|---|---|---|---|
|**View 与 Controller 的交互方式**|View 通过事件通知 Controller,Controller 决定更新 View|WPF 支持双向数据绑定,View 会自动响应数据变化|WPF 不需要 Controller 主动更新 View|
|**UI 更新方式**|控制器调用 View 方法手动更新界面|View 绑定 ViewModel 属性,自动刷新|控制器逻辑违背 WPF 的自动更新机制|
|**耦合度**|Controller 通常知道 View 的细节|WPF 倡导松耦合,ViewModel 不知道 View|MVC 的紧耦合不适合数据绑定|
|**事件驱动 vs 绑定驱动**|MVC 以事件回调为主|WPF 以绑定驱动为主|设计理念根本不同|
假设你有个按钮点击后,更新一个 Label 的文本。
MVC
```csharp
public class Controller {
public void OnButtonClick() {
var data = model.GetData();
view.SetLabelText(data); // 控制器操作 UI
}
}
这里,Controller 必须直接操纵 View 控件,破坏了 WPF 的绑定思想。
在 WPF + MVVM 中:
// View.xaml
<Button Command="{Binding LoadCommand}" />
<TextBlock Text="{Binding Message}" />
// ViewModel.cs
public ICommand LoadCommand => new RelayCommand(() => Message = model.GetData());
public string Message { get; set; } // 通知 UI 自动更新
这里,WPF 自动完成数据更新。ViewModel 根本不需要知道 UI 是什么控件,UI 根本不需要知道谁负责处理我的事件
总之,WPF 的设计理念是绑定驱动的声明式 UI,而 MVC 是事件驱动的命令式逻辑,两者冲突明显。所以 MVVM 才是 WPF 的“原生”架构模式。
下面是一个完整例子。分别用WPF方式实现MVC和MVVM模式
用WPF模拟MVC
model.cs
public class MessageModel
{
public string GetMessage() => "你好,WPF!";
}
MainWindow.xaml (界面View)
<Window x:Class="MvcDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MVC Demo" Height="150" Width="300">
<StackPanel Margin="20">
<Button Name="btnShow" Content="显示消息" Margin="0 0 0 10" />
<TextBlock Name="lblMessage" FontSize="16" />
</StackPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private MessageController controller;
public MainWindow()
{
InitializeComponent();
controller = new MessageController(this);
}
}
Controller.cs
public class MessageController
{
private readonly MessageModel model;
private readonly MainWindow view;
public MessageController(MainWindow view)
{
this.view = view;
this.model = new MessageModel();
this.view.btnShow.Click += OnButtonClick;
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
string message = model.GetMessage();
view.lblMessage.Text = message; // 直接操作 UI
}
}
可以运行,但 Controller 强依赖 View 结构,无法单元测试、无法复用,也违背了 WPF 的绑定哲学。
用WPM实现MVP
MessageModel.cs
(保持不变)
public class MessageModel
{
public string GetMessage() => "你好,WPF!";
}
MainWindow.xaml
(与mvc一样)
<Window x:Class="MvpDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MVP Demo" Height="150" Width="300">
<StackPanel Margin="20">
<Button Name="btnShow" Content="显示消息" Margin="0 0 0 10" />
<TextBlock Name="lblMessage" FontSize="16" />
</StackPanel>
</Window>
IMessageView.cs
(View 接口)
public interface IMessageView
{
void SetMessage(string message);
event RoutedEventHandler ShowButtonClicked;
}
MainWindow.xaml.cs
(实现接口)
public partial class MainWindow : Window, IMessageView
{
private MessagePresenter presenter;
public MainWindow()
{
InitializeComponent();
presenter = new MessagePresenter(this);
}
public void SetMessage(string message)
{
lblMessage.Text = message;
}
public event RoutedEventHandler ShowButtonClicked
{
add { btnShow.Click += value; }
remove { btnShow.Click -= value; }
}
}
MessagePresenter.cs
public class MessagePresenter
{
private readonly MessageModel model = new();
private readonly IMessageView view;
public MessagePresenter(IMessageView view)
{
this.view = view;
this.view.ShowButtonClicked += OnShowClicked;
}
private void OnShowClicked(object sender, RoutedEventArgs e)
{
string message = model.GetMessage();
view.SetMessage(message); // 不直接操作控件
}
}
MVC vs MVP 对比
|比较点|MVC|MVP| |—|—|—| |控制器操作|直接操作控件|通过接口控制视图| |解耦程度|Controller ←→ View 耦合|Presenter ←→ View 解耦(靠接口)| |可测试性|较弱|强:Presenter 可单元测试| |UI 变化影响|Controller 需改动|View 接口不变可复用 Presenter|
用WPF实现MVVM
MessageModel.cs
(保持不变)
public class MessageModel
{
public string GetMessage() => "你好,WPF!";
}
ViewModel.cs
public class MessageViewModel : INotifyPropertyChanged
{
private readonly MessageModel model = new();
private string _message = string.Empty;
public string Message
{
get => _message;
set {
if (_message != value)
{
_message = value;
OnPropertyChanged(nameof(Message));
}
}
}
public ICommand ShowMessageCommand => new RelayCommand(() =>
{
Message = model.GetMessage(); // 只修改属性,不直接操作 UI
});
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string prop)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
RelayCommand.cs(命令帮助类)
public class RelayCommand : ICommand
{
private readonly Action _execute;
public RelayCommand(Action execute) => _execute = execute;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged;
}
MainWindow.xaml (View) 绑定命令和属性
<Window x:Class="MvvmDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MVVM Demo" Height="150" Width="300">
<StackPanel Margin="20">
<Button Content="显示消息" Command="{Binding ShowMessageCommand}" Margin="0 0 0 10"/>
<TextBlock Text="{Binding Message}" FontSize="16"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MessageViewModel(); // 绑定 ViewModel
}
}
✅ View 只绑定 ViewModel,不知道 Model
✅ ViewModel 不知道 UI 控件,容易测试
✅ Model 是独立逻辑层,可复用
MVC / MVP / MVVM 文件结构对比
| 文件名 | MVC | MVP | MVVM | | ——————– | —– | —- | ————- | | Model.cs | ✅ | ✅ | ✅ | | View.xaml | ✅ | ✅ | ✅(绑定) | | View.xaml.cs | 控制器绑定 | 实现接口 | ❌ 无逻辑 | | Controller/Presenter | ✅ | ✅ | ❌ ViewModel代替 | | ViewModel.cs | ❌ | ❌ | ✅ | | RelayCommand.cs | ❌ | ❌ | ✅ 用于命令绑定 | MVC / MVP / MVVM实现效果对比
|比较维度|MVC|MVP|MVVM| |—|—|—|—| |控制方式|Controller|Presenter|ViewModel| |是否依赖事件|✅ 需要|✅ 需要|❌ 不需| |是否手动操作控件|✅ 是|❌ 通过接口|❌ 全自动| |易测试性|❌ 差|✅ 强|✅ 强| |是否支持绑定|❌ 否|❌ 否|✅ 原生支持|
mvvm的设计,用到了哪些经典设计模式
MVVM(Model-View-ViewModel)模式本身并不是一个“设计模式”,而是一个架构模式,但它内部广泛应用了多个经典设计模式来实现松耦合、可测试和易扩展的架构。下面是 MVVM 中常用的设计模式:
| 设计模式 | 用途/在 MVVM 中的角色 |
| —————————— | ———————————————————————————– |
| Observer(观察者模式) | 用于通知 UI 更新。ViewModel 实现 INotifyPropertyChanged
接口,View 观察 ViewModel 属性变化,自动更新 UI。 |
| Command(命令模式) | 用于将按钮等 UI 行为转化为命令对象。ICommand
接口 + RelayCommand
是典型命令模式实现。 |
| Mediator(中介者模式) | View 和 ViewModel 通过绑定机制间接通信,不直接依赖彼此,WPF 的 Binding
系统本质上是一个中介者。 |
| Dependency Injection(依赖注入) | 将服务(如 Model 或 Repository)注入到 ViewModel 中,解耦依赖关系,便于测试和替换。 |
| Factory(工厂模式) | ViewModel、Model、服务对象的创建往往使用工厂方法(比如 ViewModelLocator 或 ServiceLocator)。 |
| Service Locator(服务定位器) | 可选的模式,用于统一管理服务实例(但滥用会导致隐藏依赖)。某些 MVVM 框架如 Prism 提供此模式支持。 |
| Template Method(模板方法模式) | 用于定义数据加载、初始化过程的通用骨架,ViewModel 中常有 BaseViewModel 定义生命周期方法。 |
| Strategy(策略模式) | 有时用于在 ViewModel 中切换不同的业务处理方式,比如根据用户权限切换显示策略。 |
| State(状态模式) | ViewModel 中的 UI 状态(比如是否加载中、是否只读)可视为状态机模型。 |
RelayCommand
是谁写的?
❌ 不是 WPF 官方提供的
✅ 是 社区约定俗成的写法,你需要自己写一份(或者引用别人写好的)
你如果不用库,就必须写一个 RelayCommand
或 DelegateCommand
自己封装它。
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;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() =>
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
整个项目只写一个 RelayCommand 类(通常放在 Common 或 Infrastructure 文件夹) 然后在任何 ViewModel 中重复使用它: MyViewMode: ```csharp public class MyViewModel { public ICommand SaveCommand { get; }
public MyViewModel()
{
SaveCommand = new RelayCommand(_ => Save());
}
private void Save()
{
// 执行保存逻辑
} } ``` 常见Command变体
|命名|来源 / 框架|
|—|—|
|RelayCommand
|MVVM Light / 通用|
|DelegateCommand
|Prism 框架|
|AsyncRelayCommand
|支持异步命令执行|
|MyCommand
|你自己随便起的名字|
总结
|问题|回答| |—|—| |WPF 官方提供 RelayCommand 吗?|❌ 没有,需要自己写| |每个 ViewModel 都要写一份?|❌ 不需要,只写一个,全项目通用| |为什么社区都叫它 RelayCommand?|因为它像“转接器”,帮你把方法转成 ICommand| |推荐用现成框架吗?|✅ 是的,例如 MVVM Toolkit 或 Prism|