0

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.

dhilmathy
  • 2,800
  • 2
  • 21
  • 29
ebiondi
  • 23
  • 9
  • Is `PropertyCheckedByCanDoSomething` firing a PropertyChanged event? – Neil Feb 01 '22 at 18:10
  • If you like to use a different MVVM framework have a look at https://reactiveui.net – Sir Rufo Feb 01 '22 at 18:27
  • @Neil Yes as documented:`set => SetProperty(ref _propertyCheckedByCanDoSomething, value);}` – ebiondi Feb 01 '22 at 18:37
  • The DoSomething call is not awaited. Anything happing after `await Task.Delay` will not affect the UI. Except the UI is somehow also bound to PropertyCheckedByCanDoSomething , we do not know. You may search the web for an AsyncRelayCommand implementation. – Clemens Feb 01 '22 at 19:36
  • @Clemens I changed the code to AsyncRelayCommand. Does not help. BTW: Everything works as expected except the visual representation of the button. Thank you anyway. – ebiondi Feb 01 '22 at 20:23

2 Answers2

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.

phibel
  • 151
  • 1
  • 14
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 06 '22 at 15:20
  • This solution looks really simple. I even looked at the Github issue. But probably not so detailed:-) Thanks for the contribution. – ebiondi May 14 '22 at 05:15
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();
}
DRapp
  • 47,638
  • 12
  • 72
  • 142
  • Thank you very much for providing this code. It may leed me to the right way. There are however some obstacles. ` CanExecute` initially disables my Button because initially _isMyTaskRunning is false. In addition I wonder that such a ordinary topic leads to efforts like this. – ebiondi Feb 02 '22 at 06:31
  • @ebiondi, oops... changed to NOT in the canExecute call ( !_isMyTaskRunning ). As for efforts like this... not always, but its sometimes hard to encapsulate and it was mostly a snippet of existing code I just tacked on the extra _isMyTaskRunning portion. – DRapp Feb 02 '22 at 12:50
  • Thank you again! Now I can push the button but I get a `System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'` in `RaiseCanExecuteChanged()`. Any ideas? – ebiondi Feb 02 '22 at 14:24
  • @ebiondi, it is probably due to the separate task thread not being the same as the UI thread. So, see revision in the ButtonTaskIsComplete() method using the dispatcher to invoke the refresh. – DRapp Feb 02 '22 at 16:02
  • Thank you for your patience. In the meantime, I have uploaded the code https://github.com/gitiondi/CanExecute. But now the execute method is not called anymore:-( The current (2nd) commit uses MyRelayCommand. – ebiondi Feb 02 '22 at 17:36
  • @ebiondi, did not change the Execute event, just the result of the ContinueWith component. Should not have had an impact for the initial calling. – DRapp Feb 02 '22 at 17:43
  • Yes I agree in theory. But when I comment the ContinueWith part it works again. – ebiondi Feb 02 '22 at 18:08
  • @ebiondi, see revision for creating an execute method that allows for extra ACTION parameter to act as a callback before IT finishes, thus resetting the flag/refresh. – DRapp Feb 02 '22 at 19:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/241675/discussion-between-ebiondi-and-drapp). – ebiondi Feb 03 '22 at 05:23