wpf MVVM 异步等待模式

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38717533/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 13:53:34  来源:igfitidea点击:

MVVM async await pattern

c#wpfasynchronousmvvmasync-await

提问by waxingsatirical

I've been trying to write an MVVM screen for a WPF application, using the async & await keywords to write asynchronous methods for 1. Initially loading data, 2. Refreshing the data, 3. Saving changes and then refreshing. Although I have this working, the code is very messy and I can't help thinking that there must be a better implementation. Can anyone advise on a simpler implementation?

我一直在尝试为 WPF 应用程序编写 MVVM 屏幕,使用 async & await 关键字为 1. 最初加载数据,2. 刷新数据,3. 保存更改然后刷新编写异步方法。虽然我有这个工作,但代码非常混乱,我不禁想到必须有更好的实现。任何人都可以就更简单的实现提出建议吗?

This is a cut-down version of my ViewModel:

这是我的 ViewModel 的精简版:

public class ScenariosViewModel : BindableBase
{
    public ScenariosViewModel()
    {
        SaveCommand = new DelegateCommand(async () => await SaveAsync());
        RefreshCommand = new DelegateCommand(async () => await LoadDataAsync());
    }

    public async Task LoadDataAsync()
    {
        IsLoading = true; //synchronously set the busy indicator flag
        await Task.Run(() => Scenarios = _service.AllScenarios())
            .ContinueWith(t =>
            {
                IsLoading = false;
                if (t.Exception != null)
                {
                    throw t.Exception; //Allow exception to be caught on Application_UnhandledException
                }
            });
    }

    public ICommand SaveCommand { get; set; }
    private async Task SaveAsync()
    {
        IsLoading = true; //synchronously set the busy indicator flag
        await Task.Run(() =>
        {
            _service.Save(_selectedScenario);
            LoadDataAsync(); // here we get compiler warnings because not called with await
        }).ContinueWith(t =>
        {
            if (t.Exception != null)
            {
                throw t.Exception;
            }
        });
    }
}

IsLoading is exposed to the view where it is bound to a busy indicator.

IsLoading 暴露给绑定到繁忙指示器的视图。

LoadDataAsync is called by the navigation framework when the screen is first viewed, or when a refresh button is pressed. This method should synchronously set IsLoading, then return control to the UI thread until the service has returned the data. Finally throwing any exceptions so they can be caught by the global exception handler (not up for discussion!).

当第一次查看屏幕或按下刷新按钮时,导航框架会调用 LoadDataAsync。此方法应同步设置 IsLoading,然后将控制权返回给 UI 线程,直到服务返回数据。最后抛出任何异常,以便它们可以被全局异常处理程序捕获(不讨论!)。

SaveAync is called by a button, passing updated values from a form to the service. It should synchronously set IsLoading, asynchronously call the Save method on the service and then trigger a refresh.

SaveAync 由按钮调用,将更新的值从表单传递到服务。应该同步设置IsLoading,异步调用服务的Save方法,然后触发刷新。

回答by Stephen Cleary

There are a few problems in the code that jump out to me:

