WPF ICommand CanExecute(): RaiseCanExecuteChanged() 还是通过 DispatchTimer 自动处理?

Posted

技术标签:

【中文标题】WPF ICommand CanExecute(): RaiseCanExecuteChanged() 还是通过 DispatchTimer 自动处理?【英文标题】:WPF ICommand CanExecute(): RaiseCanExecuteChanged() or automatic handling via DispatchTimer? 【发布时间】:2015-09-13 17:30:36 【问题描述】:

我正在尝试确定使 ICommands 的 CanExecute() 反映在 UI 中的最佳方式。

我了解 Dispatcher 是处理 UI 绘制的 WPF(引擎?),默认情况下,Dispatcher 在实例化时评估 ICommands 的 CanExecute() 方法以及活动用户界面(点击 UI 或键盘输入。)

显然,当任何给定 ICommand 上的 CanExecute() 发生更改,但未提供鼠标或键盘输入时,这是一个问题 - 因此 UI 不会更改以反映 ICommand CanExecute() 状态的更改。

这个问题似乎有两种解决方案,都包括调用 System.Windows.Input.CommandManager.InvalidateRequerySuggested()。

这指示 Dispatcher 重新评估每个 ICommand 的 CanExecute() 并相应地更新 UI。我也知道这可能会出现与性能相关的问题,但这似乎只是一个问题,如果一个人有很多 ICommands(1000+?)或者在他们的 CanExecute() 方法中执行他们不应该的工作(对于例如,网络操作。)

假设一个具有良好构造的 CanExecute() 方法,并且鉴于解决方案是调用 InvalidateRequerySuggested,我发现有两种方法可以这样做:

    在您的 ICommand 接口解决方案上实现“RaiseCanExecuteChanged()”方法。

无论是使用继承自 ICommand 的 DelegateCommand 还是 RelayCommand(或其他一些实现),它们都会添加一个“RaiseCanExecuteChanged()”公共方法,该方法只是调用上述 InvalidateRequerySuggested,并且调度程序会重新评估所有 ICommand 并相应地更新 UI .

使用此方法,可能应该通过以下方式调用它:

Application.Current.Dispatcher.BeginInvoke(
    DispatcherPriority.Normal,
    (System.Action)(() => 
    
        System.Windows.Input.CommandManager.InvalidateRequerySuggested();
    ));

(据我所知,应该使用它的原因是因为它告诉 Dispatcher 在主 UI 线程上调用 InvalidateRequerySuggested() - 并且取决于调用“RaiseCanExecuteChanged()”的位置,它们可能不在 UI 线程上,因此 Dispatcher 会尝试更新该线程(而不是主 UI 线程),不会导致控件/UI 按预期更新。)

    在应用程序启动时实现 DispatcherTimer,并在计时器上运行,让它定期调用 InvalidateRequerySuggested。

此解决方案以设定的时间间隔创建 DispatcherTimer(在后台线程上运行),然后在该时间间隔调用 InvalidateRequerySuggested(),刷新 ICommand。 DispatcherTimer 的本质是运行在 Dispatcher 线程(UI 线程)上,因此不需要上面的封装调用。

例如,可以在此处将其添加到他们的 App.xaml.cs:

    public partial class App : Application
    
        protected override void OnStartup(StartupEventArgs e)
        
            base.OnStartup(e);

            System.Windows.Threading.DispatcherTimer dt = new System.Windows.Threading.DispatcherTimer()
            
                Interval = new TimeSpan(0, 0, 0, 0, 25),
                IsEnabled = true
            ;

            dt.Tick += delegate(object sender, EventArgs be)
            
                System.Windows.Input.CommandManager.InvalidateRequerySuggested();
            ;

            dt.Start();

           // ... Other startup logic in your app here.
    

此解决方案会导致 InvalidateRequerySuggested() 重新评估给定的计时器(此处每四分之一秒显示一次)并根据需要自动更新 UI。

问题/想法

我喜欢 DispatcherTimer 自动运行并在设定的时间间隔内重新评估的想法。但是,该间隔必须很小,否则 ICommand 的 CanExecute() 更改和 UI 更新之间的滞后对用户来说可能很重要。 (例如,如果 DispatchTimer 以 10 秒的间隔运行,并且您的应用程序中发生了某些事情导致按钮的 CanExecute() 更改为 False,则 UI 可能需要最多 10 秒才能相应地更新。)

我不喜欢这个解决方案,因为它感觉就像是在重新评估 ICommands 很多。 但是,以这种方式自动更新 UI 消除了手动调用“RaiseCanExecuteChanged()”的需要,并节省了样板文件。

INotifyPropertyChanged 的工作方式与 RaiseCanExecuteChanged() 类似,但它使用 NotifyPropertyChanged(string propertyName) 来处理仅更新指定的属性。但是,可以将 null 传递给 NotifyPropertyChanged,这会导致(Dispatcher?)重新评估 all 属性,实际上是刷新它们。

    社区认为实现 InvalidateRequerySuggested() 的最佳方式是什么?通过 RaiseCanExecuteChanged() 还是通过 DispatcherTimer? 是否可以消除调用“NotifyPropertyChanged()”的需要,而在上面的 DispatcherTimer 中不仅调用“InvalidateRequerySuggested()”而且在给定计时器上调用“NotifyPropertyChanged(null)”,同时刷新 INotifyPropertyChanged 和 ICommands时间?

【问题讨论】:

假设有一个函数提供了CanExecute 调用的布尔值,那么这个函数的更新是什么? @Contango 一个典型的例子是 Josh Smith 的 RelayCommand 实现,你可以在这里查看:msdn.microsoft.com/en-us/magazine/dd419663.aspx 注意这个实现如何接受一个 Action 和一个 Predicate 作为 ICommand 的 CanExecute() 和 Execute() 的参数. 【参考方案1】:

在回答您的问题时:“显然,当任何给定 ICommand 上的 CanExecute() 发生更改时,这是一个问题,但未提供鼠标或键盘输入 - 因此 UI 不会更改以反映 ICommand CanExecute 中的更改() 状态。”:

通常,您会将按钮的IsEnabled 状态绑定到 ViewModel 中的属性。这意味着您可以手动启用或禁用该按钮。

CanExecute()IsEnabled 属性的有效语法糖。

现在我们可以控制按钮的IsEnabled 状态,我们可以在此之上构建任何东西:我们可以使用其他按钮控制它的状态,或者订阅 RX(Reactive Extensions)流等

【讨论】:

以上是关于WPF ICommand CanExecute(): RaiseCanExecuteChanged() 还是通过 DispatchTimer 自动处理?的主要内容,如果未能解决你的问题,请参考以下文章

wpf mvvm模式 Icommand接口应该如何理解?

WPF 自定义快捷键命令(COMMAND)(转)

使用 CommandManager 时对 ICommand.CanExecute 进行单元测试

wpf的命令怎么绑定多个条件

命令基础

WPF 定义Command