我有以下的ICommand实现,它很好用,但我想扩展它,让我能够传递外部的canExecute参数。
在其他调用程序中,我仍然希望能够只使用必选的 `callback` 使用 `AsyncRelayCommand()`。在这种情况下,`CanExecute` 应该从 `AsyncRelayCommand` 在内部求值,就像我的原始实现一样。
为了完整起见,以下是我的看法:
我想要实现的是用户按下一个按钮后,直到所有任务完成前,该按钮和另一个按钮都将变为禁用状态。
解决方案
我最终采用了以下实现方式,目前看来已经按照预期工作:
谢谢大家提供的非常宝贵的帮助!
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;
}
}
这个实现能否以一种方式扩展,使得当调用者的CanExecute()改变时,Execute1AsyncCommand和Execute2AsyncCommand都能意识到?这是我的调用者类:
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;
}
}
}
在其他调用程序中,我仍然希望能够只使用必选的 `callback` 使用 `AsyncRelayCommand()`。在这种情况下,`CanExecute` 应该从 `AsyncRelayCommand` 在内部求值,就像我的原始实现一样。
为了完整起见,以下是我的看法:
<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>
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;
}
}
}
}
我想要实现的是用户按下一个按钮后,直到所有任务完成前,该按钮和另一个按钮都将变为禁用状态。
解决方案
我最终采用了以下实现方式,目前看来已经按照预期工作:
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);
}
使用方法:
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;
}
}
}
谢谢大家提供的非常宝贵的帮助!
AsyncRelayCommand
类如何知道调用者中发生了什么?更新命令状态是调用者的责任。例如,您可以在SomeCondition
或OtherCondition
更改时设置IsExecuting
属性。否则,命令实现必须订阅这些属性的更改。 - mm8.ConfigureAwait(false);
?实际上,这意味着await
下面的代码可能会在随机的线程池线程上执行,而不是在UI线程上执行。我不确定,但对我来说似乎可能会发生竞态条件。 - E. ShcherboConfigureAwait
,因为它默认会捕获同步上下文。所以我个人认为即使加上true
后缀,这个调用也更加令人困惑。 - E. Shcherbo