待办事项小程序(2) 使用框架CommunityToolkit.Mvvm重写这个程序
选择CommunityToolkit.Mvvm框架的理由有两个:
1. 它是微软管维护的mvvm轻量级框架,当前最新的.net10是支持的,未来版本也会长期支持。
2. 它会在编译时自动生成样板代码(对打了特性的命令方法)
解决方案
解决方案还是上节的方案。
可以看到RelayCommand.cs和ViewModelBase.cs已经缷载了。
需要框架进行改造的只有:
TodoItem.cs
TodoViewModel.cs

TodoItem.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows.Input;
using WpfApp1.Model;
namespace WpfApp1.ViewModel
{
// 改写后:继承 ObservableObject(框架内置,替代手搓的 ViewModelBase)
public partial class TodoViewModel : ObservableObject
{
// 改写后:使用 [ObservableProperty] 特性,编译时自动生成以下内容:
// 1. 私有字段 _newTodoText
// 2. 公共属性 NewTodoText(包含 get/set,且 set 时自动触发 PropertyChanged 通知)
// 3. 无需手动调用 OnPropertyChanged()
[ObservableProperty]
private string _newTodoText = string.Empty;
// 待办列表(核心:ObservableCollection,保持不变,框架不替代集合本身)
public ObservableCollection<TodoItem> TodoItems { get; } = new();
// 未完成待办数量(计算属性,保持逻辑不变,仅触发通知的方式简化)
public int UnfinishedCount => TodoItems.Count(t => !t.IsCompleted);
// 改写后:使用 [RelayCommand] 特性,编译时自动生成以下内容:
// 1. 公共 ICommand 属性 AddTodoCommand
// 2. 无需手动初始化 RelayCommand,框架自动绑定对应的执行方法
// 3. 支持配套的 CanAddTodo 方法(命名规范:命令方法名 + CanExecute),自动作为 CanExecute 逻辑
[RelayCommand(CanExecute = nameof(CanAddTodo))]
private void AddTodo()
{
var newTodo = new TodoItem { Content = NewTodoText.Trim(), IsCompleted = false };
TodoItems.Add(newTodo);
NewTodoText = string.Empty; // 清空输入框(赋值时,框架自动触发 PropertyChanged)
}
// AddTodo 命令的 CanExecute 逻辑(命名规范:命令方法名 + CanExecute)
private bool CanAddTodo()
{
return !string.IsNullOrWhiteSpace(NewTodoText);
}
// 改写后:泛型命令,[RelayCommand] 自动支持参数类型,无需手动写 RelayCommand<T>
[RelayCommand]
private void DeleteTodo(TodoItem? todo)
{
if (todo != null && TodoItems.Contains(todo))
{
TodoItems.Remove(todo);
}
}
// 改写后:无参数命令,框架自动生成 ClearCompletedCommand
[RelayCommand]
private void ClearCompleted()
{
var completedItems = TodoItems.Where(t => t.IsCompleted).ToList();
foreach (var item in completedItems)
{
TodoItems.Remove(item);
}
}
public TodoViewModel()
{
// 监听 TodoItems 集合变更,更新 UnfinishedCount
// 仅需手动触发计算属性的通知,框架无额外简化(集合变更本身是 ObservableCollection 的职责)
TodoItems.CollectionChanged += (s, e) => OnPropertyChanged(nameof(UnfinishedCount));
}
}
/* 下面是手搓代码--------------------------------------------------------
public class TodoViewModel : ViewModelBase // 复用之前的ViewModelBase
{
// 绑定输入框的待办内容
private string _newTodoText;
public string NewTodoText
{
get => _newTodoText;
set { _newTodoText = value; OnPropertyChanged(); }
}
// 待办列表(核心:ObservableCollection)
public ObservableCollection<TodoItem> TodoItems { get; } = new();
// 未完成待办数量(计算属性,实时更新)
public int UnfinishedCount
{
get => TodoItems.Count(t => !t.IsCompleted);
}
// 命令:添加待办
public ICommand AddTodoCommand { get; }
// 命令:删除待办
public ICommand DeleteTodoCommand { get; }
// 命令:清空已完成
public ICommand ClearCompletedCommand { get; }
public TodoViewModel()
{
// 初始化命令
AddTodoCommand = new RelayCommand(AddTodo, CanAddTodo);
DeleteTodoCommand = new RelayCommand<TodoItem>(DeleteTodo);
ClearCompletedCommand = new RelayCommand(ClearCompleted);
// 监听TodoItems集合变更,更新UnfinishedCount
TodoItems.CollectionChanged += (s, e) => OnPropertyChanged(nameof(UnfinishedCount));
// 监听每个TodoItem的IsCompleted变更,更新UnfinishedCount
// (需额外处理:新增TodoItem时订阅其PropertyChanged事件)
}
// AddTodoCommand的执行逻辑
private void AddTodo(object? parameter)
{
var newTodo = new TodoItem { Content = NewTodoText.Trim(), IsCompleted = false };
TodoItems.Add(newTodo);
NewTodoText = string.Empty; // 清空输入框
}
// AddTodoCommand的CanExecute逻辑(输入为空时禁用)
private bool CanAddTodo(object? parameter)
{
return !string.IsNullOrWhiteSpace(NewTodoText);
}
// DeleteTodoCommand的执行逻辑
private void DeleteTodo(TodoItem? todo)
{
if (todo != null && TodoItems.Contains(todo))
{
TodoItems.Remove(todo);
}
}
// ClearCompletedCommand的执行逻辑
private void ClearCompleted(object? parameter)
{
var completedItems = TodoItems.Where(t => t.IsCompleted).ToList();
foreach (var item in completedItems)
{
TodoItems.Remove(item);
}
}
}
*/
}TodoViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
namespace WpfApp1.Model
{
// 改写后:使用 ObservableObject(框架内置)+ [ObservableProperty],消除所有手搓的 INotifyPropertyChanged 样板代码
public partial class TodoItem : ObservableObject
{
// 框架自动生成:私有字段 _content + 公共属性 Content + PropertyChanged 通知
[ObservableProperty]
private string _content = string.Empty;
// 框架自动生成:私有字段 _isCompleted + 公共属性 IsCompleted + PropertyChanged 通知
[ObservableProperty]
private bool _isCompleted;
}
/*
* 下面是手搓代码--------------------------------------------------
// 待办项实体,实现INotifyPropertyChanged(状态变更通知界面)
public class TodoItem : INotifyPropertyChanged
{
private string _content;
private bool _isCompleted;
public string Content
{
get => _content;
set { _content = value; OnPropertyChanged(); }
}
public bool IsCompleted
{
get => _isCompleted;
set { _isCompleted = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
*/
}对比一下区别
核心差异对比(手搓代码 vs CommunityToolkit.Mvvm)
用表格清晰列出关键差异,让你一眼看到框架的简化效果:

