一、前言:为什么要学习 CommunityToolkit.Mvvm?
如果你刚开始接触 WPF 开发,可能会遇到这样的困扰:界面逻辑和业务代码混在一起,改一个地方就要改很多文件,代码越来越难维护。MVVM 模式正是为了解决这个问题而生的,它通过分离视图(View,也就是界面)、视图模型(ViewModel)和模型(Model),让代码更清晰易维护。
而CommunityToolkit.Mvvm是微软官方维护的轻量级 MVVM 工具包(以前叫 Microsoft.Toolkit.Mvvm),是“.NET 基金会”的一部分。它和其他 MVVM 框架主要有以下区别:
| 对比项 | 传统手写 MVVM | CommunityToolkit.Mvvm | Prism(主流企业级框架) |
|---|---|---|---|
| 代码量 | 每个属性需写大量重复代码 | 一个特性[ObservableProperty]搞定 | 支持 Region、Module,配置较多 |
| 学习曲线 | 低(但代码繁琐) | 很低 | 较高 |
| 上手难度 | 容易 | 最容易 | 中等偏高 |
| 包体积 | 无依赖 | 很小,按需引用 | 较大,功能全面 |
| 最适用场景 | 学习理解 MVVM 原理 | 中小项目、快速开发、学习入门 | 大型企业级复杂应用 |
这个工具包让写 MVVM 变得非常简单——你不用再自己写那些繁琐、重复的“样板代码”了。它把 MVVM 里最常用、最繁琐的部分,比如属性通知、命令绑定、消息传递,都封装成了开箱即用的组件。
学完本文后你将能够:
✅ 搭建一个现代化的 WPF + MVVM 项目
✅ 用
[ObservableProperty]一行代码实现属性通知✅ 用
[RelayCommand]轻松处理按钮点击等操作✅ 用
Messenger实现 ViewModel 之间的解耦通信✅ 写出结构清晰、可维护的 WPF 应用程序
我们整个教程会一步一步跟着做,从零开始,确保你边学边练,真正掌握。
二、环境准备
2.1 需要安装的软件
Visual Studio 2022(社区版完全够用,免费)
.NET 6 或更高版本(推荐 .NET 6 / 8)
💡 如果在安装 Visual Studio 2022 时没有勾选“.NET 桌面开发”工作负载,可以在“工具 → 获取工具和功能”中补充安装。验证方法:在 Visual Studio 中新建项目时能找到“WPF 应用程序”模板即为环境就绪。
2.2 创建 WPF 项目
打开 Visual Studio 2022,点击“创建新项目”
搜索“WPF”,选择“WPF 应用程序”,点击“下一步”
给你的项目起个名字(比如
MvvmTutorial),选择存放位置目标框架选择“.NET 6.0”或更高版本(这里选 .NET 8.0 更好),点击“创建”
项目就创建好了!现在你会看到 Visual Studio 自动生成的MainWindow.xaml(界面文件)和MainWindow.xaml.cs(后台代码文件)。
2.3 安装 CommunityToolkit.Mvvm 包
现在需要安装 MVVM 工具包。有两种方式,选最简单的就行:
方式一:NuGet 包管理器界面(推荐新手)
在“解决方案资源管理器”中,右键点击你的项目名称(比如
MvvmTutorial)选择“管理 NuGet 程序包”
点击“浏览”标签页
搜索框输入
CommunityToolkit.Mvvm选择最新稳定版(目前是 8.2.2),点击“安装”
方式二:包管理器控制台
点击菜单栏“工具 → NuGet 包管理器 → 包管理器控制台”
输入命令:
text
Install-Package CommunityToolkit.Mvvm
✅ 检查安装是否成功:在项目依赖项中应该能看到
CommunityToolkit.Mvvm。这个包会自动引入所有必要的依赖,包括源代码生成器,会帮我们在编译时自动生成样板代码。
三、创建第一个示例:计数器应用
我们先从最简单的“计数器”开始——这是一个经典的入门例子,包含一个显示数字的文本、三个按钮(增加、减少、重置)。
3.1 项目目录结构
首先,为了让代码更规范,我们要整理一下项目结构。在解决方案资源管理器中,给你的项目添加三个文件夹:
text
MvvmTutorial/ ├── Models/ # 数据模型 ├── ViewModels/ # 视图模型(我们的核心工作区域) └── Views/ # 视图(界面文件)
然后把现有的MainWindow.xaml拖进Views文件夹。
3.2 创建 ViewModel
在ViewModels文件夹中新建一个类MainViewModel.cs,代码如下:
csharp
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace MvvmTutorial.ViewModels; // 关键点1:类必须声明为 partial(分部类),这样源代码生成器才能正常工作 // 关键点2:继承 ObservableObject 基类,它实现了属性变更通知功能 public partial class MainViewModel : ObservableObject { // 关键点3:[ObservableProperty] 特性会自动生成一个名为 Count 的公共属性 // 和对应的属性变更通知代码,我们只需要维护私有字段就够了 [ObservableProperty] private int _count = 0; // 私有字段命名为 _count,会自动生成 Count 属性 // 关键点4:[RelayCommand] 特性会自动生成一个名为 IncrementCommand 的公共命令属性 // 这个方法会在按钮被点击时执行 [RelayCommand] private void Increment() { Count++; // 这里的 Count 是自动生成的属性,赋值时会自动通知界面更新 } [RelayCommand] private void Decrement() { Count--; } [RelayCommand] private void Reset() { Count = 0; } }🔧代码解读(仔细看这里,这是最重要的部分):
ObservableObject:这是 CommunityToolkit 提供的基础类,已经帮我们实现了INotifyPropertyChanged接口,也就是说,“属性值变化时自动通知界面更新”的功能它已经帮我们做好了。
[ObservableProperty]:你只需要在私有字段上加上这个标记,编译器会自动生成一个同名的公共属性(_count→Count),并且在属性值变化时自动触发界面刷新。这就是“源生成器”技术的威力——代码在编译时自动生成,既简洁又快读。
[RelayCommand]:你只需要定义一个以private void开头的方法,加上这个标记,编译器就会自动生成一个同名的公共命令属性(Increment→IncrementCommand),这个属性可以直接绑定到 WPF 的按钮上。命名规范很重要:私有字段必须使用小写驼峰或以下划线开头(如
_count),自动生成的属性名会变成大写驼峰(Count)。建议统一使用_lowerCamel命名。
3.3 创建 View 界面
打开Views/MainWindow.xaml,把自动生成的代码全部替换成下面这个:
<Window x:Class="MvvmTutorial.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MvvmTutorial - 计数器示例" Height="300" Width="400" WindowStartupLocation="CenterScreen"> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="250"> <!-- 显示当前计数的文本,绑定到 ViewModel 中的 Count 属性 --> <TextBlock Text="{Binding Count}" FontSize="48" HorizontalAlignment="Center" Margin="0,0,0,20"/> <!-- 三个按钮的 Command 属性分别绑定到自动生成的命令 --> <Button Content="增加 (+)" Command="{Binding IncrementCommand}" Height="40" Margin="0,5" Background="LightGreen"/> <Button Content="减少 (-)" Command="{Binding DecrementCommand}" Height="40" Margin="0,5" Background="LightCoral"/> <Button Content="重置" Command="{Binding ResetCommand}" Height="40" Margin="0,5" Background="LightGray"/> </StackPanel> </Grid> </Window>🔧绑定语法解读:
{Binding Count}:告诉界面,这个控件显示的内容来自视图模型中名为Count的属性。一旦Count的值改变了,界面会自动更新显示(因为 ViewModel 继承自ObservableObject)。
Command="{Binding IncrementCommand}":告诉按钮,当被点击时,执行视图模型中名为IncrementCommand的命令。这个命令是自动生成的,对应我们写的Increment()方法。
3.4 连接 View 和 ViewModel——重要!
现在 View 和 ViewModel 都准备好了,还需要最后一步——把它们连接起来。
打开Views/MainWindow.xaml.cs(这个文件在MainWindow.xaml下面,点左边的小三角展开就能看到),修改代码如下:
using MvvmTutorial.ViewModels; namespace MvvmTutorial.Views; public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); // 设置数据上下文(DataContext)为 ViewModel 的一个实例 // 这是 MVVM 中最重要的一步——把 View 和 ViewModel 绑定在一起 DataContext = new MainViewModel(); } }3.5 运行程序!
按F5运行程序。你应该能看到:
一个显示数字
0的文本点击“增加”按钮,数字 +1;
点击“减少”按钮,数字 -1;
点击“重置”按钮,数字归零
🎉恭喜你!你刚刚完成了第一个完整的 MVVM 程序!
3.6 如果遇到问题怎么办?
| 现象 | 可能的原因 | 解决方法 |
|---|---|---|
| 界面显示空白,没有数字 | DataContext 没有设置 | 检查MainWindow.xaml.cs中是否写了DataContext = new MainViewModel(); |
| 点击按钮没有反应 | 命令方法命名不规范 | 确保方法是private void,添加了[RelayCommand],且字段命名符合_lowerCamel规范 |
| 界面不更新 Count | 属性通知不生效 | 确认字段用了[ObservableProperty]特性,并且 UI 中绑定的是生成的公共属性名(如Count,不是_count) |
| 代码编译出错 | using 引用缺失 | 确认 ViewModel 文件顶部有using CommunityToolkit.Mvvm.ComponentModel;和using CommunityToolkit.Mvvm.Input; |
| 自动生成的属性找不到 | ViewModel 不是 partial 类 | 确保类声明是public partial class MainViewModel : ObservableObject |
四、深入一:属性通知详解
理解了上面的计数器例子之后,我们来深入了解一下属性通知的机制——这是 MVVM 的核心。
4.1 传统写法 vs ToolKit 写法对比
在传统的 MVVM 中,要实现一个像Count这样的属性,开发者需要这样写:
// 传统写法——你不需要记住,了解一下就好 private int _count; public int Count { get => _count; set { if (_count != value) { _count = value; OnPropertyChanged(); // 手动触发通知 } } }每个属性都要写这么一大坨重复代码,一个 ViewModel 动辄几百行,一大半都是这样的“样板代码”。
而用CommunityToolkit.Mvvm,你只需要:
[ObservableProperty] private int _count;就这么简单!编译器会自动帮你生成上面那一大坨代码。
4.2 进阶用法:属性变化时执行额外逻辑
有时候,我们希望当一个属性变化时,同时更新其他属性或执行某些操作。[ObservableProperty]给我们提供了几个钩子(Hook)方法:
[ObservableProperty] private string _userName; // 当 UserName 将要改变时触发(参数是要设置的新值) partial void OnUserNameChanging(string? value) { // 可以在这做验证,比如不允许空字符串 } // 当 UserName 已经改变后触发(参数是新的值) partial void OnUserNameChanged(string? value) { // 可以在这里更新其他属性 GreetingMessage = $"你好,{value}!"; } // 也可以使用带 oldValue 和 newValue 的版本 partial void OnUserNameChanged(string? oldValue, string? newValue) { // 比较前后的变化 }4.3 通知其他属性更新:[NotifyPropertyChangedFor]
如果你有一个属性依赖于另一个属性的值(比如FullName取决于FirstName和LastName),可以用[NotifyPropertyChangedFor]特性:
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] // 当 FirstName 变化时,同时通知 FullName 也变化了 private string _firstName; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] // 当 LastName 变化时,也通知 FullName private string _lastName; // 这是一个只读的计算属性 public string FullName => $"{FirstName} {LastName}";这样,无论FirstName还是LastName变化,FullName绑定的 UI 元素都会自动刷新。
💡 还有
[NotifyCanExecuteChangedFor]特性,用于当属性变化时重新评估某个命令是否可用,下一节会讲到。
五、深入二:命令详解
5.1 同步命令
我们已经在计数器中看到同步命令的用法了。更完整的同步命令示例:
[RelayCommand] private void SaveData() { // 保存数据的业务逻辑 System.Diagnostics.Debug.WriteLine("数据已保存"); } // 可选:控制命令是否可用(CanExecute) [RelayCommand(CanExecute = nameof(CanSave))] private void Save() { // 保存逻辑 } private bool CanSave() { // 当返回 false 时,绑定的按钮会变成禁用状态 return !string.IsNullOrWhiteSpace(UserName); }当UserName属性变化时,为了让命令重新评估可用性,需要配合[NotifyCanExecuteChangedFor]使用:
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] // 注意:命令名 + "Command" private string _userName;5.2 异步命令(重要!)
在实际开发中,很多操作是异步的,比如从网络获取数据、访问数据库、读写文件等。CommunityToolkit.Mvvm提供了AsyncRelayCommand来处理这些情况。
using CommunityToolkit.Mvvm.Input; public partial class MainViewModel : ObservableObject { // 异步命令:方法返回 Task [RelayCommand] private async Task LoadDataAsync() { IsLoading = true; // 显示加载中 try { // 模拟一个耗时的网络操作 await Task.Delay(2000); // 模拟获取数据 Data = "数据加载完成!"; } finally { IsLoading = false; } } [ObservableProperty] private string _data; [ObservableProperty] private bool _isLoading; }异步命令有一个非常实用的特性:当异步方法正在执行时,命令自动处于“不可执行”状态(绑定的按钮会自动禁用),方法执行完毕后自动恢复。这大大简化了“防止重复点击”的逻辑。
5.3 带参数的命令
如果需要传递参数(比如从 ListView 选中的项的 ID),可以使用泛型版本的RelayCommand<T>:
// 带参数的命令(注意方法声明中加了 <int>) [RelayCommand] private void DeleteItem(int itemId) { // 执行删除操作 System.Diagnostics.Debug.WriteLine($"删除 ID 为 {itemId} 的项"); } 在 XAML 中传递参数: xml <ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="100"/> <Button Content="删除" Command="{Binding DataContext.DeleteItemCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding Id}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>⚠️命令命名规则重要提醒:带有
[RelayCommand]的方法名和生成的命令属性名之间有固定的转换规则:
private void Submit()→ 生成SubmitCommand
private async Task LoadDataAsync()→ 生成LoadDataCommand(Async 后缀会被自动去除)
private void DeleteItem(int id)→ 生成DeleteItemCommand
六、深入三:ViewModel 间的通信——Messenger 消息中心
在稍微复杂一点的应用程序中,经常会有多个 ViewModel 需要互相通信。比如:
用户登录后,导航栏要更新欢迎语,侧边栏要刷新菜单权限
在一个界面修改了数据,另一个列表界面要自动刷新
弹出一个设置窗口,关闭后主窗口要应用新设置
如果让 ViewModel 直接互相引用,代码会变得耦合严重、难以维护。Messenger(消息中心)就是解决这个问题的利器。
6.1 消息中心的基本工作原理
┌─────────────┐ 发送消息 ┌─────────────────────────────┐ │ 发送方 │ ─────────────→ │ WeakReferenceMessenger │ │ (ViewModel) │ │ (消息中心) │ └─────────────┘ └─────────────────────────────┘ │ ↓ 分发消息 ┌─────────────────────┐ │ 接收方 1 │ │ (NavViewModel) │ ├─────────────────────┤ │ 接收方 2 │ │ (SideViewModel) │ └─────────────────────┘💡 消息中心的核心优势是“弱引用”——订阅者(接收方)不会被消息系统强引用,当订阅者被垃圾回收时,消息系统会自动清理对应的订阅,从根本上避免了忘记取消注册导致的内存泄漏。
6.2 实战:用 Messenger 实现登录状态通知
第一步:定义消息类型
在项目中新建一个文件夹Messages,添加一个消息类:
// 使用 record 类型定义消息,简洁且类型安全 public record UserLoginChangedMessage(bool IsLoggedIn, string UserName);第二步:发送消息(发送方)
假设有一个LoginViewModel,用户点击登录后发送消息:
using CommunityToolkit.Mvvm.Messaging; public partial class LoginViewModel : ObservableObject { [RelayCommand] private void Login() { // 登录验证逻辑... // 发送消息通知所有订阅者:用户已登录 WeakReferenceMessenger.Default.Send(new UserLoginChangedMessage(true, "张三")); } [RelayCommand] private void Logout() { // 发送消息:用户已登出 WeakReferenceMessenger.Default.Send(new UserLoginChangedMessage(false, "")); } }第三步:接收消息(接收方)
在需要接收消息的 ViewModel(比如NavBarViewModel)中注册监听:
using CommunityToolkit.Mvvm.Messaging; public partial class NavBarViewModel : ObservableObject { [ObservableProperty] private string _welcomeText = "请登录"; public NavBarViewModel() { // 注册对 UserLoginChangedMessage 消息的监听 // 当收到消息时,下面的匿名方法会被自动调用 WeakReferenceMessenger.Default.Register<UserLoginChangedMessage>( this, (recipient, message) => { if (message.IsLoggedIn) { recipient.WelcomeText = $"欢迎回来,{message.UserName}!"; } else { recipient.WelcomeText = "请登录"; } }); } }虽然WeakReferenceMessenger使用弱引用,一般在 ViewModel 被销毁时会自动清理,但如果你需要手动取消注册:
WeakReferenceMessenger.Default.Unregister<UserLoginChangedMessage>(this);6.3 完整的实战示例
你可以在官网的示例应用中看到多个完整的 Messenger 使用案例。一个典型场景是:在窗口里输入关键词,点“加载”就从服务层拿到一组数据展示到列表中,同时用 Messenger 把状态文字更新到下方状态栏——这就体现了跨 ViewModel 通信的威力。
6.4 带 Token 的精确消息发送
当同一个消息类型被多个不同的订阅者用于不同目的时,可以用 Token 来区分:
// 为不同的目的定义不同的 Token public static class MessageTokens { public const string StatusBar = "StatusBar"; public const string Navigation = "Navigation"; } // 发送时指定 Token WeakReferenceMessenger.Default.Send(new StatusMessage("加载完成"), MessageTokens.StatusBar); // 接收时也指定相同的 Token WeakReferenceMessenger.Default.Register<StatusMessage, string>( this, MessageTokens.StatusBar, (recipient, message) => { /* 处理状态栏消息 */ });📝 消息通信的三个核心方法是:
Register(注册监听)、Send(发送消息)、Unregister(取消注册)。发送方和接收方必须使用同一个IMessenger实例(通常是WeakReferenceMessenger.Default)才能正常通信。
七、完整项目:待办事项应用
现在我们把这些知识综合起来,做一个完整的待办事项管理程序。这比计数器复杂一些,但会让你真正掌握 MVVM 的开发模式。
7.1 项目结构
TodoApp/ ├── Models/ │ └── TodoItem.cs # 数据模型 ├── ViewModels/ │ ├── MainViewModel.cs # 主视图模型 │ └── AddTodoViewModel.cs # 添加待办的视图模型(可选,演示 Messenger) ├── Views/ │ └── MainWindow.xaml # 主界面 ├── Services/ │ └── ITodoService.cs # 模拟数据服务 └── Messages/ └── TodoAddedMessage.cs # 消息定义(可选)
7.2 Model:创建数据模型
// Models/TodoItem.cs namespace TodoApp.Models; public class TodoItem { public int Id { get; set; } public string Title { get; set; } = string.Empty; public bool IsCompleted { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; }7.3 Service:创建模拟数据服务
// Services/ITodoService.cs namespace TodoApp.Services; public interface ITodoService { Task<List<TodoItem>> GetAllAsync(); Task AddAsync(TodoItem item); Task DeleteAsync(int id); Task ToggleCompleteAsync(int id); } // Services/TodoService.cs using System.Collections.ObjectModel; namespace TodoApp.Services; public class TodoService : ITodoService { private readonly List<TodoItem> _todos = new(); private int _nextId = 1; public Task<List<TodoItem>> GetAllAsync() { return Task.FromResult(_todos.ToList()); } public Task AddAsync(TodoItem item) { item.Id = _nextId++; _todos.Add(item); return Task.CompletedTask; } public Task DeleteAsync(int id) { var item = _todos.FirstOrDefault(x => x.Id == id); if (item != null) _todos.Remove(item); return Task.CompletedTask; } public Task ToggleCompleteAsync(int id) { var item = _todos.FirstOrDefault(x => x.Id == id); if (item != null) item.IsCompleted = !item.IsCompleted; return Task.CompletedTask; } }7.4 ViewModel:核心逻辑
// ViewModels/MainViewModel.cs using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using TodoApp.Models; using TodoApp.Services; namespace TodoApp.ViewModels; public partial class MainViewModel : ObservableObject { private readonly ITodoService _todoService; public MainViewModel(ITodoService todoService) { _todoService = todoService; LoadTodosAsync(); // 构造时加载数据 } // 待办事项列表 —— 用 ObservableCollection 才能在集合变化时自动刷新界面 [ObservableProperty] private ObservableCollection<TodoItem> _todos = new(); // 新增待办内容的绑定字段 [ObservableProperty] private string _newTodoTitle = string.Empty; // 按钮是否可用的判断(新内容不为空且不全是空格) private bool CanAddTodo => !string.IsNullOrWhiteSpace(NewTodoTitle); // 加载所有待办事项(异步) [RelayCommand] private async Task LoadTodosAsync() { var todos = await _todoService.GetAllAsync(); Todos = new ObservableCollection<TodoItem>(todos); } // 添加新待办事项 [RelayCommand(CanExecute = nameof(CanAddTodo))] private async Task AddTodoAsync() { var newTodo = new TodoItem { Title = NewTodoTitle }; await _todoService.AddAsync(newTodo); // 刷新列表 await LoadTodosAsync(); // 清空输入框 NewTodoTitle = string.Empty; } // 删除待办事项(带参数) [RelayCommand] private async Task DeleteTodoAsync(int todoId) { await _todoService.DeleteAsync(todoId); await LoadTodosAsync(); } // 切换完成状态 [RelayCommand] private async Task ToggleCompleteAsync(TodoItem todoItem) { await _todoService.ToggleCompleteAsync(todoItem.Id); await LoadTodosAsync(); } }7.5 View:界面代码
<!-- Views/MainWindow.xaml --> <Window x:Class="TodoApp.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="待办事项管理器" Height="500" Width="500" WindowStartupLocation="CenterScreen"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 添加新事项的区域 --> <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10"> <TextBox Text="{Binding NewTodoTitle, UpdateSourceTrigger=PropertyChanged}" Width="300" Height="30" Margin="0,0,10,0" VerticalContentAlignment="Center"/> <Button Content="添加" Command="{Binding AddTodoCommand}" Width="80" Height="30" IsEnabled="{Binding CanAddTodo}"/> </StackPanel> <!-- 待办事项列表 --> <ListBox Grid.Row="1" ItemsSource="{Binding Todos}" Margin="0,0,0,0"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderBrush="LightGray" BorderThickness="0,0,0,1" Padding="5"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <!-- 完成状态的复选框 --> <CheckBox Grid.Column="0" IsChecked="{Binding IsCompleted}" Command="{Binding DataContext.ToggleCompleteCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}" VerticalAlignment="Center" Margin="0,0,10,0"/> <!-- 待办标题(已完成时加删除线) --> <TextBlock Grid.Column="1" Text="{Binding Title}" VerticalAlignment="Center" TextDecorations="{Binding IsCompleted, Converter={StaticResource StrikeThroughConverter}}"/> <!-- 删除按钮 --> <Button Grid.Column="2" Content="删除" Command="{Binding DataContext.DeleteTodoCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding Id}" Background="LightCoral" Width="50" Height="25"/> </Grid> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>💡 上图中的
StrikeThroughConverter是一个值转换器,你可以在项目里添加它来实现“已完成文字加删除线”的效果。如果暂时不需要这个效果,可以删除TextDecorations那一行。
7.6 连接——带依赖注入的高级版本
在大型项目中,我们通常会使用依赖注入(DI)来管理对象生命周期。下面是App.xaml.cs的完整写法:
// App.xaml.cs using Microsoft.Extensions.DependencyInjection; using System.Windows; using TodoApp.Services; using TodoApp.ViewModels; using TodoApp.Views; namespace TodoApp; public partial class App : Application { public IServiceProvider Services { get; private set; } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 配置依赖注入容器 var services = new ServiceCollection(); // 注册服务 services.AddSingleton<ITodoService, TodoService>(); // 整个应用共享一个实例 services.AddTransient<MainViewModel>(); // 每次需要时新建实例 // 注册视图(通常作为单例) services.AddSingleton<MainWindow>(); Services = services.BuildServiceProvider(); // 解析主窗口并显示 var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.DataContext = Services.GetRequiredService<MainViewModel>(); mainWindow.Show(); } }对应的 XAML 修改(App.xaml):
<Application x:Class="TodoApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Application.Resources> </Application.Resources> </Application>注意:StartupUri被移除了,因为我们在代码中手动创建和显示主窗口。
八、常用技巧和最佳实践
8.1 避免重复代码:创建基类 ViewModel
如果你的多个 ViewModel 都有相同的属性和命令,可以创建一个基类:
public abstract class ViewModelBase : ObservableObject { [ObservableProperty] private bool _isBusy; [ObservableProperty] private string _statusMessage = string.Empty; } public partial class MainViewModel : ViewModelBase { // 自动继承了 IsBusy 和 StatusMessage }8.2 数据验证:使用 ObservableValidator
CommunityToolkit.Mvvm提供了ObservableValidator基类,结合System.ComponentModel.DataAnnotations命名空间,可以轻松实现属性验证:
using CommunityToolkit.Mvvm.ComponentModel; using System.ComponentModel.DataAnnotations; public partial class LoginViewModel : ObservableValidator { [ObservableProperty] [Required(ErrorMessage = "用户名不能为空")] [MinLength(3, ErrorMessage = "用户名至少需要3个字符")] private string _userName = string.Empty; [ObservableProperty] [Required(ErrorMessage = "密码不能为空")] [MinLength(6, ErrorMessage = "密码至少需要6个字符")] private string _password = string.Empty; [RelayCommand] private void ValidateAndLogin() { ValidateAllProperties(); // 触发所有属性的验证 if (HasErrors) { // 显示验证错误 return; } // 执行登录... } }在界面上使用ValidatesOnDataErrors=True可以自动显示验证错误信息:
<TextBox Text="{Binding UserName, ValidatesOnDataErrors=True}" /> <TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=UserNameTextBox}"/>8.3 常用特性速查表
| 特性 | 作用 | 常见用法 |
|---|---|---|
[ObservableProperty] | 自动生成支持通知的属性 | 用在私有字段上 |
[NotifyPropertyChangedFor] | 当前属性变化时通知其他属性更新 | 搭配[ObservableProperty]使用 |
[NotifyCanExecuteChangedFor] | 当前属性变化时重新评估命令的可用性 | 搭配[ObservableProperty]使用 |
[RelayCommand] | 自动生成同步命令 | 用在private void方法上 |
[RelayCommand(CanExecute = ...)] | 带条件控制的可执行命令 | 直接指定判断方法名 |
8.4 常见问题排查清单
| 问题 | 检查步骤 |
|---|---|
| 绑定不生效 | DataContext 是否设置正确?属性名称拼写是否一致? |
| 界面不更新 | 是否用了[ObservableProperty]?UI 中绑定的是否是自动生成的公共属性名? |
| 命令无效/按钮不响应 | 是否有[RelayCommand]特性?方法是否为private?绑定的命令名是否加上了Command后缀? |
| 异步命令按钮卡住 | 异步方法中是否正确处理了异常?(异常时命令可能保持禁用状态) |
| Messenger 收不到消息 | 发送方和接收方是否使用同一个IMessenger实例?消息类型是否完全一致? |
| 编译错:找不到自动生成的属性 | ViewModel 类必须是partial分部类,且文件顶部必须 using 正确的命名空间 |
九、总结:核心要点回顾
学完了整个教程,我们来快速回顾一下最核心的要点:
属性通知:用
ObservableObject作为 ViewModel 的基类,加上[ObservableProperty]特性,就能用一行代码搞定原本长篇的样板代码。命令处理:用
[RelayCommand]特性标记方法,编译器自动生成命令属性,直接绑定到按钮的Command属性上。异步操作:方法返回
Task,用[RelayCommand]会自动处理 Loading 状态的按钮禁用。ViewModel 通信:用
WeakReferenceMessenger.Default发送和接收消息,通过弱引用避免内存泄漏。数据验证:继承
ObservableValidator,配合[Required]等特性进行属性验证。依赖注入:用
ServiceCollection注册服务和 ViewModel,由容器统一管理依赖。
下一步可以做什么?
🎯 继续学习官方文档
🎯 查看官方示例应用(包含 WinUI、UWP、WPF 等平台的完整示例)
🎯 尝试在自己的项目中实践——从小功能开始,逐步熟悉
按照这个教程一步一步操作下来,你已经掌握了CommunityToolkit.Mvvm的核心知识。如果学习中遇到问题,欢迎随时提问!