[英]Asyncronous ICommand implementation with optional canExecute
I have following ICommand implementation, which works great, but I want to expand it so I can pass external canExecute parameter我有以下 ICommand 实现,效果很好,但我想扩展它,以便我可以传递外部 canExecute 参数
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?能否以某种方式扩展此实现,以便当调用者的 CanExecute() 更改时, Execute1AsyncCommand 和 Execute2AsyncCommand 都会承认这一点? 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.在其他调用者中,我仍然希望能够使用
AsyncRelayCommand()
,而只是callback
是强制性的。 In this case CanExecute
should be evaluated internally from AsyncRelayCommand
as in my original implementation.在这种情况下
CanExecute
应该从内部评估AsyncRelayCommand
在我原来的执行。
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:和 ObservableTask 类:
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!感谢大家的宝贵帮助!
I have some ready-to-use solution.我有一些现成的解决方案。
RelayCommand
.RelayCommand
。CanExecute
is false
while command is executing, thus it will disable the control automatically.CanExecute
为false
,因此会自动禁用控制。 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 .注意:您无法处理
ObservableCollection
从非UI线程,作为解决办法,我建议这一个。
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);
}));
If your Caller
had a method called CanExecute
like this:如果您的
Caller
有一个名为CanExecute
的方法,如下所示:
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:然后你就可以将它作为委托类型
Func<bool>
的实例传递给你的AsyncRelayCommand
,当然,如果你的AsyncRelayCommand
定义了带有所需参数的构造函数:
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.因此,您的
AsyncRelayCommand
将能够调用canExecute
委托并获得实际结果。
Or you can leave CanExecute
as the property, but when you create AsyncRelayCommand
, wrap it to the lambda expression like this或者您可以将
CanExecute
作为属性保留,但是当您创建AsyncRelayCommand
,将其包装到这样的 lambda 表达式中
MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, () => CanExecute, ErrorHandler);
To apply fallback logic to your CanExecute
for AsyncRelayCommand
you can change the code in the following way:要将回退逻辑应用于
CanExecute
的AsyncRelayCommand
您可以通过以下方式更改代码:
Func<bool>
called, let's say, _canExecute
.Func<bool>
类型的实例变量被调用,比方说, _canExecute
。 Then assign it in the constructor with whatever value accepted as the argument Func<bool> canExecute
even if it's null
.Func<bool> canExecute
的null
即使它是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.public CanExecute(object param)
只需检查_canExecute
是否为null
,只需返回!IsExecuting
就可以了,如果它不是null
,则返回_canExecute
返回的任何_canExecute
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.