代码中有几个问题让我跳出来:

  • Usage of ContinueWith. ContinueWithis a dangerous API (it has a surprising default value for its TaskScheduler, so it should really only be used if you specify a TaskScheduler). It's also just plain awkward compared to the equivalent awaitcode.
  • Setting Scenariosfrom a thread pool thread. I always follow the guideline in my code that data-bound VM properties are treated as part of the UI and must only be accessed from the UI thread. There are exceptions to this rule (particularly on WPF), but they're not the same on every MVVM platform (and are a questionable design to begin with, IMO), so I just treat VMs as part of the UI layer.
  • Where the exceptions are thrown. According to the comment, you want exceptions raised to Application.UnhandledException, but I don't think this code will do that. Assuming TaskScheduler.Currentis nullat the start of LoadDataAsync/SaveAsync, then the re-raising exception code will actually raise the exception on a thread poolthread, not the UIthread, thus sending it to AppDomain.UnhandledExceptionrather than Application.UnhandledException.
  • How the exceptions are re-thrown. You'll lose your stack trace.
  • Calling LoadDataAsyncwithout an await. With this simplified code, it'll probably work, but it does introduce the possibility of ignoring unhandled exceptions. In particular, if any of the synchronous part of LoadDataAsyncthrows, then that exception would be silently ignored.
  • 的用法ContinueWithContinueWith是一个危险的 API(它的 有一个令人惊讶的默认值TaskScheduler,所以它应该只在您指定 a 时才使用TaskScheduler)。与等效await代码相比,它也很尴尬。
  • Scenarios从线程池线程设置。我始终遵循代码中的准则,即数据绑定的 VM 属性被视为 UI 的一部分,并且只能从 UI 线程访问。这个规则有一些例外(特别是在 WPF 上),但它们在每个 MVVM 平台上都不相同(并且一开始是一个有问题的设计,IMO),所以我只是将 VM 视为 UI 层的一部分。
  • 抛出异常的地方。根据评论,您希望将异常提升到Application.UnhandledException,但我认为这段代码不会这样做。假设TaskScheduler.Currentnull在开始LoadDataAsync/ SaveAsync,然后再提高异常代码实际上将提高对异常线程池中的线程,而不是UI线程,从而将其发送到AppDomain.UnhandledException,而不是Application.UnhandledException
  • 如何重新抛出异常。您将丢失堆栈跟踪。
  • 调用LoadDataAsync没有await。使用这个简化的代码,它可能会工作,但它确实引入了忽略未处理异常的可能性。特别是,如果任何同步部分LoadDataAsync抛出,那么该异常将被静默忽略。

Instead of messing around with the manual-exception-rethrows, I recommend just using the more natural approach of exception propagation through await:

我建议不要使用手动异常重新抛出,而是使用更自然的异常传播方法await

  • If an asynchronous operation fails, the task gets an exception placed on it.
  • awaitwill examine this exception, and re-raise it in a proper way (preserving the original stack trace).
  • async voidmethods do not have a task on which to place an exception, so they will re-raise it directly on their SynchronizationContext. In this case, since your async voidmethods run on the UI thread, the exception will be sent to Application.UnhandledException.
  • 如果异步操作失败,则任务会在其上放置异常。
  • await将检查此异常,并以适当的方式重新引发它(保留原始堆栈跟踪)。
  • async void方法没有可以放置异常的任务,因此它们将直接在其SynchronizationContext. 在这种情况下,由于您的async void方法在 UI 线程上运行,因此异常将发送到Application.UnhandledException.

(the async voidmethods I'm referring to are the asyncdelegates passed to DelegateCommand).

async void我所指的方法是async传递给的委托DelegateCommand)。

The code now becomes:

代码现在变成:

public class ScenariosViewModel : BindableBase
{
  public ScenariosViewModel()
  {
    SaveCommand = new DelegateCommand(async () => await SaveAsync());
    RefreshCommand = new DelegateCommand(async () => await LoadDataAsync());
  }

  public async Task LoadDataAsync()
  {
    IsLoading = true;
    try
    {
      Scenarios = await Task.Run(() => _service.AllScenarios());
    }
    finally
    {
      IsLoading = false;
    }
  }

  private async Task SaveAsync()
  {
    IsLoading = true;
    await Task.Run(() => _service.Save(_selectedScenario));
    await LoadDataAsync();
  }
}

Now all the problems have been resolved:

现在所有问题都已解决:

  • ContinueWithhas been replaced with the more appropriate await.
  • Scenariosis set from the UI thread.
  • All exceptions are propagated to Application.UnhandledExceptionrather than AppDomain.UnhandledException.
  • Exceptions maintain their original stack trace.
  • There are no un-await-ed tasks, so all exceptions will be observed some way or another.
  • ContinueWith已经换成更合适的了await
  • Scenarios从 UI 线程设置。
  • 所有异常都传播到Application.UnhandledException而不是AppDomain.UnhandledException
  • 异常保持其原始堆栈跟踪。
  • 没有未await完成的任务,因此将以某种方式观察所有异常。

And the code is cleaner, too. IMO. :)

而且代码也更简洁。海事组织。:)