Search code examples
c#wpficommandcanexecute

Why does my button with an ICommand binding not immediately appear disabled when clicked?


I have a simple WPF program with an ICommand. I am finding that the button doesn't enable/disable as I would expect. I can illustrate this best with a contrived code example:

class Reload : ICommand
{
    private readonly BackgroundWorker _bworker = new BackgroundWorker();

    public Reload()
    {
        this._isExecuting = false;

        this._bworker.DoWork += this._bworker_DoWork;
        this._bworker.RunWorkerCompleted += this._bworker_RunWorkerCompleted;
    }

    public event EventHandler CanExecuteChanged;
    private void OnCanExecuteChanged()
    {
        if (this.CanExecuteChanged != null)
            this.CanExecuteChanged(this, EventArgs.Empty);
    }

    private bool _isExecuting;
    private void SetIsExecuting(bool isExecuting)
    {
        this._isExecuting = isExecuting;
        this.OnCanExecuteChanged();
    }

    public bool CanExecute(object parameter)
    {
        return !this._isExecuting;
    }

    public void Execute(object parameter)
    {
        //this does not update the GUI immediately
        this.SetIsExecuting(true);

        //This line doesn't fix my problem
        CommandManager.InvalidateRequerySuggested();

        //during this wait, button appears "clicked"
        Thread.Sleep(TimeSpan.FromSeconds(2)); //simulate first calculation

        this._bworker.RunWorkerAsync();
    }

    private void _bworker_DoWork(object sender, DoWorkEventArgs e)
    {
        //during this wait, button appears disabled
        Thread.Sleep(TimeSpan.FromSeconds(2)); //simulate second calculation
    }

    private void _bworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //this updates immediately
        this.SetIsExecuting(false);
    }
}

In the Execute(object) method, I trigger the CanExecuteChanged event in a way that will cause CanExecute(object) to return false. After that call, I expect the button to become disabled immediately, but it doesn't become disabled until some point between the call to RunWorkerAsync() and the second simulated calculation.

In the background worker's RunWorkerCompleted(...) event handler, I again trigger the CanExecuteChanged event, but this time in a way that will cause CanExecuteChanged(object) to return true. After this call, the button immediately becomes enabled.

Why does the button not immediately appear as disabled when I trigger the CanExecuteChanged event?

Note #1: that the first simulated calculation represents code that I have that should run on the main GUI thread. If I remove this call, the button acts as I would expect.

Note #2: I've read about using CommandManager.InvalidateRequerySuggested() to force the code to call the CanExecute(object) method. I've shown in my comments that this isn't working for me. Considering that I call OnCanExecuteChanged(...), I think that that suggestion is redundant anyway.


Solution

  • The right solution is the one you've already found, move the first long-running operation off of the UI thread.

    However, if you can't do that, the problem is that you aren't giving the UI a chance to run its binding and update the state. It probably updates as soon as the background worker starts (because control is returned from your function).

    You could take advantage of async/await and Task.Delay to give up some time for the UI to update:

    public async void Execute(object parameter)
    {
        //this does not update the GUI immediately
        this.SetIsExecuting(true);
    
        //Delays this function executing, gives the UI a chance to pick up the changes
        await Task.Delay(500);
    
        //during this wait, button appears "clicked"
        Thread.Sleep(TimeSpan.FromSeconds(2)); //simulate first calculation
    
        this._bworker.RunWorkerAsync();
    }
    

    Async/Await allows you to execute an operation asynchronously, and wait for it to finish, while allowing the current thread to continue executing (outside of the current method call). Its not super easy to explain all the technical details, see the link for more info.

    I would wait at least 20ms, and probably 50ms. Obviously delaying like this isn't the cleanest solution, but without removing the Sleep (or moving the code it represents off the UI thread) your options are pretty limited.