CommunityToolkit.Mvvm 实战
大约 8 分钟约 2445 字
CommunityToolkit.Mvvm 实战
简介
CommunityToolkit.Mvvm(原名 MVVM Toolkit)是微软官方维护的 MVVM 库,提供源生成器驱动的 ObservableProperty、RelayCommand 等标注,大幅减少 ViewModel 的样板代码。相比手写 INotifyPropertyChanged,源生成器可以在编译时自动生成属性变更通知代码,代码更简洁、性能更好。
特点
基础 ViewModel
ObservableObject 与属性
/// <summary>
/// 基础 ViewModel — 源生成器方式
/// </summary>
using CommunityToolkit.Mvvm.ComponentModel;
public partial class UserViewModel : ObservableObject
{
// 自动生成 public string Name { get; set; } + OnPropertyChanged()
[ObservableProperty]
private string _name = "";
[ObservableProperty]
private int _age;
[ObservableProperty]
private bool _isLoading;
// 属性变更回调
partial void OnNameChanged(string value)
{
// Name 属性变化时自动调用
Console.WriteLine($"名称变为:{value}");
OnPropertyChanged(nameof(DisplayName)); // 联动更新
}
partial void OnAgeChanged(int value)
{
Console.WriteLine($"年龄变为:{value}");
}
// 计算属性
public string DisplayName => string.IsNullOrEmpty(Name) ? "未知用户" : $"{Name} ({Age}岁)";
// [NotifyPropertyChangedFor] — 通知关联属性
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullInfo))]
private string _email = "";
public string FullInfo => $"{Name} - {Email}";
}验证属性
/// <summary>
/// 属性验证 — ObservableValidator
/// </summary>
using CommunityToolkit.Mvvm.ComponentModel;
public partial class RegisterViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage = "用户名不能为空")]
[MinLength(3, ErrorMessage = "用户名至少3个字符")]
private string _username = "";
[ObservableProperty]
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
private string _email = "";
[ObservableProperty]
[Required(ErrorMessage = "密码不能为空")]
[MinLength(8, ErrorMessage = "密码至少8个字符")]
private string _password = "";
[ObservableProperty]
[Required(ErrorMessage = "请确认密码")]
[CustomValidation(typeof(RegisterViewModel), nameof(ValidateConfirmPassword))]
private string _confirmPassword = "";
public static ValidationResult ValidateConfirmPassword(string confirmPassword, ValidationContext context)
{
var viewModel = (RegisterViewModel)context.ObjectInstance;
return confirmPassword == viewModel.Password
? ValidationResult.Success!
: new ValidationResult("两次密码不一致");
}
[RelayCommand]
private void Register()
{
ValidateAllProperties();
if (HasErrors) return;
// 执行注册
}
}命令
RelayCommand
/// <summary>
/// RelayCommand — 命令绑定
/// </summary>
using CommunityToolkit.Mvvm.Input;
public partial class ProductViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Product> _products = new();
[ObservableProperty]
private Product? _selectedProduct;
[ObservableProperty]
private string _searchText = "";
// 无参命令
[RelayCommand]
private async Task LoadProductsAsync()
{
var products = await _productService.GetAllAsync();
Products = new ObservableCollection<Product>(products);
}
// 带参命令
[RelayCommand]
private async Task DeleteProductAsync(Product product)
{
await _productService.DeleteAsync(product.Id);
Products.Remove(product);
}
// 带执行条件
[RelayCommand(CanExecute = nameof(CanEdit))]
private void EditProduct()
{
// 编辑逻辑
}
private bool CanEdit() => SelectedProduct != null;
// 通知 CanExecute 变化
partial void OnSelectedProductChanged(Product? value)
{
EditProductCommand.NotifyCanExecuteChanged();
}
// 同步命令
[RelayCommand]
private void Refresh()
{
SearchText = "";
_ = LoadProductsAsync();
}
}消息系统
弱引用消息
/// <summary>
/// Messaging — 组件间通信
/// </summary>
using CommunityToolkit.Mvvm.Messaging;
// 定义消息
public record UserLoginMessage(User User);
public record NotificationMessage(string Title, string Content);
// 发送消息
public partial class LoginViewModel : ObservableObject
{
[RelayCommand]
private void Login()
{
var user = _authService.Login(Username, Password);
if (user != null)
{
// 发送登录成功消息
WeakReferenceMessenger.Default.Send(new UserLoginMessage(user));
}
}
}
// 接收消息
public partial class MainViewModel : ObservableObject, IRecipient<UserLoginMessage>
{
public MainViewModel()
{
// 注册接收
WeakReferenceMessenger.Default.Register<UserLoginMessage>(this);
}
[ObservableProperty]
private string _welcomeText = "请登录";
public void Receive(UserLoginMessage message)
{
WelcomeText = $"欢迎,{message.User.Name}";
}
}
// 带令牌的定向消息
public enum MessengerTokens { UI, Data }
WeakReferenceMessenger.Default.Send(new NotificationMessage("标题", "内容"), MessengerTokens.UI);依赖属性与附加属性
ObservableProperty 与依赖属性协作
/// <summary>
/// 在 CustomControl 中使用 [ObservableProperty] 生成依赖属性
/// </summary>
/// 注意:[ObservableProperty] 只能生成普通 CLR 属性通知
/// 如果需要作为绑定目标(如动画、样式 Setter),仍需手动注册 DependencyProperty
public partial class GaugeControl : Control
{
// 手动注册依赖属性(支持 Style Setter 和动画)
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(GaugeControl),
new PropertyMetadata(0.0, OnMinimumChanged));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(GaugeControl),
new PropertyMetadata(100.0));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(double), typeof(GaugeControl),
new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnValueChanged));
public double Minimum { get => (double)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); }
public double Maximum { get => (double)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); }
public double Value { get => (double)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
// 依赖属性变更时通知 ViewModel
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (GaugeControl)d;
// 触发重绘或其他逻辑
control.InvalidateVisual();
}
private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// 最小值变更逻辑
}
// 附加属性示例 — 控制输入范围
public static readonly DependencyProperty InputRangeProperty =
DependencyProperty.RegisterAttached("InputRange", typeof(string),
typeof(GaugeControl),
new PropertyMetadata("", OnInputRangeChanged));
public static string GetInputRange(DependencyObject obj) => (string)obj.GetValue(InputRangeProperty);
public static void SetInputRange(DependencyObject obj, string value) => obj.SetValue(InputRangeProperty, value);
private static void OnInputRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox && e.NewValue is string range)
{
// 解析范围并设置验证规则
var parts = range.Split('-');
if (parts.Length == 2 &&
double.TryParse(parts[0], out double min) &&
double.TryParse(parts[1], out double max))
{
// 绑定验证规则
}
}
}
}异步操作模式
异步命令最佳实践
/// <summary>
/// 异步命令完整模式 — 包含加载状态、错误处理和取消支持
/// </summary>
public partial class DashboardViewModel : ObservableObject
{
private readonly IDashboardService _service;
private CancellationTokenSource? _cts;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string? _errorMessage;
[ObservableProperty]
private ObservableCollection<DeviceSummary> _devices = new();
[ObservableProperty]
private bool _canRefresh = true;
// 带取消的异步命令
[RelayCommand(CanExecute = nameof(CanRefresh))]
private async Task RefreshDataAsync(CancellationToken ct)
{
IsLoading = true;
ErrorMessage = null;
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
var data = await _service.GetDeviceSummariesAsync(_cts.Token);
Devices = new ObservableCollection<DeviceSummary>(data);
}
catch (OperationCanceledException)
{
ErrorMessage = "操作已取消";
}
catch (Exception ex)
{
ErrorMessage = $"加载失败:{ex.Message}";
_logger.LogError(ex, "刷新数据失败");
}
finally
{
IsLoading = false;
_cts?.Dispose();
_cts = null;
CanRefresh = true;
RefreshDataCommand.NotifyCanExecuteChanged();
}
}
// 取消命令
[RelayCommand]
private void Cancel()
{
_cts?.Cancel();
CanRefresh = false;
RefreshDataCommand.NotifyCanExecuteChanged();
}
// 定时刷新
private Timer? _autoRefreshTimer;
[RelayCommand]
private void StartAutoRefresh(int intervalSeconds = 30)
{
StopAutoRefresh();
_autoRefreshTimer = new Timer(async _ =>
{
await App.Current.Dispatcher.InvokeAsync(async () =>
{
await RefreshDataCommand.ExecuteAsync(null);
});
}, null, intervalSeconds * 1000, intervalSeconds * 1000);
}
[RelayCommand]
private void StopAutoRefresh()
{
_autoRefreshTimer?.Dispose();
_autoRefreshTimer = null;
}
}集合操作增强
ObservableCollection 高级操作
/// <summary>
/// 增强的 ObservableCollection — 支持批量更新、排序、过滤
/// </summary>
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
// 批量添加(只触发一次 CollectionChanged)
public void AddRange(IEnumerable<T> items)
{
if (items == null) throw new ArgumentNullException(nameof(items));
var newList = items.ToList();
if (newList.Count == 0) return;
foreach (var item in newList)
Items.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
// 批量移除
public void RemoveRange(IEnumerable<T> items)
{
if (items == null) return;
foreach (var item in items)
Items.Remove(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
// 替换全部内容
public void ReplaceAll(IEnumerable<T> items)
{
Items.Clear();
AddRange(items);
}
}/// <summary>
/// 集合过滤 ViewModel
/// </summary>
public partial class ProductListViewModel : ObservableObject
{
private readonly List<Product> _allProducts = new();
private readonly ObservableRangeCollection<Product> _filteredProducts = new();
public ObservableRangeCollection<Product> FilteredProducts => _filteredProducts;
[ObservableProperty]
private string _searchText = "";
[ObservableProperty]
private string _categoryFilter = "全部";
partial void OnSearchTextChanged(string value)
{
ApplyFilters();
}
partial void OnCategoryFilterChanged(string value)
{
ApplyFilters();
}
private void ApplyFilters()
{
var query = _allProducts.AsEnumerable();
if (!string.IsNullOrWhiteSpace(SearchText))
{
query = query.Where(p =>
p.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ||
p.Code.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(CategoryFilter) && CategoryFilter != "全部")
{
query = query.Where(p => p.Category == CategoryFilter);
}
_filteredProducts.ReplaceAll(query.ToList());
}
[RelayCommand]
private async Task LoadProductsAsync()
{
var products = await _productService.GetAllAsync();
_allProducts.Clear();
_allProducts.AddRange(products);
ApplyFilters();
}
}服务注入
ViewModel 与服务
/// <summary>
/// ViewModel 服务注入
/// </summary>
public partial class OrderViewModel : ObservableObject
{
private readonly IOrderService _orderService;
private readonly IDialogService _dialogService;
public OrderViewModel(IOrderService orderService, IDialogService dialogService)
{
_orderService = orderService;
_dialogService = dialogService;
}
[ObservableProperty]
private ObservableCollection<Order> _orders = new();
[ObservableProperty]
private bool _isBusy;
[RelayCommand]
private async Task LoadOrdersAsync()
{
IsBusy = true;
try
{
var orders = await _orderService.GetOrdersAsync();
Orders = new ObservableCollection<Order>(orders);
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task CreateOrderAsync()
{
var order = await _dialogService.ShowCreateOrderDialogAsync();
if (order != null)
{
await _orderService.CreateAsync(order);
await LoadOrdersAsync();
}
}
}优点
缺点
总结
CommunityToolkit.Mvvm 是 WPF MVVM 开发的首选库。核心用法:[ObservableProperty] 标注字段自动生成属性,[RelayCommand] 标注方法自动生成命令,ObservableValidator 提供验证支持。消息系统用 WeakReferenceMessenger 实现组件间解耦通信。推荐所有新项目使用。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《CommunityToolkit.Mvvm 实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《CommunityToolkit.Mvvm 实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《CommunityToolkit.Mvvm 实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《CommunityToolkit.Mvvm 实战》最大的收益和代价分别是什么?
