在MVVM中使用异步/等待而不使用Void方法

6
我想在我的Windows Phone 8 MVVM项目中使用async/await,并且我正在努力寻找一种好的方法来使用这个API实现我的ICommands。 我已经阅读了一些关于此主题的文章,其中我看到了下面这篇来自MSDN的文章,它指出我必须避免使用async voids,因为很难捕获未处理的异常: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx 在另一个问题中,我提到了这个问题,有人也说我不应该使用async voids,除非是用于事件。
但问题在于,我在互联网上找到的所有示例都使用async voids。 我找到的这两篇文章就是例子: http://richnewman.wordpress.com/2012/12/03/tutorial-asynchronous-programming-async-and-await-for-beginners/http://blog.mycupof.net/2012/08/23/mvvm-asyncdelegatecommand-what-asyncawait-can-do-for-uidevelopment/ 最后一个是使用async/await实现ICommand的一个例子,但它也使用了async voids。 我正在尝试想出一个解决方案,所以我写了这个基于RelayCommand的ICommand实现:
public delegate Task AsyncAction();

public class RelayCommandAsync : ICommand
{
    private AsyncAction _handler;
    public RelayCommandAsync(AsyncAction handler)
    {
        _handler = handler;
    }

    private bool _isEnabled;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set
        {
            if (value != _isEnabled)
            {
                _isEnabled = value;
                if (CanExecuteChanged != null)
                {
                    CanExecuteChanged(this, EventArgs.Empty);
                }
            }
        }
    }

    public bool CanExecute(object parameter)
    {
        return IsEnabled;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        ExecuteAsync();
    }

    private Task ExecuteAsync()
    {
        return _handler();
    }
}

我正在尝试像这样使用它: 在构造函数中:

saveCommand = new RelayCommandAsync(SaveSourceAsync);

然后:

private async Task SaveSourceAsync()
{
    await Task.Run(() => { Save(); });
}

private void Save()
{
    // Slow operation
}

问题在于我对这种实现方式以及其他实现方式都不感到舒适,因为我不知道哪种是最好和最优的。

有人能够指导我如何使用它吗,最好是用MVVM模式?

1个回答

22
在引用的文章中,我指出了ICommand.Execute实际上就是一个事件处理程序,因此它应该被视为“避免使用async void”指南的例外:

总之,你应该优先选择async Task而非async void。这个指南的例外是异步事件处理程序,必须返回void。这个例外包括那些逻辑上是事件处理程序但实际上不是事件处理程序(例如,ICommand.Execute实现)的方法。

关于你的ICommand实现,实际上通过使用async void引入了一个缺陷:ICommand.Execute实现将丢弃Task而不观察其异常。因此,该实现将忽略由async委托引发的任何异常。
相比之下,你链接的博客文章具有async void ICommand.Execute的实现,它await Task,从而允许异常传播到UI同步上下文。在这种情况下,这是期望的行为,因为当同步ICommand.Execute引发异常时,你会得到相同的行为。

如果您有兴趣,我希望您尝试一下我为可能将来包含在我的AsyncEx库中编写的一个或两个ICommand第一个非常类似于您发布的博客中的简单命令。 第二个是一个更完整的“异步命令”实现,包括取消、进度报告和CanExecute的自动管理。 我会感激任何反馈意见。


我正在查看你的库和实现。那么告诉我一些事情,对于我在这里发布的博客文章中的ICommand实现,如果发生异常,它是否会在Windows Phone App.xaml中引发未处理的异常事件?我稍后会进行测试。 - Eric.M
我正在研究SimpleAsyncCommand,看起来它符合我的需求,我只想知道:我的ExecuteAsync方法将要调用的方法将在UI上下文中运行,我需要在这个方法中使用await Task.Run,对吗?这样做会丢失任何异常吗?如果不会,我想我会使用你的库! - Eric.M
ICommand的异常将在WP UI上引发,因此它们将被发送到Application.UnhandledExceptionExecuteAsync将在UI线程上执行(因此您的async委托将在UI线程上开始运行)。如果您需要为该命令执行阻塞工作,则可以使用await Task.Run;如果您有异步工作要做,则不需要Task.Run,只需使用await即可。实际上,它还不是我的库的一部分;您现在只需复制源代码。 - Stephen Cleary
哦,如果我有一个像将数据保存到数据库这样的操作,我就不需要使用Task.Run了吗? 比如,如果我将保存数据的代码放在返回Task的方法中并等待它,那么它不会阻塞UI吗?Task.Run只用于像访问Web服务这样真正密集的操作吗?如果是这样的话,那太好了!哈哈哈 =) - Eric.M
1
数据库现在有些棘手。它们本应该是自然异步的,但许多数据库API尚不支持(这一点)。因此,如果您的DB API中有某种SaveChangesAsync或其他内容,则可以直接await它,无需使用Task.Run。Web服务具有更好的异步支持,例如,HttpClient具有GetAsync等功能,您可以直接awaitTask.Run实际上是用于CPU密集型(而非I/O绑定)代码,但如果需要,它也可以用于使同步API看起来异步。 - Stephen Cleary
显示剩余2条评论

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