如何在DelegateCommand中使用异步方法

25

我想在Xamarin.Forms中的Prism框架中将异步方法链接到委托命令,我的问题是如何实现?

以下解决方案是否正确?是否存在任何陷阱?(死锁、UI缓慢或冻结、不良实践等)

{      // My view model constructor
       ... 
       MyCommand = new DelegateCommand(async () => await MyJobAsync());
       ...
}

private async Task MyJobAsync()
{
       ... // Some await calls
       ... // Some UI element changed such as binded Observable collections
}

@Haukinger 这真的是异步的吗?换句话说,如果我在 MyJobAsync 中有昂贵的性能成本代码,UI 线程不会阻塞并且可以平稳运行吗? - sorosh_sabz
我建议从这里开始:https://msdn.microsoft.com/en-us/magazine/dn630647.aspx 大多数流行的MVVM框架都是开源的,你可以查看它们如何解决问题。 - EvZ
1
如果您使用异步 API,则它是异步的。如果您需要进行 CPU 工作,请使用 Task.Run。 - Haukinger
1
@Nkosi,这些评论和答案为我的问题提供了有用的信息,我并不是在寻求代码审查,而是想知道如何在Prism中使用async方法以及该主题周围存在哪些最佳实践。 - sorosh_sabz
@Haukinger 在调用Command中的async方法时,是否有更好的解决方案(或最佳实践)? - sorosh_sabz
显示剩余4条评论
7个回答

17

你可以直接使用 async void。但是,根据我的经验,有几点需要注意...

你的代码结构是:启动异步操作,然后使用结果更新UI。这表明对于异步的数据绑定,你最好使用 NotifyTask<T> 这种方法,而不是命令。请参阅我的《异步MVVM数据绑定》文章,了解有关 NotifyTask<T> 设计背后的更多内容(但请注意,最新代码中已包含错误修复和其他增强功能)。

如果你真的需要异步的命令(这种情况比较少见),你可以直接使用 async void 或者像我在《异步MVVM命令》文章中所描述的那样构建异步命令类型。我还提供了支持此操作的类型,但这些API可能会有所改变。

如果你选择直接使用 async void

  • 请考虑使你的 async Task 逻辑公开,或至少可以访问你的单元测试。
  • 别忘了正确处理异常。正如纯 DelegateTask 一样,要正确处理任何来自委托的异常。

1
如果你真的需要一个异步命令(这种情况非常罕见),为什么你认为使用“异步命令”是一种罕见情况?如果我们将所有命令都设置为“异步”,会发生什么? - isxaker
根据我的经验,它们并不常见。通常,一个命令会同步启动一个异步操作,然后(异步地)更新UI。在这种情况下,命令本身实际上是同步的,并且只使用异步数据绑定来表示操作。 - Stephen Cleary
这非常有用,您能否更新此答案中的链接? - Colin

6

如果你正在使用Prism Library, 可以查看这个链接:https://prismlibrary.com/docs/commands/commanding.html#implementing-a-task-based-delegatecommand

如果你想将一个CommandParameter传递给DelegateCommand,可以在变量声明中使用以下语法。

public DelegateCommand<object> MyCommand { get; set; }

在ViewModel的构造函数中,按照以下方式进行初始化:
MyCommand = new DelegateCommand<object>(HandleTap);

其中 HandleTap 的声明如下:

private async void HandleTap(object param)

希望这能有所帮助。

5
正如已经提到的,使用委托命令处理异步代码的方法是使用async void。这已经远远超出了Prism或Xamarin Forms的讨论范围。底线是,Xamarin Forms的Command和Prism的DelegateCommand都受限于ICommandvoid Execute(object obj)。如果您想获取更多信息,我建议您阅读Brian Lagunas的博客,他解释了为什么DelegateCommand.FromAsync处理程序已过时
通常,大多数问题都可以通过更新代码来轻松解决。例如,我经常听到关于Exceptions的抱怨,认为这就是需要FromAsync的原因,但在他们的代码中,我看不到try catch。由于async void是“fire and forget”,我听到的另一个抱怨是命令可能会执行两次。这也很容易用DelegateCommandsObservesPropertyObservesCanExecute来解决。

