简体   繁体   English

MVVM异步等待模式

[英]MVVM async await pattern

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. 我一直在尝试为WPF应用程序编写MVVM屏幕,使用async和await关键字为1编写异步方法。最初加载数据,2。刷新数据,3。保存更改然后刷新。 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? 任何人都可以建议更简单的实施?

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. 首次查看屏幕或按下刷新按钮时,导航框架将调用LoadDataAsync。 This method should synchronously set IsLoading, then return control to the UI thread until the service has returned the data. 此方法应同步设置IsLoading,然后将控制权返回给UI线程,直到服务返回数据。 Finally throwing any exceptions so they can be caught by the global exception handler (not up for discussion!). 最后抛出任何异常,以便它们可以被全局异常处理程序捕获(不用讨论!)。

SaveAync is called by a button, passing updated values from a form to the service. SaveAync由按钮调用,将更新的值从表单传递到服务。 It should synchronously set IsLoading, asynchronously call the Save method on the service and then trigger a refresh. 它应该同步设置IsLoading,异步调用服务上的Save方法,然后触发刷新。

There are a few problems in the code that jump out to me: 跳出来的代码中有一些问题:

  • Usage of ContinueWith . ContinueWith用法。 ContinueWith is a dangerous API (it has a surprising default value for its TaskScheduler , so it should really only be used if you specify a TaskScheduler ). ContinueWith是一个危险的API(它的TaskScheduler有一个令人惊讶的默认值,因此它应该只在你指定一个TaskScheduler )。 It's also just plain awkward compared to the equivalent await code. 与等效的await代码相比,它也只是简单的尴尬。
  • Setting Scenarios from a thread pool thread. 从线程池线程设置Scenarios 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. 我始终遵循我的代码中的指南,即数据绑定的VM属性被视为UI的一部分,并且只能从UI线程访问。 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. 这个规则有一些例外(特别是在WPF上),但它们在每个MVVM平台上都不一样(并且一开始就是一个值得怀疑的设计,IMO),所以我只是将虚拟机视为UI层的一部分。
  • 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. 根据评论,您希望将异常引发到Application.UnhandledException ,但我认为此代码不会这样做。 Assuming TaskScheduler.Current is null at the start of LoadDataAsync / SaveAsync , then the re-raising exception code will actually raise the exception on a thread pool thread, not the UI thread, thus sending it to AppDomain.UnhandledException rather than Application.UnhandledException . 假设在LoadDataAsync / SaveAsync开始时TaskScheduler.Currentnull ,则重新引发的异常代码实际上会在线程池线程而不是UI线程上引发异常,从而将其发送到AppDomain.UnhandledException而不是Application.UnhandledException
  • How the exceptions are re-thrown. 如何重新抛出异常。 You'll lose your stack trace. 你将失去堆栈跟踪。
  • Calling LoadDataAsync without an await . 在没有await情况下调用LoadDataAsync 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 LoadDataAsync throws, then that exception would be silently ignored. 特别是,如果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. 如果异步操作失败,则任务会在其上发生异常。
  • await will examine this exception, and re-raise it in a proper way (preserving the original stack trace). await将检查此异常,并以适当的方式重新提升它(保留原始堆栈跟踪)。
  • async void methods do not have a task on which to place an exception, so they will re-raise it directly on their SynchronizationContext . async void方法没有放置异常的任务,因此它们将直接在SynchronizationContext上重新引发它。 In this case, since your async void methods run on the UI thread, the exception will be sent to Application.UnhandledException . 在这种情况下,由于您的async void方法在UI线程上运行,因此异常将发送到Application.UnhandledException

(the async void methods I'm referring to are the async delegates passed to DelegateCommand ). (我所指的async void方法是传递给DelegateCommandasync委托)。

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: 现在所有问题都已解决:

  • ContinueWith has been replaced with the more appropriate await . ContinueWith已经被更合适的await所取代。
  • Scenarios is set from the UI thread. Scenarios是从UI线程设置的。
  • All exceptions are propagated to Application.UnhandledException rather than AppDomain.UnhandledException . 所有异常都传播到Application.UnhandledException而不是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. 有没有非await -ed任务,所以所有的异常会观察到一些这样或那样的。

And the code is cleaner, too. 而且代码也更清晰。 IMO. IMO。 :) :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM