如何使用System.Reactive控制复杂场景下WPF命令的可用性
实现方案
前置依赖
需要安装如下NuGet包:
System.Reactive:Rx核心库- 如果选择更简便的ReactiveUI实现,额外安装
ReactiveUI、ReactiveUI.WPF
简便实现(基于ReactiveUI)
ReactiveUI的WhenAnyValue天然支持嵌套属性监听、自动空值处理,完全匹配需求。
首先让MyViewModel继承ReactiveObject,然后按如下写法实现:
合并条件写法
public class MyViewModel : ReactiveObject { public MyViewModel() { // 监听所有依赖属性的变更,自动处理空值 var canExecute = this.WhenAnyValue( vm => vm.ActiveDocument.Highlighting.Type, vm => vm.ActiveDocument.IsReadOnly, vm => vm.License.Kind, vm => vm.License.TrialDay, (type, isReadOnly, licenseKind, trialDay) => type == Highlighting.Xml && !isReadOnly && (licenseKind == LicenseKind.Full || trialDay < 30) ) .DistinctUntilChanged() // 仅当状态变更时才触发更新 .StartWith(false); // 初始默认值为false MyCommand = ReactiveCommand.Create(DoSomething, canExecute); } public ICommand MyCommand { get; } // 其他所需属性 }
拆分条件写法(便于复用)
public class MyViewModel : ReactiveObject { public MyViewModel() { // 拆分单个条件,可多处复用 var highlightingIsXml = this.WhenAnyValue(vm => vm.ActiveDocument.Highlighting.Type) .Select(t => t == Highlighting.Xml) .StartWith(false); var documentNotReadonly = this.WhenAnyValue(vm => vm.ActiveDocument.IsReadOnly) .Select(isReadOnly => !isReadOnly) .StartWith(false); var appIsLicensed = this.WhenAnyValue(vm => vm.License.Kind, vm => vm.License.TrialDay) .Select(pair => pair.Item1 == LicenseKind.Full || pair.Item2 < 30) .StartWith(false); // 组合所有条件 var canExecute = Observable.CombineLatest( highlightingIsXml, documentNotReadonly, appIsLicensed, (isXml, notReadonly, licensed) => isXml && notReadonly && licensed ) .DistinctUntilChanged(); MyCommand = ReactiveCommand.Create(DoSomething, canExecute); } public ICommand MyCommand { get; } // 其他所需属性 }
纯System.Reactive实现(不依赖ReactiveUI)
如果不想引入ReactiveUI,可以自行实现属性变更的监听扩展方法:
// 基础扩展方法,将INotifyPropertyChanged属性变更转为可观测序列 public static class RxExtensions { public static IObservable<TProperty> ObserveProperty<T, TProperty>( this T source, Expression<Func<T, TProperty>> propertyExpr) where T : INotifyPropertyChanged { var getter = propertyExpr.Compile(); var memberExpr = (MemberExpression)propertyExpr.Body; var propertyName = memberExpr.Member.Name; return Observable.FromEventPattern<PropertyChangedEventArgs>( h => source.PropertyChanged += h, h => source.PropertyChanged -= h) .Where(e => e.EventArgs.PropertyName == propertyName || string.IsNullOrEmpty(e.EventArgs.PropertyName)) .Select(_ => getter(source)) .StartWith(getter(source)); } }
嵌套属性的监听可以通过Switch操作符实现自动切换:
public class MyViewModel : INotifyPropertyChanged { public MyViewModel() { // 监听ActiveDocument.Highlighting.Type的全链路变更 var typeObs = this.ObserveProperty(vm => vm.ActiveDocument) .Select(doc => doc == null ? Observable.Return(default(Highlighting)) : doc.ObserveProperty(d => d.Highlighting) .Select(h => h?.Type ?? default)) .Switch(); // 监听ActiveDocument.IsReadOnly变更 var isReadOnlyObs = this.ObserveProperty(vm => vm.ActiveDocument) .Select(doc => doc == null ? Observable.Return(true) : doc.ObserveProperty(d => d.IsReadOnly)) .Switch(); // 监听授权相关属性变更 var licenseObs = this.ObserveProperty(vm => vm.License) .Select(lic => lic == null ? Observable.Return(false) : lic.ObserveProperty(l => l.Kind) .CombineLatest(lic.ObserveProperty(l => l.TrialDay), (kind, day) => kind == LicenseKind.Full || day < 30)) .Switch(); // 组合所有条件 var canExecute = Observable.CombineLatest( typeObs, isReadOnlyObs, licenseObs, (type, isReadOnly, licensed) => type == Highlighting.Xml && !isReadOnly && licensed ) .DistinctUntilChanged() .StartWith(false); // 自定义接收IObservable<bool>的Command实现即可 MyCommand = new CustomCommand(DoSomething, canExecute); } public ICommand MyCommand { get; } // 其他所需属性、INotifyPropertyChanged实现 }
能力适配说明
以上实现完全覆盖原LambdaCondition的核心能力:
- 全链路变更监听:自动监听访问路径上所有实现INotifyPropertyChanged的实例的属性变更,实例替换时自动解绑旧实例、绑定新实例
- 空值自动处理:访问链任意节点为null时自动返回预设默认值,无需手动编写多层判空逻辑
- 按需刷新:
DistinctUntilChanged保证仅当命令可执行状态实际发生变化时才触发通知,避免UI无效重绘
内容的提问来源于stack exchange,提问作者Spook
相关产品推荐
相关产品推荐

