少有人走的路

勇哥的工业自动化技术网站

待办事项小程序(2) 使用框架CommunityToolkit.Mvvm重写这个程序

待办事项小程序(2) 使用框架CommunityToolkit.Mvvm重写这个程序


选择CommunityToolkit.Mvvm框架的理由有两个:

1. 它是微软管维护的mvvm轻量级框架,当前最新的.net10是支持的,未来版本也会长期支持。

2. 它会在编译时自动生成样板代码(对打了特性的命令方法)


解决方案

解决方案还是上节的方案。

可以看到RelayCommand.cs和ViewModelBase.cs已经缷载了。

需要框架进行改造的只有:

TodoItem.cs

TodoViewModel.cs

image.png


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)

用表格清晰列出关键差异,让你一眼看到框架的简化效果:

image.png

额外说明(帮你深入理解框架)

1、关于「部分类(partial)」:代码中的 TodoViewModelTodoItem 都加了 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 命令创建,代码量大幅减少且更不易出错。


运行后新问题

运行后,发现一个问题:

输入项目后,添加按钮是灰色的。

gif6.gif

核心原因

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] 自动生成的部分方法,直接写即可。


问题解决了:


gif7.gif



源代码下载

使用vs2026

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

«    2026年1月    »
1234
567891011
12131415161718
19202122232425
262728293031
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
搜索
最新留言
文章归档
网站收藏
友情链接

Powered By Z-BlogPHP 1.7.3

Copyright www.skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864