额外说明(帮你深入理解框架)
1、关于「部分类(partial)」:代码中的 TodoViewModel 和 TodoItem 都加了 partial 关键字,
这是因为 CommunityToolkit.Mvvm 会在编译时生成对应类的另一部分代码(包含自动生成的属性、命令),
partial 是多文件拆分类的必要关键字,无需手动修改。
2、查看生成的代码:在 Visual Studio 中,你可以展开项目的「依赖项」→「分析器」→
「CommunityToolkit.Mvvm.SourceGenerators」→
「CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator」,
就能看到框架为 [ObservableProperty] 生成的代码,直观感受「编译时源码生成」的效果。
命令的参数传递:泛型命令(如 DeleteTodo)的参数类型会被框架自动识别,
XAML 中绑定命令时无需额外强转,和手搓代码的使用效果一致,但代码更简洁。
总结
1、CommunityToolkit.Mvvm 核心价值是消除 MVVM 样板代码,
让开发者专注于业务逻辑,提升开发效率。
2、框架通过「编译时源码生成」实现优化,既简化了代码,又不引入额外运行时开销,同时提供编译时校验。
3、与手搓代码相比,框架的核心简化点集中在 INotifyPropertyChanged 实现和 RelayCommand 命令创建,代码量大幅减少且更不易出错。
运行后新问题
运行后,发现一个问题:
输入项目后,添加按钮是灰色的。

核心原因
1、初始状态:NewTodoText 初始为空字符串,所以 CanAddTodo() 一开始就返回 false,按钮默认是灰色的。
2、状态未自动刷新:当你在输入框中输入内容时,NewTodoText 会更新,但 RelayCommand 默认
不会自动检测 CanExecute 条件的变化,所以按钮状态不会自动恢复可用。
解决方法:
让 CanExecute 自动刷新(关键一步)
有两种优雅的方式可以让 CanExecute 状态随 NewTodoText 变化而自动更新:
方式 1:使用框架的 NotifyCanExecuteChangedFor(推荐)
在 NewTodoText 的属性变更时,主动通知命令刷新 CanExecute 状态:
[ObservableProperty]
private string _newTodoText = string.Empty;
// 当 NewTodoText 变化时,通知 AddTodoCommand 刷新 CanExecute
partial void OnNewTodoTextChanged(string value)
{
AddTodoCommand.NotifyCanExecuteChanged();
}注:OnNewTodoTextChanged 是框架根据 [ObservableProperty] 自动生成的部分方法,直接写即可。
问题解决了:

使用vs2026