Reputation: 1297
I have following ICommand implementation, which works great, but I want to expand it so I can pass external canExecute parameter
public class AsyncRelayCommand : ICommand
{
private readonly Func<object, Task> callback;
private readonly Action<Exception> onException;
private bool isExecuting;
public bool IsExecuting
{
get => isExecuting;
set
{
isExecuting = value;
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
public event EventHandler CanExecuteChanged;
public AsyncRelayCommand(Func<object, Task> callback, Action<Exception> onException = null)
{
this.callback = callback;
this.onException = onException;
}
public bool CanExecute(object parameter) => !IsExecuting;
public async void Execute(object parameter)
{
IsExecuting = true;
try
{
await callback(parameter);
}
catch (Exception e)
{
onException?.Invoke(e);
}
IsExecuting = false;
}
}
Can this implementation be extended in a way so when caller's CanExecute() changes, both Execute1AsyncCommand and Execute2AsyncCommand will acknowledge that? Here is my caller class:
public class Caller : ObservableObject
{
public ObservableTask Execute1Task { get; } = new ObservableTask();
public ObservableTask Execute2Task { get; } = new ObservableTask();
public ICommand Execute1AsyncCommand { get; }
public ICommand Execute2AsyncCommand { get; }
public Caller()
{
Execute1AsyncCommand = new AsyncRelayCommand(Execute1Async);
Execute2AsyncCommand = new AsyncRelayCommand(Execute2Async);
}
private bool CanExecute(object o)
{
return Task1?.Running != true && Task2?.Running != true;
}
private async Task Execute1Async(object o)
{
Task1.Running = true;
try
{
await Task.Run(()=>Thread.Sleep(2000)).ConfigureAwait(true);
Task1.RanToCompletion = true;
}
catch (Exception e)
{
Task1.Faulted = true;
}
}
private async Task Execute2Async(object o)
{
Task2.Running = true;
try
{
await Task.Run(() => Thread.Sleep(2000)).ConfigureAwait(true);
Task2.RanToCompletion = true;
}
catch (Exception e)
{
Task2.Faulted = true;
}
}
}
In other callers I still want to be able to use AsyncRelayCommand()
with just callback
being mandatory. In this case CanExecute
should be evaluated internally from AsyncRelayCommand
as in my original implementation.
For completeness, here is my view:
<StackPanel>
<Button Content="Execute Task 1"
Command="{Binding Execute1AsyncCommand}" />
<Button Content="Execute Task 2"
Command="{Binding Execute2AsyncCommand}" />
<TextBlock Text="Task 1 running:" />
<TextBlock Text="{Binding Task1.Running}" />
<TextBlock Text="Task 2 running:" />
<TextBlock Text="{Binding Task2.Running}" />
</StackPanel>
And ObservableTask class:
public class ObservableTask : ObservableObject
{
private bool running;
private bool ranToCompletion;
private bool faulted;
public Task Task { get; set; }
public bool WaitingForActivation => !Running && !RanToCompletion && !Faulted;
public bool Running
{
get => running;
set
{
running = value;
if (running)
{
RanToCompletion = false;
Faulted = false;
}
}
}
public bool RanToCompletion
{
get => ranToCompletion;
set
{
ranToCompletion = value;
if (ranToCompletion)
{
Running = false;
}
}
}
public bool Faulted
{
get => faulted;
set
{
faulted = value;
if (faulted)
{
Running = false;
}
}
}
}
What I want to achieve is after user press one button both become disabled until all tasks are done.
Solution
I ended up with the following implementation which so far works as intended:
public class AsyncRelayCommand : ICommand
{
private bool isExecuting;
private readonly Func<object, Task> execute;
private readonly Predicate<object> canExecute;
private readonly Action<Exception, object> onException;
private Dispatcher Dispatcher { get; }
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public AsyncRelayCommand(Func<object, Task> execute, Predicate<object> canExecute = null, Action<Exception, object> onException = null)
{
this.execute = execute;
this.canExecute = canExecute;
this.onException = onException;
Dispatcher = Application.Current.Dispatcher;
}
private void InvalidateRequerySuggested()
{
if (Dispatcher.CheckAccess())
CommandManager.InvalidateRequerySuggested();
else
Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
}
public bool CanExecute(object parameter) => !isExecuting && (canExecute == null || canExecute(parameter));
private async Task ExecuteAsync(object parameter)
{
if (CanExecute(parameter))
{
try
{
isExecuting = true;
InvalidateRequerySuggested();
await execute(parameter);
}
catch (Exception e)
{
onException?.Invoke(e, parameter);
}
finally
{
isExecuting = false;
InvalidateRequerySuggested();
}
}
}
public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}
Usage:
public class Caller: ObservableObject
{
public ObservableTask Task1 { get; } = new ObservableTask();
public ObservableTask Task2 { get; } = new ObservableTask();
public ObservableTask Task3 { get; } = new ObservableTask();
public ICommand Execute1AsyncCommand { get; }
public ICommand Execute2AsyncCommand { get; }
public ICommand Execute3AsyncCommand { get; }
public Caller()
{
// Command with callers CanExecute method and error handled by callers method.
Execute1AsyncCommand = new AsyncRelayCommand(Execute1Async, CanExecuteAsMethod, Execute1ErrorHandler);
// Command with callers CanExecute parameter and error handled inside task therefore not needed.
Execute2AsyncCommand = new AsyncRelayCommand(Execute2Async, _=>CanExecuteAsParam);
// Some other, independent command.
// Minimum example - CanExecute is evaluated inside command, error handled inside task.
Execute3AsyncCommand = new AsyncRelayCommand(Execute3Async);
}
public bool CanExecuteAsParam => !(Task1.Running || Task2.Running);
private bool CanExecuteAsMethod(object o)
{
return !(Task1.Running || Task2.Running);
}
private async Task Execute1Async(object o)
{
Task1.Running = true;
await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
Task1.RanToCompletion = true;
}
private void Execute1ErrorHandler(Exception e, object o)
{
Task1.Faulted = true;
}
private async Task Execute2Async(object o)
{
try
{
Task2.Running = true;
await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
Task2.RanToCompletion = true;
}
catch (Exception e)
{
Task2.Faulted = true;
}
}
private async Task Execute3Async(object o)
{
try
{
Task3.Running = true;
await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
Task3.RanToCompletion = true;
}
catch (Exception e)
{
Task3.Faulted = true;
}
}
}
Thank you everybody for invaluable help!
Upvotes: 4
Views: 1566
Reputation: 4824
I have some ready-to-use solution.
RelayCommand
.CanExecute
is false
while command is executing, thus it will disable the control automatically.Implementation
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync(object param);
}
public class AsyncRelayCommand : IAsyncCommand
{
private bool _isExecuting;
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
private Dispatcher Dispatcher { get; }
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public AsyncRelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
Dispatcher = Application.Current.Dispatcher;
}
private void InvalidateRequerySuggested()
{
if (Dispatcher.CheckAccess())
CommandManager.InvalidateRequerySuggested();
else
Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
}
public bool CanExecute(object parameter) => !_isExecuting && (_canExecute == null || _canExecute(parameter));
public async Task ExecuteAsync(object parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
InvalidateRequerySuggested();
await Task.Run(() => _execute(parameter));
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
finally
{
_isExecuting = false;
InvalidateRequerySuggested();
}
}
}
public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}
Usage
private IAsyncCommand _myAsyncCommand;
public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(parameter =>
{
Thread.Sleep(2000);
}));
Note: you can't deal with ObservableCollection
from non-UI Thread, as workaround I suggest this one.
Asynchronous delegate version
public class AsyncRelayCommand : IAsyncCommand
{
private bool _isExecuting;
private readonly Func<object, Task> _executeAsync;
private readonly Predicate<object> _canExecute;
private Dispatcher Dispatcher { get; }
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public AsyncRelayCommand(Func<object, Task> executeAsync, Predicate<object> canExecute = null)
{
_executeAsync = executeAsync;
_canExecute = canExecute;
Dispatcher = Application.Current.Dispatcher;
}
private void InvalidateRequerySuggested()
{
if (Dispatcher.CheckAccess())
CommandManager.InvalidateRequerySuggested();
else
Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
}
public bool CanExecute(object parameter) => !_isExecuting && (_canExecute == null || _canExecute(parameter));
public async Task ExecuteAsync(object parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
InvalidateRequerySuggested();
await _executeAsync(parameter);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
finally
{
_isExecuting = false;
InvalidateRequerySuggested();
}
}
}
public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}
Usage
private IAsyncCommand _myAsyncCommand;
public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(async parameter =>
{
await Task.Delay(2000);
}));
Upvotes: 2
Reputation: 1148
If your Caller
had a method called CanExecute
like this:
private bool CanExecute()
{
return SomeCondition && OtherCondition;
}
Then you would be able to pass it to your AsyncRelayCommand
as an instance of delegate type Func<bool>
, of course, if your AsyncRelayCommand
defined constructor with the needed parameter:
public AsyncRelayCommand(Func<object, Task> callback, Func<bool> canExecute, Action<Exception> onException = null)
{
this.callback = callback;
this.onException = onException;
this.canExecute = canExecute;
}
Then you pass it to the constructor like this:
MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, CanExecute, ErrorHandler);
Thus, your AsyncRelayCommand
would be able to invoke canExecute
delegate and will get the actual results.
Or you can leave CanExecute
as the property, but when you create AsyncRelayCommand
, wrap it to the lambda expression like this
MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, () => CanExecute, ErrorHandler);
To apply fallback logic to your CanExecute
for AsyncRelayCommand
you can change the code in the following way:
Func<bool>
called, let's say, _canExecute
. Then assign it in the constructor with whatever value accepted as the argument Func<bool> canExecute
even if it's null
. Then in your public CanExecute(object param)
just check if _canExecute
is null
, just return !IsExecuting
as you're doing it now, if it's not null
, then return whatever _canExecute
return.Upvotes: 1