news 2026/4/27 8:49:26

CommunityToolkit.Mvvm 从零开始完全教程——手把手带你做WPF程序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CommunityToolkit.Mvvm 从零开始完全教程——手把手带你做WPF程序

一、前言:为什么要学习 CommunityToolkit.Mvvm?

如果你刚开始接触 WPF 开发,可能会遇到这样的困扰:界面逻辑和业务代码混在一起,改一个地方就要改很多文件,代码越来越难维护。MVVM 模式正是为了解决这个问题而生的,它通过分离视图(View,也就是界面)、视图模型(ViewModel)和模型(Model),让代码更清晰易维护。

CommunityToolkit.Mvvm是微软官方维护的轻量级 MVVM 工具包(以前叫 Microsoft.Toolkit.Mvvm),是“.NET 基金会”的一部分。它和其他 MVVM 框架主要有以下区别:

对比项传统手写 MVVMCommunityToolkit.MvvmPrism(主流企业级框架)
代码量每个属性需写大量重复代码一个特性[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 项目

  1. 打开 Visual Studio 2022,点击“创建新项目”

  2. 搜索“WPF”,选择“WPF 应用程序”,点击“下一步”

  3. 给你的项目起个名字(比如MvvmTutorial),选择存放位置

  4. 目标框架选择“.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]:你只需要在私有字段上加上这个标记,编译器会自动生成一个同名的公共属性_countCount),并且在属性值变化时自动触发界面刷新。这就是“源生成器”技术的威力——代码在编译时自动生成,既简洁又快读。

  • [RelayCommand]:你只需要定义一个以private void开头的方法,加上这个标记,编译器就会自动生成一个同名的公共命令属性(IncrementIncrementCommand),这个属性可以直接绑定到 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取决于FirstNameLastName),可以用[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()→ 生成LoadDataCommandAsync 后缀会被自动去除

  • 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 正确的命名空间

九、总结:核心要点回顾

学完了整个教程,我们来快速回顾一下最核心的要点:

  1. 属性通知:用ObservableObject作为 ViewModel 的基类,加上[ObservableProperty]特性,就能用一行代码搞定原本长篇的样板代码。

  2. 命令处理:用[RelayCommand]特性标记方法,编译器自动生成命令属性,直接绑定到按钮的Command属性上。

  3. 异步操作:方法返回Task,用[RelayCommand]会自动处理 Loading 状态的按钮禁用。

  4. ViewModel 通信:用WeakReferenceMessenger.Default发送和接收消息,通过弱引用避免内存泄漏。

  5. 数据验证:继承ObservableValidator,配合[Required]等特性进行属性验证。

  6. 依赖注入:用ServiceCollection注册服务和 ViewModel,由容器统一管理依赖。

下一步可以做什么?

  • 🎯 继续学习官方文档

  • 🎯 查看官方示例应用(包含 WinUI、UWP、WPF 等平台的完整示例)

  • 🎯 尝试在自己的项目中实践——从小功能开始,逐步熟悉

按照这个教程一步一步操作下来,你已经掌握了CommunityToolkit.Mvvm的核心知识。如果学习中遇到问题,欢迎随时提问!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/27 8:47:31

Transformer上下文向量原理与可视化实践

1. 理解Transformer中的上下文向量在自然语言处理领域&#xff0c;Transformer架构彻底改变了我们处理序列数据的方式。作为其核心机制之一&#xff0c;上下文向量&#xff08;context vectors&#xff09;承载了单词在特定语境中的语义信息。与传统的词向量不同&#xff0c;上…

作者头像 李华
网站建设 2026/4/27 8:47:29

XUnity自动翻译器:打破语言壁垒,让所有Unity游戏都能说中文

XUnity自动翻译器&#xff1a;打破语言壁垒&#xff0c;让所有Unity游戏都能说中文 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为外语游戏中的生涩文本而烦恼吗&#xff1f;XUnity自动翻译器为你…

作者头像 李华
网站建设 2026/4/27 8:45:22

HPH构造:高强预应力筋和普通钢筋这样搭配,梁高直降25厘米

于土木工程范畴之内&#xff0c;HPH所指&#xff1a;“构造”&#xff0c;这向来都是被专业技术人员予以重点关注的对象。HPH这一构造&#xff0c;其全称为&#xff1a;“高预应力混杂配筋”&#xff0c;也就是&#xff08;High Prestressed Hybrid Reinforcement&#xff09;构…

作者头像 李华
网站建设 2026/4/27 8:42:55

让AI断言成功率提升20%:我的重试策略设计思路

在APP UI自动化测试场景中&#xff0c;AI断言已经成为我们验证界面元素状态的重要手段。通过多模态大模型对截图进行理解&#xff0c;我们能够判断按钮是否高亮、弹窗是否出现、图标是否正确显示。然而&#xff0c;实际落地过程中&#xff0c;AI断言的不稳定性给测试框架的可靠…

作者头像 李华
网站建设 2026/4/27 8:41:47

浅谈MapperScan

前言 在日常开发中&#xff0c;我们经常使用 MapperScan 来自动扫描 MyBatis 的 Mapper 接口&#xff0c;例如&#xff1a; SpringBootApplication MapperScan("com.demo.mapper") public class App { }它可以自动将 Mapper 接口注册为 Spring Bean&#xff0c;避免逐…

作者头像 李华
网站建设 2026/4/27 8:34:43

3分钟快速上手:baidupankey百度网盘提取码智能查询终极指南

3分钟快速上手&#xff1a;baidupankey百度网盘提取码智能查询终极指南 【免费下载链接】baidupankey 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 还在为百度网盘分享链接的提取码而烦恼吗&#xff1f;每次遇到需要密码的资源都要四处搜索&#xff0c;浪…

作者头像 李华