'CanExecute works only partial
I am working on a WPF .NET 5 application that needs to handle a longer task using a button command. Until the task is done, the button should be disabled.
I am using the RelayCommand from Microsoft.Toolkit.Mvvm:BtnCmd = new RelayCommand(DoSomething, CanDoSomething);
The first thing the DoSomething method does is make the return value of CanDoSomething
false. This prevents DoSomething from being executed again, but it is not visually visible on the Button.
While researching, I came across that this is indeed the case on https://github.com/CommunityToolkit/MVVM-Samples/issues/41:
"Sergio0694 commented on Mar 28, 2021": "That is correct, by default RelayCommand will not automatically update its visual state on WPF...".
The solution he recommends is using: https://gist.github.com/Sergio0694/130bc344e552e501563546454bd4e62a and
<button xmlns:input="using:Microsoft.Toolkit.Mvvm.Wpf.Input"
Command="{Binding Command}"
input:RelayCommandExtensions.IsCommandUpdateEnabled="True"/>
My DoSomething Mehod looks like this:
private async void DoSomething()
{
PropertyCheckedByCanDoSomething = false;
await Task.Delay(1000);
PropertyCheckedByCanDoSomething = true;
}
It will give the desired visual effect, but only on the line: PropertyCheckedByCanDoSomething = false;
With PropertyCheckedByCanDoSomething = true;
the effect is only visible after clicking into the application or doing a window switch.
How can I fix this?
Thanks a lot for any support.
Solution 1:[1]
Without knowing how all your bindings and such are going, with I might approach it by subclassing your relay command, something like
using System;
using System.Windows.Input;
namespace MyTestApp
{
public class MyRelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public MyRelayCommand(Action<object> execute) : this(execute, CanAlwaysExecute)
{ }
public MyRelayCommand(Action<object> execute, Func<object, bool> canExecute)
{
// Lamda expression to execute each respectively
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object cmdParm)
{ return _canExecute(cmdParm); }
public static bool CanAlwaysExecute(object cmdParm)
{ return true; }
public void Execute(object cmdParm)
{
if (!_doingWithCallback)
_execute(cmdParm);
else
Execute2(cmdParm);
}
// The CanExecuteChanged event handler is required from the ICommand interface
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, new EventArgs());
}
private bool _isMyTaskRunning = false;
public bool IsMyTaskRunning
{ get { return _isMyTaskRunning; } }
private bool _doingWithCallback;
private readonly Action<object, Action> _executeWithCallback;
public MyRelayCommand(Action<object, Action> executeWithCallback) : this( executeWithCallback, CanAlwaysExecute)
{ }
public MyRelayCommand(Action<object, Action> executeWithCallback, Func<object, bool> canExecute)
{
// new flag, so when the default "Execute" method is called, it can then redirect to
// calling the Execute2() method that checks to prevent the double-click and then
// calls your function with the additional parameter of the action method to call upon completion.
_doingWithCallback = true;
_executeWithCallback = executeWithCallback;
_canExecute = canExecute;
}
public void Execute2(object cmdParm)
{
// prevent double action if running vs not
if (_isMyTaskRunning)
return;
// flag it to prevent double action
_isMyTaskRunning = true;
// trigger raising the CanExecute changed which will update the user interface
RaiseCanExecuteChanged();
// turn off when done, but if being done from a "task()" process,
// you probably want to have a return function be called when the
// TASK is finished to re-enable the button... maybe like
// NOW, call your execute function that accepts TWO parameters.
// the first is whatever parameter MAY come from the button click itself.
// the SECOND parameter will be to accept MY ACTION HERE to reset the flag when finished
System.Threading.Tasks.Task.Run(() => _executeWithCallback(cmdParm, ButtonTaskIsComplete));
}
public void ButtonTaskIsComplete()
{
_isMyTaskRunning = false;
System.Windows.Application.Current.Dispatcher.Invoke(() => { RaiseCanExecuteChanged(); });
}
}
}
May not be a perfect fit, but might offer a possible wrapper solution for you.
And here is a sample implementation to call it in your existing form area.
private MyRelayCommand _myFormButton;
public MyRelayCommand MyFormButton
{ get { return _myFormButton ?? ( _myFormButton = new MyRelayCommand( YourFormMethod )); } }
public void YourFormMethod(object cmdParm, System.Action doThisWhenFinished)
{
MessageBox.Show("Something from after the click event of the button");
// NOW, the callback function so the button re-enables itself once finished.
doThisWhenFinished();
}
Solution 2:[2]
I had a similar problem with that RelayCommandExtension you refered to.
There exists a Github issue regarding this. The following solution was posted there:
In RelayCommandExtensions.cs
add the following line:
notifiers.Add(obj, notifier);
to the OnIsCommandUpdateEnabledChanged(...)
method so that it looks like that:
// Ensure a notifier is enabled so that we can monitor for changes to the command
// property in the future. This is also needed in case this attached property is
// set before the binding engine has actually resolved the target command instance.
if (!notifiers.TryGetValue(obj, out _))
{
PropertyChangeNotifier notifier = new(obj, nameof(ICommandSource.Command));
notifier.PropertyChanged += (_, _) => ToggleIsCommandUpdateEnabled(obj);
notifiers.Add(obj, notifier);
}
This is doing the trick for me.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |