WPF MVVM Community Toolkit. Mvvm 社区框架

Community Toolkit. Mvvm 社区框架

微软官方文档

主要内容:CommunityToolkit.Mvvm 框架

  1. 概念,安装,使用(重要API:ObservableObject,RelayCommand)
  2. 源生成器([ObservableProperty],[RelayCommand],[INotifyPropertyChanged])
  3. 命令详解(异步命令:AsyncRelayCommand 和 AsyncRelayCommand)
  4. 依赖注入(IServiceProvider,ServiceCollection)
  5. 信使(IMessenger,WeakReferenceMessenger、StrongReferenceMessenger 等)

ViewModel 支持通知:继承 ObservableObject

属性支持通知:SetProperty(ref name, value) 或 OnPropertyChanged( )

创建命令:直接使用类型 RelayCommand


核心功能

Community Toolkit. Mvvm 内置源生成器可以使用特性生成代码

1. [INotifyPropertyChanged] 和 [ObservableProperty]

ObservableObject 是 ViewModel 的基类,支持 INotifyPropertyChanged INotifyPropertyChanged

要 .Net 8.0 版本以后才可以使用源生成器这种语法

csharp 复制代码
 

[INotifyPropertyChanged] //相当于 类继承 :ObservableObject
public partial class PersonViewModel  //🔴!需要 partial 部分类
{
    [ObservableProperty] //[ObservableProperty] 会生成一个: 基于 name的字段的属性Name
    private string name; //🔴调用时,要调:Name 大写
//  相当于: public string Name
//         {
//             get => name;
//             set => SetProperty(ref name, value);
//         }

    [ObservableProperty]
    private int age;
}

这段代码会自动生成 NameAge 的属性,同时自动实现 INotifyPropertyChanged 的通知。

  1. SetProperty(ref name, value):支持通知

    csharp 复制代码
    SetProperty(ref name, value) //通知的方法,相当于: 
    
    //   ⬇⬇⬇ 相当于
    
    if (_name != value)
    {
      _name = value;
      OnPropertyChanged(); // 或 OnPropertyChanged(nameof(Name));
    }

2. RelayCommand 命令

你可以轻松创建命令而不需要手动实现 ICommand 接口。

csharp 复制代码
using CommunityToolkit.Mvvm.Input;

[INotifyPropertyChanged]
public partial class PersonViewModel //🔴需要 partial 部分类
{
    1️⃣//同步命令
    [RelayCommand] //在编译时会自动在方法后面加:Command
    private void SayHello() //🔴调用时要调用:SayHelloCommand
    {
        MessageBox.Show("Hello!");
    }
  
    2️⃣//异步命令
    [RelayCommand]
    private async Task GreetUserAsync() //🔴编译时会变成:GreetUserCommand
    {
        await Task.Delay(3000);
        MessageBox.Show($"Hello 源生器!");
    }

  
    3️⃣//带 CanExecute 命令
    [RelayCommand(CanExecute = nameof(CanGreetUser))] //加入 CanExecute 判断执行
    private void SayHello() 
    {
        MessageBox.Show("Hello!");
    }
    private bool CanGreetUser(User? user)
    {
      return user is not null;
    }
  
}

使用 [RelayCommand] 会自动生成一个 SayHelloCommand 属性,供 XAML 使用。

异步方法会 去掉 Async ,添加 Command ,变成 SayHelloCommand


3. ObservableValidator 验证

如果你需要验证输入数据(比如表单),可以继承 ObservableValidator

csharp 复制代码
public partial class FormViewModel : ObservableValidator
{
    [ObservableProperty]
    [Required]
    [MinLength(3)]
    private string name;
}

XAML 示例绑定命令

xml 复制代码
<Button Content="Say Hello" Command="{Binding SayHelloCommand}" />
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />

信使

"信使",在 MVVM 架构中指的是 Messenger(消息通信机制),用于实现 ViewModel 之间的解耦通信。

MVVM 工具包提供两种现用的实现:

  1. WeakReferenceMessenger
    • 在内部使用弱引用,为收件人提供自动内存管理
  2. StrongReferenceMessenger
    • 使用强引用,并要求开发人员在不再需要收件人时手动取消订阅收件人

使用场景

  • 不同 ViewModel 之间发送和接收消息
  • 让 ViewModel 通知其他模块某件事发生了
  • 解耦:不通过事件或引用直接交互

示例:发送与接收消息