为什么MVVM框架或Xamarin.Forms没有提供像IAsyncCommand或DelegateAsyncCommand这样的一些设施?这是完全无用的吗? - sorosh_sabz
@sorosh_sabz:IAsyncCommand 的主要目的是进行单元测试。直接使用 async void(而不公开 async Task 等效项)会阻止对 VM 进行简单的单元测试。 - Stephen Cleary

3

我认为在从同步执行的方法(ICommand.Execute)调用异步方法时存在两个主要问题:1)拒绝在上一个调用仍在运行时再次执行;2)异常处理。这两个问题都可以通过像下面这样的实现(原型)来解决。这将是委托命令的异步替代品。

public sealed class AsyncDelegateCommand : ICommand
{
    private readonly Func<object, Task> func;
    private readonly Action<Exception> faultHandlerAction;
    private int callRunning = 0;

    // Pass in the async delegate (which takes an object parameter and returns a Task) 
    // and a delegate which handles exceptions
    public AsyncDelegateCommand(Func<object, Task> func, Action<Exception> faultHandlerAction)
    {
        this.func = func;
        this.faultHandlerAction = faultHandlerAction;
    }

    public bool CanExecute(object parameter)
    {
        return callRunning == 0;
    }

    public void Execute(object parameter)
    {
        // Replace value of callRunning with 1 if 0, otherwise return - (if already 1).
        // This ensures that there is only one running call at a time.
        if (Interlocked.CompareExchange(ref callRunning, 1, 0) == 1)
        {
            return;
        }
        OnCanExecuteChanged();
        func(parameter).ContinueWith((task, _) => ExecuteFinished(task), null, TaskContinuationOptions.ExecuteSynchronously);
    }

    private void ExecuteFinished(Task task)
    {
        // Replace value of callRunning with 0
        Interlocked.Exchange(ref callRunning, 0);
        // Call error handling if task has faulted
        if (task.IsFaulted)
        {
            faultHandlerAction(task.Exception);
        }
        OnCanExecuteChanged();
    }

    public event EventHandler CanExecuteChanged;

    private void OnCanExecuteChanged()
    {
        // Raising this event tells for example a button to display itself as "grayed out" while async operation is still running
        var handler = CanExecuteChanged;
        if (handler != null) handler(this, EventArgs.Empty);
    }
}

异步无返回值

我个人会尽可能避免使用 "异步无返回值"。从外部无法知道操作何时完成,错误处理也变得复杂。例如,编写一个 "异步任务" 方法被一个 "异步无返回值" 方法调用时,几乎需要知道其失败的任务如何传播:

public async Task SomeLogic()
{
    var success = await SomeFurtherLogic();
    if (!success) 
    {
        throw new DomainException(..); // Normal thing to do
    }
}

接着,另一天有人写道:

public async void CommandHandler()
{
    await SomeLogic();  // Calling a method. Normal thing to do but can lead to an unobserved Task exception
}

1
是的,UI线程运行DelegateCommand。在异步情况下,它会一直运行,直到第一个await语句,然后恢复其常规UI线程工作。如果awaiter配置为捕获同步上下文(即不使用.ConfigureAwait(false)),则UI线程将继续在await之后运行DelegateCommand。
无论“await表达式”在后台线程、前台线程、线程池线程还是其他线程上运行,取决于您调用的API。例如,您可以使用Task.Run将CPU密集型工作推送到线程池中,也可以使用诸如Stream.ReadAsync之类的方法等待I/O操作而不使用任何线程。

-1
public ICommand MyCommand{get;set;}

//constructor
public ctor()
{
    MyCommand = new Xamarin.Forms.Command(CmdDoTheJob);
}

public async void DoTheJob()
{
    await TheMethod();
}

不,那不是我的答案,我想使用棱镜框架。不过,感谢你的回答。 - sorosh_sabz

-1
public DelegateCommand MyCommand => new DelegateCommand(MyMethod);

private async void MyMethod()
{

}

没有陷阱。异步方法中的void返回类型是专门为委托而创建的。如果您想更改对UI产生影响的内容,请在此块中插入相关代码:

Device.BeginOnMainThread(()=>
{
    your code;
});

实际上,ICommand和DelegateCommand非常相似,因此上面的回答是相当正确的。


1
为什么我必须使用 Device.BeginOnMainThread ?之前我一直认为 await 会捕获父上下文(我认为视图模型中的上下文是 UI 上下文),并在此上下文中运行代码块,所以为什么我不能直接在 async 方法中使用 UI 元素呢? - sorosh_sabz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接