Reputation: 8208
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.
Upvotes: 1
Views: 821
Reputation: 61339
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.
Upvotes: 2