1. 创建消息类(推荐继承 ValueChangedMessage<T>
csharp 复制代码
public class NameChangedMessage : ValueChangedMessage<string>
{
  public NameChangedMessage(string value) : base(value) 
  { 
    
  }
}

2. 接收消息的 ViewModel
csharp 复制代码
public class ReceiverViewModel : ObservableRecipient
{
    public ReceiverViewModel()
    {
        // 必须开启 IsActive 才能自动注册接收器
        IsActive = true;

        1️⃣ Messenger.Register<ReceiverViewModel, NameChangedMessage>(this, (r, m) =>
        { 
            // 处理消息
            //在这里处理消息,r为接收方,m为发送方
            //输入消息。使用传递的接收者作为输入使得
            // lambda表达式不会捕获"this",从而提高性能。
            Console.WriteLine($"接收到新名字:{m.Value}");
          
        });
    }
}

2️⃣ //不继承ObservableRecipient 的方案
WeakReferenceMessenger.Default.Register<ReceiverViewModel>(this, (r, m) =>
{

});

Messenger 基类自带的属性,

不继承 ObservableRecipient 也可以直接用 WeakReferenceMessenger.Default.Register


3. 发送消息的 ViewModel
csharp 复制代码
public class SenderViewModel
{
    public void SendMessage()
    {
        WeakReferenceMessenger.Default.Send(new NameChangedMessage("新名字张三"));
    }
}

使用 DI 注入 IMessenger(可选)

你也可以将 Messenger 注入进来,而不是使用静态默认实例:

csharp 复制代码
public class SenderViewModel
{
    private readonly IMessenger _messenger;

    public SenderViewModel(IMessenger messenger)
    {
        _messenger = messenger;
    }

    public void SendMessage()
    {
        _messenger.Send(new NameChangedMessage("李四"));
    }
}

App.xaml.cs 中注册:

csharp 复制代码
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);

注销收件人

当不再需要收件人时,应将其注销,以便停止接收消息。 可以按消息类型、注册令牌或收件人取消注册:

csharp 复制代码
//取消对消息类型的接收者的注册
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);

//取消指定通道中消息类型的接收者的注册
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);

//取消所有通道中所有消息的接收方注册
WeakReferenceMessenger.Default.UnregisterAll(this);
说明
ObservableRecipient 让 ViewModel 支持消息监听,需设置 IsActive = true
WeakReferenceMessenger 默认静态全局信使,可直接用
Register<TMessage> 注册监听消息
Send<TMessage> 发送消息

总结:

功能 实现方式
ViewModel 通信 使用 Messenger.RegisterSend
解耦 ViewModel 不直接引用其他 ViewModel 或服务
支持 DI 可注入 IMessenger

IServiceProvider 服务提供者接口

IServiceProvider:服务提供者接口

在 WPF + MVVM 应用中,IServiceProvider 常用于实现 依赖注入(DI),尤其是在使用 CommunityToolkit.Mvvm 配合 .NET Core/5+/6+ 的 WPF 应用时,这个接口是核心组成之一。

什么是 IServiceProvider:

  1. IServiceProvider服务器提供者接口,主要给VM提供各种服务(数据库操作服务,日志服务,定位服务等)
  2. IServiceProvider接口中只有一个GetService()方法,用来获取服务
  3. 它通常与 Microsoft.Extensions.DependencyInjection 一起使用,用于解析依赖项,比如 ViewModel、服务类等。

使用示例

需要安装 Microsoft.Extensions.DependencyInjection 包

后引用:using Microsoft.Extensions.DependencyInjection;

通用依赖注入容器库,可以:

  • 注册服务:services.AddSingleton<T>() / AddTransient<T>()
  • 构建服务提供器:services.BuildServiceProvider()
  • 获取服务:provider.GetRequiredService<T>()
  • 与 MVVM 模式完美配合
1. 注册依赖(Startup)

你需要在 App.xaml.cs 中设置依赖注入容器:

csharp 复制代码
using Microsoft.Extensions.DependencyInjection;

public partial class App : Application
{
  public App()
  {
    Services = ConfigureServices();
    this.InitializeComponent();  
  }

  //获取当前应用程序实例并转换为App类型
  //在全项目范围通过 App.Current 安全访问自定义的 App 对象
  public new static App Current => (App)Application.Current; 

  public IServiceProvider Services { get; } //用来接收 服务提供器 的字段

  // 配置服务
  public static IServiceProvider ConfigureServices()
  {
    //1️⃣ 定义一个服务集合,服务集合编译之后,产生一个ServiceProvider
    // 而ServiceProvider类型实现了IServiceProvider接口
    var services = new ServiceCollection();

    //2️⃣ 注册 ViewModel 和服务
    // services.AddTransient<MainWindowViewModel>();
    // Transient 瞬间模式;
    // Singleton 单例模式
    services.AddSingleton<MainWindowViewModel>();
    services.AddSingleton<AnimalWindowViewModel>();

    // 注册服务
    //services.AddSingleton<接口, 实现类>();
    //AddSingleton 使用时会自动创建一个唯一的实现实例,注入后直接调用接口就可以使用其实现类实现的方法
    services.AddSingleton<IMyService, MyService>();
    services.AddSingleton<IPerson<Person,ViewPerson>, PersonService>();
    services.AddSingleton<IAnimal<Animal,ViewAnimal>, AnimalService>();

    //3️⃣ 构建服务提供器 services.BuildServiceProvider() 并返回
    return services.BuildServiceProvider();
  }
}
  1. 你注册的时候:

    csharp 复制代码
    ✅//这表示 MainViewModel 需要的服务(IMyService)会自动注入。
    services.AddSingleton<IMyService, MyService>();
    services.AddSingleton<MainViewModel>();
  2. 在上面代码中:

    csharp 复制代码
    ✅//是 WPF 应用中为了更方便地获取当前应用程序实例的一种做法。
    public new static App Current => (App)Application.Current;
    • Application.Current 是一个静态属性,返回当前运行的 WPF 应用实例,其类型Application

    • 你自定义的 App 类一般是继承自 Application

    • 为了能够在程序其他地方方便地访问你自定义的 App 类型,而不是基类 Application,你需要做一次类型转换。


