How do I make an MvvmLight command async?

2019-07-25 21:25发布

Still getting my head around await/async, and am having difficulty seeing how I use it in an MVVM setting. I'm using MvvmLight, but I imagine the question applies to any other MVVM framework.

Suppose I have a WPF application that shows a list of pets. I have a RefreshCommand, that refreshes the list from a WCF service. The UI has an indicator to show that it's busy, and this is bound to an IsBusy bool property on the view model. My command code currently looks like this...

  BackgroundWorker bw = new BackgroundWorker();
  IsBusy = true;
  bw.DoWork += (_, __) => {
    Pets = _petService.GetPets();
    IsBusy = false;
  };
  bw.RunWorkerAsync();

Pets is an ObservableCollection property on the view model.

I want to use the await/async keywords, and do away with the BackgroundWorker. I've searched and read, and read and searched, and am getting confused. Most of the articles/blog posts/etc I've seen seem to require you to create your own async implementation of ICommand. Why is this necessary? Given that MVVM and ICommand are built-in to the .NET framework, surely the ability to use async/await with them should also be built-in, without the need to create our own async command class.

Have I missed something, or is this really the only way to do it?

I tried the following, but it didn't work...

  private async void LoadAsyncData() {
    IsBusy = true;
    Task<ObservableCollection<Pet>> t = GetPetsAsync();
    Pets = await t;
    IsBusy = false;
  }

  private async Task<ObservableCollection<Pet>> GetPetsAsync() {
    await Task.Delay(1);
    return _petService.GetPets();
  }

I added the call to Task.Delay(1) as the compiler warned me that the method would be called sync.

The data loaded, but the UI never responded to the IsBusy property being changed, so the user doesn't see that the UI is busy.

Anyone able to advise?

5条回答
劫难
2楼-- · 2019-07-25 21:44
  private async void LoadAsyncData() {
    IsBusy = true;
    var Pets  =  await GetPetsAsync();
    IsBusy = false;
  }

  private async Task<ObservableCollection<Pet>> GetPetsAsync() {
    var pets = await Task<ObservableCollection<Pet>>.Run(()=>{ 
       return _petService.GetPets();
    });
    return pets;       
  }

In the method GetPetsAsync, a Task is started which may be awaited as shown. If you set a variable (var pets) you are performing a closure. When this is done, then pets is the result of the observable collection and the system will not return until the task is eithr done or faulted. Your petService as shown is synchronous.

查看更多
我命由我不由天
3楼-- · 2019-07-25 21:49

You better listen to what compiler warns you about instead of getting that under the carpet :) After that useless await Task.Delay method will continue on UI thread and so it's basically synchronous method still - that is why UI did not respond to IsBusy property, it was blocked. Instead you have to either

  • Explicitly run your _petService.GetPets on background thread:

    private async void LoadAsyncData() {
       IsBusy = true;
       Pets = await Task.Run(() => _petService.GetPets());
       IsBusy = false;
    }
    
  • (better) Change your petService by adding asynchronous version of GetPets:

    private async void LoadAsyncData() {
       IsBusy = true;
       Pets = await _petService.GetPetsAsync();
       IsBusy = false;
    }
    
查看更多
Lonely孤独者°
4楼-- · 2019-07-25 22:00

If lots of your commands are async, try ReactiveUI. You can use just its ICommand implementation, which takes care of everything: disabling command execution while it's running, marshaling results bak to UI thread etc.

Then it would look like that (in SubViewModel constuctor):

    Pets = new ReactiveList<Pet>();

    GetPets = ReactiveCommand.Create<IEnumerable<Pet>>(async _ => {
     return await _service.GetPets();
    });

    GetPets.Subscribe(pets => {
        using(Pets.SuppressChangeNotifications()) // it's much easier to have one list and just change the items
        {
            Pets.Clear();
            foreach(var pet in pets)
                Pets.Add(pet);
        }
    });

    GetPets.ThrownExceptions.Subscribe(ex =>{
        // show error to user, retry etc
    })

_isBusy = GetPets.IsExecuting.ToProperty(this, x => x.IsBusy);

// property definition
bool IsBusy => _isBusy?.Value ?? false;
查看更多
\"骚年 ilove
5楼-- · 2019-07-25 22:08

Still getting my head around await/async, and am having difficulty seeing how I use it in an MVVM setting.

I have a three-part article series on async MVVM that may help: async data binding, async commands, and async services.

I want to use the await/async keywords, and do away with the BackgroundWorker.

If you just want to do the easier way of replacing BGW with Task.Run, I have a blog post series on doing just that.

Most of the articles/blog posts/etc I've seen seem to require you to create your own async implementation of ICommand. Why is this necessary?

It's not required, but it is a pattern that I (and others) have found useful. It ensures your async MVVM commands are unit testable and properly callable from other code.

Given that MVVM and ICommand are built-in to the .NET framework, surely the ability to use async/await with them should also be built-in, without the need to create our own async command class.

There's no IAsyncCommand in the .NET Framework. I believe it provides a useful abstraction, but it does not follow that that abstraction should be included in every MVVM application.

I added the call to Task.Delay(1) as the compiler warned me that the method would be called sync.

No, that's just avoiding the compiler warning. What you really want to do is fix the warning by making the method asynchronous. Evk's answer has the appropriate code for both approaches.

查看更多
【Aperson】
6楼-- · 2019-07-25 22:08

The BackgroundWorker simulates asynchrony by synchronously calling WCF service on a background thread.

In your following example, although the the delay is asynchronous, you are back on the UI thread calling the service synchronously.

private async Task<ObservableCollection<Pet>> GetPetsAsync() {
    await Task.Delay(1); 
    return _petService.GetPets(); //synchronous WCF call on UI thread - UI will freeze.
}

What you are missing is true asynchronous WCF client call - which you must generate while creating the WCF proxy. You must choose to 'Allow generation of asynchronous operation' and 'Generate task-based operations'. See step 6 here for reference.

Once you have the proxy right, you can use the asynchronous version like this:

private async void LoadAsyncData() {
    IsBusy = true;
    Pets = await _petService.GetPetsAsync(); // true async
    IsBusy = false;
}
查看更多
登录 后发表回答