【问题标题】:Asyncronous ICommand implementation with optional canExecute带有可选 canExecute 的异步 ICommand 实现
【发布时间】:2021-01-18 03:26:49
【问题描述】:

我有以下 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;
        }
    }

能否以某种方式扩展此实现,以便当调用者的 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;
    }
}
}

在其他呼叫者中,我仍然希望能够使用AsyncRelayCommand(),而只有callback 是强制性的。在这种情况下,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 类应该如何知道调用者发生了什么?调用者有责任更新命令的状态。例如,您可以在 SomeConditionOtherCondition 更改时设置 IsExecuting 属性。否则,命令实现必须订阅对此属性的更改。
  • @mm8 这个订阅可以在 AsyncRelayCommand 中实现吗?
  • 为什么在运行任务时使用.ConfigureAwait(false);?这实际上意味着await 下面的代码可能会在随机线程池线程上执行,而不是在 UI 线程上执行。我很确定,但对我来说,似乎可能会发生比赛情况
  • 好点,谢谢。更改为 .ConfigureAwait(true)
  • 我根本不会写ConfigureAwait,因为它默认捕获同步上下文。所以我个人认为,即使是true,打电话也会更令人困惑。

标签: c# wpf mvvm


【解决方案1】:

我有一些现成的解决方案。

  • 常规同步委托,因此可以替换简单的RelayCommand
  • 委托在池线程上执行。
  • CanExecute 在命令执行时是false,因此它会自动禁用控件。

实施

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);
}

用法

private IAsyncCommand _myAsyncCommand;

public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(parameter =>
{
    Thread.Sleep(2000);
}));

注意:您不能处理来自非 UI 线程的 ObservableCollection,作为解决方法,我建议使用 one


异步委托版本

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);
}

用法

private IAsyncCommand _myAsyncCommand;

public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(async parameter =>
{
    await Task.Delay(2000);
}));

【讨论】:

  • 感谢 aepot,这几乎可以正常工作。有两个问题。任务完成后,UI 不会自动刷新。我必须单击或轻推窗口以使按钮重新启用。此外,如果我选择退出提供外部 CanExecute new AsyncRelayCommand2(p =&gt; Execute2Async(p)),我可以在任务已经运行时粉碎该按钮并多次启动执行。
  • @Pawel 啊,我明白了问题所在。在这里你不需要用异步的东西包装代码。直接在命令中执行工作,因为你可以Thread.Sleep 没有包装器。尽可能简单。尝试将Execute2Async 更改为Thread.Sleep(2000)。它将按预期工作。
  • 好的,有一些进展:) 我宁愿用Func&lt;object, Task&gt; execute 替换Action&lt;object&gt; executeExecuteAsync 中的await _execute(parameter);。我仍在对此进行测试,但您的解决方案看起来很有希望
  • @Pawel 你真的需要异步委托吗?为什么?里面会有什么样的工作?一些 I/O 绑定的东西作为网络请求?究竟是什么?如果是,您可以使用Func&lt;Task&gt; 并在没有Task.Run() 的情况下执行,但我不确定它是否真的需要。
  • @Pawel 添加了async 代表版本。但我仍然认为这是一种冗余。 new AsyncRelayCommand2(async p =&gt; await Execute2Async(p))
【解决方案2】:

如果您的 Caller 有一个名为 CanExecute 的方法,如下所示:

 private bool CanExecute()
 {
     return SomeCondition && OtherCondition;
 }

然后你可以将它作为委托类型Func&lt;bool&gt; 的实例传递给你的AsyncRelayCommand,当然,如果你的AsyncRelayCommand 定义了带有所需参数的构造函数:

    public AsyncRelayCommand(Func<object, Task> callback, Func<bool> canExecute, Action<Exception> onException = null)
    {
        this.callback = callback;
        this.onException = onException;
        this.canExecute = canExecute;
    }

然后你像这样将它传递给构造函数:

MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, CanExecute, ErrorHandler);

因此,您的 AsyncRelayCommand 将能够调用 canExecute 委托并获得实际结果。

或者你可以保留CanExecute作为属性,但是当你创建AsyncRelayCommand时,像这样将它包装到lambda表达式中

MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, () => CanExecute, ErrorHandler);

要将后备逻辑应用于 CanExecuteAsyncRelayCommand,您可以通过以下方式更改代码:

  • 有一个类型为Func&lt;bool&gt; 的实例变量称为_canExecute。然后在构造函数中将其分配为参数Func&lt;bool&gt; canExecute 接受的任何值,即使它是null。然后在您的public CanExecute(object param) 中检查_canExecute 是否为null,只需像现在一样返回!IsExecuting,如果不是null,则返回任何_canExecute

【讨论】:

  • 感谢 E. Shcherbo。调用者的 CanExecute 可以是我认为的一种方法。但是,我希望 AsyncRelayCommand canExecute 是可选的,因此当省略 AsyncRelayCommand 时,它可以像当前一样在内部评估它自己的 CanExecute。因此,如果我像 Func&lt;bool&gt; canExecute = null 这样更改签名,如果不是 null,我应该如何处理这个 canExecute func?
  • 我可以看到几种解决方法。其中一种方法是:你仍然会有一个Func&lt;bool&gt;的实例变量来保存canExecute委托对象,那么你的CanExecute属性可以像这样实现public bool CanExecute =&gt; canExecute == null ? !IsExecuting : canExecute();
  • 您的意思是CanExecute() 方法,对吗?我想我已经尝试过了,但没有成功public bool CanExecute(object parameter) =&gt; canExecute == null ? !IsExecuting : canExecute(); 那是因为调用者的ExecuteAsync 在进入时设置SomeCondition = false; 并在退出时返回true。调用者的 CanLoad() 在我单击按钮后被调用,但在 SomeCondition = false; 之前,所以 SomeCondition 始终为真
  • 感谢 E. Shcherbo。我编辑了问题并提供了更多详细信息
  • 这个解决方案几乎可以完美运行。我可以粉碎两个按钮,如果任一任务已经在运行,任务将无法启动,这正是我想要的。但是,如果任一任务正在运行,我还想要禁用按钮。目前,当任务运行时,两个按钮都保持启用状态,只是不起作用。
猜你喜欢
  • 2017-07-31
  • 2013-10-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-30
  • 2013-01-01
  • 1970-01-01
相关资源
最近更新 更多