2. 如:在构造函数中使用依赖注入

比如在 MainViewModel 中注入一个服务:

csharp 复制代码
public class MainViewModel : ObservableObject
{
    private readonly IMyService _service;

    public MainViewModel(IMyService service)
    {
        _service = service;
    }
}

3. 获取服务

你可以随时通过 App.Services.GetService() 获取已注册的对象:

csharp 复制代码
var vm = App.Services.GetRequiredService<MainViewModel>();
csharp 复制代码
//1. 如: App.xaml.cs 中启动主窗口
var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.Show();
csharp 复制代码
//2. 如:View 构造函数中注入 ViewModel
public partial class MainWindow : Window
{
    public MainWindow(MainViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
    }
}
csharp 复制代码
//3. ViewModel 构造函数中注入服务
public class MainViewModel : ObservableObject
{
    private readonly IMyService _myService;

    public MainViewModel(IMyService myService)
    {
        _myService = myService;
    }

    // 使用 _myService 来执行操作
}
csharp 复制代码
//4. 绑定上下文

// MainWindowViewModel视图模型和MainWindow窗体耦合了。
//this.DataContext = new MainWindowViewModel();

// 依赖注入,但没有完全解耦。
//this.DataContext = App.Current.Services.GetService(typeof(MainWindowViewModel));

// 完全解耦了
Type type = Type.GetType("_1.CommunityToolkit.Mvvm基本使用.ViewModels.MainWindowViewModel");
this.DataContext = App.Current.Services.GetService(type);
//this.DataContext = new MainWindowViewModel(new PersonService(), new AnimalService());

搭配 MVVM Toolkit 的建议:

虽然 CommunityToolkit.Mvvm 本身没有内建 DI 容器,但它 完全支持 DI 的设计理念。只要你配合 .NET Generic HostMicrosoft.Extensions.DependencyInjection,就能构建清晰的 MVVM 架构。

  • 注册 IServiceProvider
  • 自动注入 ViewModel
  • 使用 RelayCommand 和 ObservableProperty

📁 附:推荐的文件夹命名约定

推荐的文件夹命名结构(基于 MVVM 模式):

bash 复制代码
MyWpfApp/
├── Models/              # 业务模型、数据结构类
├── ViewModels/          # 视图模型(含 ObservableObject / RelayCommand 等)
├── Views/               # XAML 视图文件(*.xaml + *.xaml.cs)
├── Services/            # 服务类(如导航、API 调用、本地存储等)
├── Converters/          # IValueConverter 实现
├── Behaviors/           # 附加行为(例如交互逻辑)
├── Helpers/             # 帮助类、扩展方法
├── Resources/           # 资源文件(样式、模板、图片等)
└── App.xaml             # 应用入口

各文件夹用途说明:

文件夹 用途说明
Models 定义实体类、DTO、业务模型,如 User.csOrder.cs
ViewModels 每个 View 对应一个 ViewModel,例如 MainViewModel.cs
Views 存放 XAML 视图文件,如 MainView.xamlMainView.xaml.cs
Services 例如 INavigationService, IDataService
Converters 转换器类,如 BoolToVisibilityConverter.cs
Behaviors 附加行为(Blend SDK 或自定义)
Helpers 静态辅助类、扩展方法
Resources 包括 Styles.xaml, Themes, 图片等资源
相关推荐
He BianGu1 小时前
演示:【WPF-WinCC3D】 3D工业组态监控平台源代码
wpf
拜特流动1 小时前
WPF核心类继承树结构
wpf
天天进步20151 小时前
C# Prism框架详解:构建模块化WPF应用程序
开发语言·c#·wpf
沉到海底去吧Go13 小时前
【工具教程】图片识别内容改名,图片指定区域识别重命名,批量识别单据扫描件批量改名,基于WPF和腾讯OCR的实现方案
ocr·wpf
Ryannn_NN1 天前
avalonia android连接模拟器时报错adb cannot run as root in production builds,没有权限
android·adb·wpf·xamarin
界面开发小八哥1 天前
界面组件DevExpress WPF中文教程:Grid - Bands分隔符
wpf
2301_803297751 天前
web基础
前端·wpf
飞人博尔特的摄影师1 天前
WPF技巧-常用的Converter集合(更新ing)
c#·wpf·xaml·maui·uwp·技巧·valueconverter
炯哈哈1 天前
【上位机——WPF】命名空间
开发语言·windows·c#·wpf·上位机