Reputation: 117
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?
Upvotes: 0
Views: 343
Reputation: 9703
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.)
Upvotes: 2
Reputation: 9234
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.
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;
}
});
}
Upvotes: 0