MVNM async expects pattern

I am trying to write an MVVM screen for a WPF application using async keywords and expectations for writing asynchronous methods for 1. Initially loading the data, 2. Updating the data, 3. Saving the changes, and then updating. Although this works for me, the code is very dirty, and I cannot help but think that there should be a better implementation. Can anyone advise a simpler implementation?

This is a shortened version of my 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 displayed in the view where it is attached to the busy indicator.

LoadDataAsync is called by the navigation framework when you first view the screen or when you click the refresh button. This method should synchronously set IsLoading, and then return control to the UI thread until the service returns data. Finally, throwing any exceptions so that they can be caught by the global exception handler (do not discuss!).

SaveAync is called by the button, passing the updated values ​​from the form to the service. It must synchronously install IsLoading, call the Save method on the service asynchronously, and then start the update.

+5
source share
1 answer

There are several problems in the code that jumps out of me:

  • Using ContinueWith . ContinueWith is a dangerous API (it has an amazing default value for its TaskScheduler , so it should only be used if you specify TaskScheduler ). It is also just inconvenient compared to the await equivalent code.
  • Configuring Scenarios from a thread pool thread. I always follow the recommendations in my code that VM-related properties are considered part of the user interface and should only be accessible from the user interface thread. There are exceptions to this rule (in particular, WPF), but they are not the same on every MVVM platform (and this is a question with a dubious construction for starters, IMO), so I just consider virtual machines as part of the user interface layer.
  • If exceptions are excluded. According to the comment, you want the exceptions raised to Application.UnhandledException , but I don't think this code will do that. Assuming TaskScheduler.Current is null at the beginning of LoadDataAsync / SaveAsync , then the rebellion exception code will actually throw an exception in the thread pool thread, not the user interface thread, sending it to AppDomain.UnhandledException rather than Application.UnhandledException .
  • How repeated exceptions are excluded. You will lose the stack trace.
  • Call LoadDataAsync without await . With this simplified code, it will probably work, but it introduces the ability to ignore unhandled exceptions. In particular, if any of the synchronous parts of LoadDataAsync throws, then this exception will be ignored silently.

Instead of tinkering with manual exceptions, I recommend using a more natural approach of await exceptions through await :

  • If the asynchronous operation fails, the task receives an exception on it.
  • await will consider this exception and return it accordingly (preserving the original stack trace).
  • async void methods do not have a task to place an exception, so they will re-raise it directly to the SynchronizationContext . In this case, since your async void methods run in the user interface thread, an exception will be thrown to Application.UnhandledException .

(the async void methods that I am referring to are async delegates passed to the DelegateCommand ).

Now the code 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 problems are resolved:

  • ContinueWith been replaced with a more appropriate await .
  • Scenarios installed from the user interface thread.
  • All exceptions apply to Application.UnhandledException , not AppDomain.UnhandledException .
  • Exceptions keep the original stack trace.
  • There are no await free tasks, so all exceptions will be observed anyway.

And the code is also cleaner. IMO. :)

+8
source

All Articles