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 项目打开 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 包管理器 → 包管理器控制台”输入命令textInstall-Package CommunityToolkit.Mvvm✅ 检查安装是否成功在项目依赖项中应该能看到CommunityToolkit.Mvvm。这个包会自动引入所有必要的依赖包括源代码生成器会帮我们在编译时自动生成样板代码。三、创建第一个示例计数器应用我们先从最简单的“计数器”开始——这是一个经典的入门例子包含一个显示数字的文本、三个按钮增加、减少、重置。3.1 项目目录结构首先为了让代码更规范我们要整理一下项目结构。在解决方案资源管理器中给你的项目添加三个文件夹textMvvmTutorial/ ├── Models/ # 数据模型 ├── ViewModels/ # 视图模型我们的核心工作区域 └── Views/ # 视图界面文件然后把现有的MainWindow.xaml拖进Views文件夹。3.2 创建 ViewModel在ViewModels文件夹中新建一个类MainViewModel.cs代码如下csharpusing 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:ClassMvvmTutorial.Views.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml TitleMvvmTutorial - 计数器示例 Height300 Width400 WindowStartupLocationCenterScreen Grid StackPanel HorizontalAlignmentCenter VerticalAlignmentCenter Width250 !-- 显示当前计数的文本绑定到 ViewModel 中的 Count 属性 -- TextBlock Text{Binding Count} FontSize48 HorizontalAlignmentCenter Margin0,0,0,20/ !-- 三个按钮的 Command 属性分别绑定到自动生成的命令 -- Button Content增加 () Command{Binding IncrementCommand} Height40 Margin0,5 BackgroundLightGreen/ Button Content减少 (-) Command{Binding DecrementCommand} Height40 Margin0,5 BackgroundLightCoral/ Button Content重置 Command{Binding ResetCommand} Height40 Margin0,5 BackgroundLightGray/ /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可以使用泛型版本的RelayCommandT// 带参数的命令注意方法声明中加了 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 OrientationHorizontal TextBlock Text{Binding Name} Width100/ Button Content删除 Command{Binding DataContext.DeleteItemCommand, RelativeSource{RelativeSource AncestorTypeListBox}} CommandParameter{Binding Id}/ /StackPanel /DataTemplate /ListBox.ItemTemplate /ListBox⚠️命令命名规则重要提醒带有[RelayCommand]的方法名和生成的命令属性名之间有固定的转换规则private void Submit()→ 生成SubmitCommandprivate 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.RegisterUserLoginChangedMessage( this, (recipient, message) { if (message.IsLoggedIn) { recipient.WelcomeText $欢迎回来{message.UserName}; } else { recipient.WelcomeText 请登录; } }); } }虽然WeakReferenceMessenger使用弱引用一般在 ViewModel 被销毁时会自动清理但如果你需要手动取消注册WeakReferenceMessenger.Default.UnregisterUserLoginChangedMessage(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.RegisterStatusMessage, 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 { TaskListTodoItem 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 ListTodoItem _todos new(); private int _nextId 1; public TaskListTodoItem 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 ObservableCollectionTodoItem _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 ObservableCollectionTodoItem(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:ClassTodoApp.Views.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Title待办事项管理器 Height500 Width500 WindowStartupLocationCenterScreen Grid Margin10 Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition Height*/ /Grid.RowDefinitions !-- 添加新事项的区域 -- StackPanel Grid.Row0 OrientationHorizontal Margin0,0,0,10 TextBox Text{Binding NewTodoTitle, UpdateSourceTriggerPropertyChanged} Width300 Height30 Margin0,0,10,0 VerticalContentAlignmentCenter/ Button Content添加 Command{Binding AddTodoCommand} Width80 Height30 IsEnabled{Binding CanAddTodo}/ /StackPanel !-- 待办事项列表 -- ListBox Grid.Row1 ItemsSource{Binding Todos} Margin0,0,0,0 ListBox.ItemTemplate DataTemplate Border BorderBrushLightGray BorderThickness0,0,0,1 Padding5 Grid Grid.ColumnDefinitions ColumnDefinition WidthAuto/ ColumnDefinition Width*/ ColumnDefinition WidthAuto/ /Grid.ColumnDefinitions !-- 完成状态的复选框 -- CheckBox Grid.Column0 IsChecked{Binding IsCompleted} Command{Binding DataContext.ToggleCompleteCommand, RelativeSource{RelativeSource AncestorTypeListBox}} CommandParameter{Binding} VerticalAlignmentCenter Margin0,0,10,0/ !-- 待办标题已完成时加删除线 -- TextBlock Grid.Column1 Text{Binding Title} VerticalAlignmentCenter TextDecorations{Binding IsCompleted, Converter{StaticResource StrikeThroughConverter}}/ !-- 删除按钮 -- Button Grid.Column2 Content删除 Command{Binding DataContext.DeleteTodoCommand, RelativeSource{RelativeSource AncestorTypeListBox}} CommandParameter{Binding Id} BackgroundLightCoral Width50 Height25/ /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.AddSingletonITodoService, TodoService(); // 整个应用共享一个实例 services.AddTransientMainViewModel(); // 每次需要时新建实例 // 注册视图通常作为单例 services.AddSingletonMainWindow(); Services services.BuildServiceProvider(); // 解析主窗口并显示 var mainWindow Services.GetRequiredServiceMainWindow(); mainWindow.DataContext Services.GetRequiredServiceMainViewModel(); mainWindow.Show(); } }对应的 XAML 修改App.xamlApplication x:ClassTodoApp.App xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://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 数据验证使用 ObservableValidatorCommunityToolkit.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; } // 执行登录... } }在界面上使用ValidatesOnDataErrorsTrue可以自动显示验证错误信息TextBox Text{Binding UserName, ValidatesOnDataErrorsTrue} / TextBlock Text{Binding (Validation.Errors)[0].ErrorContent, ElementNameUserNameTextBox}/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的核心知识。如果学习中遇到问题欢迎随时提问