文章目录
-
- [一、MVVM 核心组件职责](#一、MVVM 核心组件职责)
- 二、数据绑定核心机制
- [三、命令绑定:ICommand 实战](#三、命令绑定:ICommand 实战)
-
- [3.1 常见问题:如何把"事件"变成命令](#3.1 常见问题:如何把“事件”变成命令)
- 四、现代方案:CommunityToolkit.Mvvm
-
- [4.0 NuGet 与命名空间](#4.0 NuGet 与命名空间)
- [4.1 核心特性(含异步命令)](#4.1 核心特性(含异步命令))
- [4.2 消息系统(解耦 ViewModel/跨 VM 通信)](#4.2 消息系统(解耦 ViewModel/跨 VM 通信))
- [4.3 集合与验证](#4.3 集合与验证)
- 五、验证机制全景
- [六、View/ViewModel 关联与 DI](#六、View/ViewModel 关联与 DI)
-
- [6.1 DataContext 作用域与 Source](#6.1 DataContext 作用域与 Source)
- [6.2 ViewModelLocator(自动关联 View/ViewModel)](#6.2 ViewModelLocator(自动关联 View/ViewModel))
- [6.3 DI 生命周期与注册策略](#6.3 DI 生命周期与注册策略)
- 七、项目结构规范
- 八、框架选型对比
- 九、常见避坑提醒

一、MVVM 核心组件职责
| 层级 | 职责 | 不做什么 |
|---|---|---|
| View | 声明式 UI(XAML),负责"展示"和"采集",通过绑定对接 ViewModel | 不写业务逻辑;不直接操作 Model/数据库 |
| ViewModel | 暴露可绑定属性与命令;编排交互逻辑、状态;作为 View 的上下文(DataContext) | 不直接引用 View 控件;不应存在 UI 命名空间依赖 |
| Model | 数据实体、业务规则、数据访问(Repository)、领域事件 | 不关心 UI 展示与绑定;不包含 ViewModel 特定逻辑 |
也可以引入 Service 层:业务/基础设施服务接口+实现,由 ViewModel 通过 DI 注入使用,避免 ViewModel 臃肿。
csharp
// View:XAML 绑定示例
<TextBlock Text="{Binding Temperature, StringFormat={}{0:F1} °C}" />
<Button Content="刷新" Command="{Binding RefreshCommand}" />
最佳实践:ViewModel 实现单一接口(或仅使用社区工具包提供的基类),方便单元测试和跨框架复用。例如 CommunityToolkit.Mvvm 提供的
ObservableObject、ObservableRecipient等。该工具包由 Microsoft 维护,是官方推荐的 MVVM 实用类型集合。
二、数据绑定核心机制
- INotifyPropertyChanged:属性变更 → 触发 UI 更新;是"响应式 UI"的基石。
- 绑定模式选择 :
OneWay:只读展示;TwoWay:可编辑输入(如 TextBox.Text 默认为 TwoWay);OneTime:初始化一次,后续不再同步;OneWayToSource:只把 UI 的变化写回源;Default:根据目标依赖属性的默认行为来决定。
- 更新时机(UpdateSourceTrigger)
PropertyChanged:每次击键都更新源;适合搜索框/实时预览。LostFocus:控件失去焦点时更新源;TextBox.Text 的默认值,减少高频写入。Explicit:需在代码中调用BindingExpression.UpdateSource(),才提交到源(很少在 MVVM 里直接使用)。
- 值转换器(IValueConverter)
用于"类型不匹配/展示定制"的场景(如 bool↔Visibility、枚举→中文描述、颜色/图片路径转换等)。
csharp
// 示例:布尔→可见性(与参数结合可反转)
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return b ? Visibility.Visible : Visibility.Collapsed;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility v)
return v == Visibility.Visible;
return false;
}
}
xml
<!-- 在资源里声明转换器 -->
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVis"/>
</Window.Resources>
<!-- 使用 -->
<TextBlock Visibility="{Binding IsAdmin, Converter={StaticResource BoolToVis}}" Text="管理员面板"/>
小提示:转换器应尽量保持无副作用、无状态,避免在 Convert/ConvertBack 里写复杂业务,以便测试和复用。
三、命令绑定:ICommand 实战
通用 RelayCommand 实现(你给的版本可用,这里补充注释与要点):
csharp
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
// WPF 提供的全局自动重询机制,适合简单场景
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
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);
}
3.1 常见问题:如何把"事件"变成命令
- 推荐用 System.Windows.Interactivity/交互触发器 (例如
EventTrigger+InvokeCommandAction),把事件路由到 ViewModel 的命令(可第三方包,也可自己轻量实现)。示例:
xml
<!-- 示例:ListBox 双击→执行 OpenDetailCommand(伪代码,具体类名因包而异) -->
<ListBox ItemsSource="{Binding Items}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding OpenDetailCommand}"
CommandParameter="{Binding SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
注意:不要把"非命令型逻辑"硬塞进 View 的 code-behind,尽量用命令/行为(Behavior)统一到 ViewModel 中,方便测试。
四、现代方案:CommunityToolkit.Mvvm
4.0 NuGet 与命名空间
- 包名:
CommunityToolkit.Mvvm(前身为 `Microsoft.Toolkit.Mvvm)。 - 常用命名空间:
CommunityToolkit.Mvvm.ComponentModel:ObservableObject、ObservableRecipient、ObservableValidator等。CommunityToolkit.Mvvm.Input:RelayCommand、AsyncRelayCommand等。CommunityToolkit.Mvvm.Messaging:WeakReferenceMessenger、IRecipient<T>等。
- 安装后即可按需引入对应命名空间使用。
4.1 核心特性(含异步命令)
csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))]
private string _username = string.Empty;
[ObservableProperty]
private bool _isLoading;
[RelayCommand(CanExecute = nameof(CanLogin))]
private async Task LoginAsync()
{
IsLoading = true;
try
{
await AuthService.LoginAsync(Username);
}
finally
{
IsLoading = false;
}
}
private bool CanLogin() => !string.IsNullOrEmpty(Username) && !IsLoading;
}
要点:
[ObservableProperty]会在编译期生成公开属性Username,并自动实现属性变更通知。AsyncRelayCommand封装了异步方法,内部提供IsRunning、ExecutionTask、CancelCommand等能力,便于 UI 做加载/取消/异常提示。
4.2 消息系统(解耦 ViewModel/跨 VM 通信)
csharp
// 定义消息(推荐用 record/value 类型)
public record UserLoggedInMessage(string UserName);
// 发送(WeakReferenceMessenger 是默认单例)
WeakReferenceMessenger.Default.Send(new UserLoggedInMessage(Username));
// 接收
public class HeaderViewModel : ObservableRecipient, IRecipient<UserLoggedInMessage>
{
public HeaderViewModel()
{
// 激活当前对象,开始监听
WeakReferenceMessenger.Default.Register<UserLoggedInMessage>(this);
}
public void Receive(UserLoggedInMessage message)
{
// 更新当前用户信息、UI 等逻辑
}
}
小提示:
- 在不需要监听时务必调用
WeakReferenceMessenger.Default.UnregisterAll(this)(或在 ViewModel 实现清理逻辑),避免"订阅后未注销"导致内存/逻辑泄漏。- 消息应尽量"只通知状态/事件",不要包含复杂业务逻辑。复杂流程仍然通过 Service/领域事件处理。
4.3 集合与验证
csharp
// 集合:推荐 ObservableCollection<T>,对增删整体替换会自动触发 UI 刷新
[ObservableProperty]
private ObservableCollection<SensorData> _sensorList = new();
// 验证:继承 ObservableValidator,支持 DataAnnotations
public partial class UserModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "用户名必填")]
[StringLength(20, MinimumLength = 2, ErrorMessage = "长度应在2-20之间")]
private string _userName;
}
要点:
[NotifyDataErrorInfo]让工具包在属性变更时自动触发INotifyDataErrorInfo的错误刷新。- XAML 中绑定可通过
ValidatesOnNotifyDataErrors=True开启 UI 校验提示(默认已经开启)。
五、验证机制全景
WPF 支持多种验证方式,MVVM 下推荐 INotifyDataErrorInfo:支持"多错误/异步验证/按属性查询",.NET 4.5+ 可用。
- 接口对比:
IDataErrorInfo:只支持单个错误信息、同步验证。INotifyDataErrorInfo:推荐;支持多错误、异步、属性级/实体级错误。ValidationRule:完全在 View 侧,适合跨控件/纯 UI 校验(格式校验等)。
- 在 XAML 中:
- 启用:
Binding默认包含ValidatesOnNotifyDataErrors=True;若使用 DataAnnotations 或ObservableValidator,需要确保 Binding 能看到INotifyDataErrorInfo的实现。 - 提示样式:
- 默认:红色边框(ErrorTemplate 为默认模板)。
- 自定义:通过
Validation.ErrorTemplate附加属性定制提示位置/样式。
- 启用:
xml
<!-- 简单示例:在控件下方显示第一条错误 -->
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder x:Name="textBox"/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
最佳实践:
- 验证逻辑放 Model 或 ViewModel(领域层/应用层),而不是 View。
- 通用基础类:可以把
INotifyDataErrorInfo+ DataAnnotations 封装到基类(或直接继承ObservableValidator)。
六、View/ViewModel 关联与 DI
6.1 DataContext 作用域与 Source
- 绑定默认沿可视树向上寻找
DataContext,直到 Window/Page 级别;也可以显式指定:Source={StaticResource vm}、RelativeSource、ElementName等。
ItemsControl.ItemTemplate内的绑定上下文是"当前集合项",需用相对路径或指定源来访问父级 ViewModel:
xml
<!-- 示例:列表项访问父级命令 -->
<Button Command="{Binding DataContext.DeleteCommand,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}" Content="删除"/>
6.2 ViewModelLocator(自动关联 View/ViewModel)
- 概念:通过约定/注册,自动把 View 的 DataContext 设置为对应 ViewModel 的实例。例如 Prism 的
ViewModelLocator:约定 Views 放.Views、ViewModels 放.ViewModels,命名对应 XxxView ↔ XxxViewModel。 - 核心要点:
- 命名约定:
View 所在程序集相同;ViewModel以"ViewModel"结尾,并位于.ViewModels命名空间。 - DI 集成:自定义工厂函数,通过容器解析 ViewModel(含构造函数注入依赖)。
- 命名约定:
即使不使用 Prism,也可以自己实现一个简易
ViewModelLocator:在 App 中注册 View↔ViewModel 的映射,然后在Window.Resources或样式里把DataContext绑到静态实例。
6.3 DI 生命周期与注册策略
使用 Microsoft.Extensions.DependencyInjection 时,常见生命周期包括:
Singleton:全局唯一,生命周期等于容器/应用;注意线程安全与长期持有的大对象。Transient:每次注入/解析都是新实例。Scoped:每个"作用域"一个实例;在 WPF 里通常不会自动创建作用域,需要手动创建并管理生命周期。
常见注册策略(WPF):
csharp
public partial class App : Application
{
private ServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
// 工具/配置/持久化
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<IAuthService, AuthService>();
services.AddSingleton<IDialogService, DialogService>();
// ViewModel:通常注册为 Transient/Scoped,按视图实例化
services.AddTransient<MainViewModel>();
services.AddTransient<LoginViewModel>();
services.AddTransient<SettingsViewModel>();
// View:也可以注册为 Transient
services.AddTransient<MainWindow>();
services.AddTransient<LoginWindow>();
_serviceProvider = services.BuildServiceProvider();
}
protected override void OnStartup(StartupEventArgs e)
{
// 推荐通过解析 View,在构造函数里注入 ViewModel(构造注入),并设置 DataContext
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
}
csharp
// MainWindow.xaml.cs:用构造注入,保持 View "薄"
public partial class MainWindow : Window
{
public MainWindow(MainViewModel vm)
{
InitializeComponent();
DataContext = vm;
}
}
关键点:
- View 不应该知道 ViewModel 的具体类型,只依赖其接口(可读性/测试性更佳)。
- ViewModel 不应该引用 View(避免反向依赖和内存泄漏)。
- 在关闭窗口/页面时,确保及时释放 ViewModel 所持有的资源(订阅/长任务/打开的连接等)。如使用
IDisposable或提供Cleanup()方法。
七、项目结构规范
MyWpfApp/
├── Models/ # 实体类/值对象/领域模型
├── ViewModels/ # 视图模型
├── Views/ # XAML 页面/Window/UserControl
├── Services/ # 业务服务接口与实现(IAuthService, IFileService 等)
├── Converters/ # IValueConverter 实现
├── Behaviors/ # 附加行为/触发器(可选)
├── Messages/ # 消息类型定义(可合并到 ViewModels)
├── Resources/ # 样式/模板/字符串资源
└── App.xaml.cs # DI 配置、全局异常处理、主题/语言等
八、框架选型对比
| 场景 | 推荐方案 | 理由(简要) |
|---|---|---|
| 中小型项目、快速原型 | CommunityToolkit.Mvvm(配合自建 DI) | 轻量、无强约束、易与 DI/其他框架组合;官方维护并内置常用类型(ObservableObject/Command/Messenger/Validator)。 |
| 大型复合应用、模块化/导航 | Prism | 提供 Module、Region 导航、Dialog、ViewModelLocator、事件聚合器、DI 抽象等;生态成熟。 |
| 需要高度定制/学习成本敏感 | 自建轻量 MVVM 基础设施 | 手写 ObservableObject、RelayCommand、Ioc,或只引入工具包中的部分类型;更透明但需自己维护。 |
九、常见避坑提醒
- 属性通知丢失 :必须使用
[ObservableProperty]或手动实现INotifyPropertyChanged;修改字段而不是属性不会触发通知。 - DataContext 作用域 :
ItemsControl.ItemTemplate内的绑定上下文是集合项本身;需要访问父 ViewModel 时用RelativeSource。 - 集合项属性修改不刷新 :集合(
ObservableCollection<T>)只对增删/整体替换生效;元素属性修改需要元素实现INotifyPropertyChanged(可继承ObservableObject)。 - 内存泄漏 :
- 长生命周期对象(单例 ViewModel/Service)不要直接引用 View。
- Messenger/事件订阅要及时注销(
Unregister/UnregisterAll)。 - 命令持有闭包/强引用时,在 ViewModel 销毁时置空命令或字段。
- 后台线程与 UI 线程 :
- ViewModel 中改变属性(触发 UI 更新)要确保在 UI 线程(可通过
Application.Current.Dispatcher),否则会抛异常。 - 异步命令的异常要在 ViewModel 中统一处理/记录,避免"静默失败"。
- ViewModel 中改变属性(触发 UI 更新)要确保在 UI 线程(可通过
- 性能 :
- 大列表使用虚拟化(
VirtualizingStackPanel.IsVirtualizing="True")。 - 避免在属性 Getter 中做重计算;复杂计算结果应缓存为属性或可观察字段。
- 避免频繁的"全量重建集合";尽量复用对象、做增量更新。
- 大列表使用虚拟化(
- 测试友好 :
- ViewModel 不依赖具体 Service,而是通过接口注入(便于 Mock)。
- 命令/消息都可通过单元测试驱动编写;避免把 UI 特定逻辑耦合进 ViewModel。