0
votes

In my application I bound my buttons to commands with "MVVM". My command is implemented as follows:

        public Command CommandLoadStuff
        {
            get
            {
                return new Command(async () =>
                {
                    await DoLongStuff();
                });
            }
        }

The problem is that these commands are async and the user can click them multiple times causing the code to execute multiple times also.

As a first approach i used CanExecute:

        public Command CommandLoadStuff
        {
            get
            {
                return new Command(async () =>
                {
                    AppIsBusy = true;
                    await DoLongStuff();
                    AppIsBusy = false;
                },() => !AppIsBusy);
            }
        }

Now I wonder if there isn't a better way than to handle the CanExecute for each command individually.

Since I initialize the command every time with "new" I wonder if the class "Command" could not be extended accordingly. It should block a second click of the button during the lifespan with CanExecute (Posibly in the Constructor?) and release it after the execution of the command is finished. ( Possibly in the Dispose function?)

Is there a way to achieve this?

2

2 Answers

2
votes

Extending the class command this way is not possible, as far as I can tell, because Execute is non-virtual and you have to pass the execute action to the constructor. Anyway, there is still a way. Command derives from ICommand which has the following interface

public interface ICommand
{
    event EventHandler CanExecuteChanged;

    void Execute(object data);

    bool CanExecute(object data);
}

You could create a class AsyncBlockingCommand (or whatsoever) that will return will return the respective value from CanExecute depending on whether an async method is still running (I know that there are issues with async void methods, so handle with care)

public class AsyncBlockingCommand : ICommand
{
    bool _canExecute = true;
    Func<Task> _toExecute;

    public AsyncBlockingCommand(Func<Task> toExecute)
    {
        _toExecute = toExecute;
    }

    public event EventHandler CanExecuteChanged;

    public async void Execute(object data)
    {
        _canExecute = false;
        RaiseCanExecuteChanged();
        await _toExecute();
        _canExecute = true;
        RaiseCanExecuteChanged();
    }

    public bool CanExecute(object data) => _canExecute; 

    private void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

Before your async method is executed, _canExecute is set to false and CanExecuteChanged is raised. This way, your Button will get notified of CanExecute having changed and disable itself. Vice versa after the async method has been called. (Probably the RaiseCanExecuteChanged will have to be invoked on the main thread.)

0
votes

You can use IsEnabled property to make the Button cannot be clicked. Like following code.

<Button 
    Text="click"
    Command={Binding Button1Command}
    IsEnabled={Binding AreButtonsEnabled} />

If the value of IsEnabled is false, you can see this button, it is grey, if you click it, it will not execute any command.

enter image description here

Here is MyViewModel code.

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

public ICommand Button1Command { get; protected set; }

public MyViewModel()
{
    Button1Command = new Command(HandleButton1Tapped);
}

private void HandleButton1Tapped()
{
    // Run on the main thread, to make sure that it is getting/setting the proper value for AreButtonsEnabled
    // And note that calls to Device.BeginInvokeOnMainThread are queued, therefore
    // you can be assured that AreButtonsEnabled will be set to false by one button's command
    // before the value of AreButtonsEnabled is checked by another button's command.
    // (Assuming you don't change the value of AreButtonsEnabled on another thread)
    Device.BeginInvokeOnMainThread(async() => 
    {
        if (AreButtonsEnabled)
        {
            AreButtonsEnabled = false;
            // DoLongStuff code
            await  Task.Delay(2000);
            AreButtonsEnabled = true;
        }
    });
}