WPF ICommand CanExecute(): RaiseCanExecuteChanged() or automatic handling via DispatchTimer?

1.5k views Asked by At

I'm trying to determine the best way to cause ICommands' CanExecute() to be reflected in the UI.

I understand that the Dispatcher is the WPF (engine?) that handles UI drawing, and that by default, the Dispatcher evaluates ICommands' CanExecute() method upon instantiation as well as active user interface (clicking the UI, or keyboard input.)

Obviously, this is a problem when CanExecute() on any given ICommand changes, but neither mouse nor keyboard input is provided--and so the UI does not change to reflect the change in ICommand CanExecute() state.

There seems to be two solutions to this problem, and both include calling System.Windows.Input.CommandManager.InvalidateRequerySuggested().

This instructs the Dispatcher to re-evaluate each ICommand's CanExecute() and update the UI accordingly. I also understand that this can have performance-related issues, but that only seems to be an issue if one has a lot of ICommands (1000+?) or is performing work in their CanExecute() methods that they shouldn't be (for example, a network operation.)

Assuming that one has well-constructed CanExecute() methods, and given that the solution is to call InvalidateRequerySuggested, here are two ways I've found to do so:

  1. Implement a "RaiseCanExecuteChanged()" method on your ICommand interface solution.

Whether one is using DelegateCommand or RelayCommand (or some other implementation) inheriting from ICommand, they add a "RaiseCanExecuteChanged()" public method which simply calls the above InvalidateRequerySuggested, and the Dispatcher re-evaluates all ICommands and updates the UI accordingly.

Using this method, one should probably invoke it the following way:

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

(The reason why this should be used, as far as I can tell, is because this tells the Dispatcher to invoke the InvalidateRequerySuggested() on the main UI thread--and depending on where one calls "RaiseCanExecuteChanged()", they may not on the UI thread, and thus the Dispatcher would try to update that thread (instead of the main UI thread), not resulting in the controls/UI being updated as expected.)

  1. Implement a DispatcherTimer at the start of your application, and running on a timer, have it call InvalidateRequerySuggested regularly.

This solution creates a DispatcherTimer (running on a background thread) at a set interval, which then calls InvalidateRequerySuggested() on that interval, refreshing ICommands. The nature of a DispatcherTimer is that it runs on the Dispatcher thread (UI thread), so the above wrapped call isn't necessary.

For example, one could add this to their App.xaml.cs here:

    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.
    }

This solution causes InvalidateRequerySuggested() to re-evaluate on the given timer (presented here every quarter of a second) and automatically update the UI as appropriate.

Questions / Thoughts

I like the idea of a DispatcherTimer running automatically and re-evaluating on a set interval. However, that interval must be small, otherwise the lag between an ICommand's CanExecute() changing and the UI updating might be significant to the user. (For example, if the DispatchTimer is running on a 10 second interval, and something happens in your application to cause a button's CanExecute() to change to False, it might take up to 10 seconds before the UI updates accordingly.)

I dislike this solution because it feels like it's re-evaluating ICommands a lot. However, automatically updating the UI this way removes the need to call "RaiseCanExecuteChanged()" manually, and saves boilerplate.

INotifyPropertyChanged works similarly to RaiseCanExecuteChanged(), but instead, uses NotifyPropertyChanged(string propertyName) to handle updating only the specified Property. One can pass null to NotifyPropertyChanged, though, which causes the (Dispatcher?) to re-evaluate all Properties, in effect refreshing them.

  1. What does the community think is the best way to implement InvalidateRequerySuggested()? Via RaiseCanExecuteChanged(), or via DispatcherTimer?
  2. Could one eliminate the need to call "NotifyPropertyChanged()" and instead in the above DispatcherTimer not only call "InvalidateRequerySuggested()" but also "NotifyPropertyChanged(null)" on a given timer, refreshing both INotifyPropertyChanged and ICommands at the same time?
1

There are 1 answers

0
Contango On

In answer to your question: "Obviously, this is a problem when CanExecute() on any given ICommand changes, but neither mouse nor keyboard input is provided--and so the UI does not change to reflect the change in ICommand CanExecute() state.":

Normally, you would bind the IsEnabled state of the button to a property in the ViewModel. This means you could manually enable or disable the button.

The CanExecute() is effectively syntactic sugar for the IsEnabled property.

Now that we can control the IsEnabled state of the button, we can build anything on top of that: we can control its state using other buttons, or subscribe to a RX (Reactive Extensions) stream, etc.