跳到主要内容

8.3 Prism 的属性通知与命令

学习目标​

你将用 Prism 减少属性通知和命令样板代码,区分数据变化、命令可用性与异步执行状态。前提:已完成 启动与容器,使用 .NET 10 WPF 和 Prism.DryIoc 9.0.537。完整操作见 PrismStudy 实践。

从后端 DTO 到界面状态​

后端 DTO 通常只承载某一时刻的数据;ViewModel 会被界面持续观察。改变字段后,绑定不会自动知道发生了什么。BindableBase 实现 INotifyPropertyChanged,SetProperty 比较旧值与新值,仅在变化时更新字段并通知。它不会自动校验业务规则,也不会自动为计算属性推导依赖关系。

例如 CanAdd 取决于 Title,标题变化时既要通知 Title,也要通知 CanAdd。ObservesCanExecute 使用这个布尔属性作为命令条件并订阅变化;ObservesProperty 则只触发条件重查,真正的判断仍由构造函数的 canExecute 委托提供。

本地新增命令​

下列是 TasksViewModel 的教学精简版,展示本地集合和命令;配套项目的完整文件还包含事件发布和删除确认,不要用本段覆盖完整文件。

ViewModels/TasksViewModel.cs:精简教学版
using System.Collections.ObjectModel;
using Prism.Commands;
using Prism.Mvvm;

namespace PrismStudy.ViewModels;

public sealed class TasksViewModel : BindableBase
{
private string _title = "";

public TasksViewModel()
{
AddCommand = new DelegateCommand(Add)
.ObservesCanExecute(() => CanAdd);
}

public ObservableCollection<string> Tasks { get; } = new();
public DelegateCommand AddCommand { get; }
public bool CanAdd => !string.IsNullOrWhiteSpace(Title);

public string Title
{
get => _title;
set
{
if (SetProperty(ref _title, value))
RaisePropertyChanged(nameof(CanAdd));
}
}

private void Add()
{
if (!CanAdd) return;
Tasks.Add(Title.Trim());
Title = "";
}
}

视图中的 TextBox 应绑定 Title 并设置 UpdateSourceTrigger=PropertyChanged,按钮绑定 AddCommand。命令状态是交互提示,业务入口仍应检查条件;程序直接调用执行方法时不能依赖按钮曾被禁用。

若采用 new DelegateCommand(Add, () => CanAdd).ObservesProperty(() => Title),也能在输入变化后重查。不要再用 ObservesCanExecute 叠加第二套判断;它会接管条件委托。条件涉及多个属性时观察全部依赖,或在相关 setter 中调用 RaiseCanExecuteChanged()。集合成员变化也不会自动等同于集合属性变化。

Prism 9 的异步命令​

Prism 9 提供 AsyncDelegateCommand。它默认在执行期间使 CanExecute 返回 false,用 IsExecuting 表示执行状态;只有确有并行需求时才启用 EnableParallelExecution()。这解决重复点击问题,服务端幂等和并发校验仍需独立实现。

以下是扩展练习片段:在上述类中增加 Status 的通知属性、SaveCommand 属性和 SaveAsync 方法,并把初始化语句放进构造函数。延时模拟网络等待,不会实际保存数据。

TasksViewModel.cs:异步扩展片段
// 文件顶部增加 using System.Threading; using System.Threading.Tasks;
// 以及 using System;
public AsyncDelegateCommand SaveCommand { get; }

// 构造函数内:
SaveCommand = new AsyncDelegateCommand(SaveAsync, () => CanAdd)
.ObservesProperty(() => Title)
.CancelAfter(TimeSpan.FromSeconds(2))
.Catch<OperationCanceledException>(_ => Status = "保存已取消")
.Catch(ex => Status = $"保存失败:{ex.Message}");

// 类中的方法:
private async Task SaveAsync(CancellationToken token)
{
if (!CanAdd) return;
var title = Title.Trim();
Status = "正在保存";
await Task.Delay(1000, token);
Status = $"模拟保存完成:{title}";
}

真实 HTTP 方法也应传递 token;取消是协作机制,不能让忽略令牌的工作强行停止。需要自行管理取消时,CancellationTokenSourceFactory 的名字虽带 Source,9.0.537 实际接收 Func<CancellationToken>,返回令牌而非令牌源。令牌源的创建、替换和释放由你的代码负责。线程与异常的进一步说明见 异步与 UI 线程。

常见错误与 Toolkit 对照​

DelegateCommand<int> 不适合作为命令参数类型:WPF 初始化绑定时可能查询空参数,Prism 因此拒绝非 nullable 值类型。使用 DelegateCommand<int?> 并检查 HasValue;引用类型参数同样要处理空值。把异步 lambda 交给同步 DelegateCommand 会形成 async void,应使用异步命令并明确取消、异常路径。

Prism 的 Region、对话框与容器不强制 ViewModel 继承 BindableBase。已有 MVVM Toolkit 类可以继续使用 ObservableObject 和生成命令;一个类不能同时继承这两个通知基类。选择一种属性通知方式即可,也不要为 Toolkit 再创建第二个应用容器。

练习与验收​

为新增条件加入“最多 30 个字符”,空白和超长输入都应立即禁用按钮,恢复有效输入后立即启用。给异步扩展配置 Status 绑定,再把延时改为三秒:验收时两秒后显示取消状态,按钮重新可用;改回一秒后只产生一次成功状态。直接执行新增命令时,非法标题也不得进入集合。

官方资料​

下一节:Region 与导航生命周期。