如何使MvvmLight命令异步?

时间:2016-05-22 20:55:51

标签: c# asynchronous mvvm async-await

还在等待/异步,我很难看到我在MVVM设置中如何使用它。我正在使用MvvmLight,但我想这个问题适用于任何其他MVVM框架。

假设我有一个显示宠物列表的WPF应用程序。我有一个RefreshCommand,它从WCF服务刷新列表。 UI有一个指示器,表明它正忙,并且它绑定到视图模型上的IsBusy bool属性。我的命令代码目前看起来像这样......

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

Pets是视图模型上的ObservableCollection属性。

我想使用await / async关键字,并取消BackgroundWorker。我搜索和阅读,阅读和搜索,我感到困惑。我见过的大多数文章/博客文章似乎都要求你创建自己的ICommand异步实现。为什么这有必要?鉴于MVVM和ICommand是.NET框架内置的,当然也应该内置使用async / await的能力,而无需创建我们自己的异步命令类。

我错过了什么,或者这是否真的是唯一的方法呢?

我尝试了以下内容,但它没有用......

  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();
  }

我添加了对Task.Delay(1)的调用,因为编译器警告我该方法将被称为sync。

加载了数据,但UI从未响应IsBusy属性被更改,因此用户没有看到UI正忙。

任何人都可以提供建议吗?

5 个答案:

答案 0 :(得分:3)

你最好听一下编译器警告你的内容而不是把它放在地毯上:)之后无用的await Task.Delay方法将继续在UI线程上,所以它基本上是同步方法 - 这就是为什么UI没有响应IsBusy属性,它被阻止了。相反,你必须

  • 在后台主题明确运行_petService.GetPets

    private async void LoadAsyncData() {
       IsBusy = true;
       Pets = await Task.Run(() => _petService.GetPets());
       IsBusy = false;
    }
    
  • (更好)通过添加GetPets的异步版本来更改您的petService:

    private async void LoadAsyncData() {
       IsBusy = true;
       Pets = await _petService.GetPetsAsync();
       IsBusy = false;
    }
    

答案 1 :(得分:2)

  

还在等待/异步,我很难看到我在MVVM设置中如何使用它。

我在async MVVM上有一个由三部分组成的系列文章可能有所帮助:async data bindingasync commandsasync services

  

我想使用await / async关键字,并取消BackgroundWorker。

如果你只是想更简单地使用replacing BGW with Task.Run,我会有一篇关于这样做的博客文章系列。

  

我见过的大多数文章/博文/等似乎都要求您创建自己的ICommand异步实现。为什么这有必要?

它不是必需的,但它是我(和其他人)发现有用的模式。它确保您的异步MVVM命令可以进行单元测试,并且可以从其他代码中正确调用。

  

鉴于MVVM和ICommand是.NET框架内置的,当然也应该内置使用async / await的能力,而无需创建我们自己的异步命令类。

.NET Framework中没有IAsyncCommand。我相信它提供了一个有用的抽象,但并不是说抽象应该包含在每个MVVM应用程序中。

  

我添加了对Task.Delay(1)的调用,因为编译器警告我该方法将被称为sync。

不,那只是避免编译器警告。你真正想做的是通过使方法异步来修复警告。 Evk's answer具有适用于这两种方法的相应代码。

答案 2 :(得分:1)

BackgroundWorker通过在后台线程上同步调用WCF服务来模拟异步。

在下面的示例中,虽然 delay 是异步的,但您将回到同步调用服务的UI线程上。

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

您缺少的是 true 异步WCF客户端调用 - 您必须在创建WCF代理时生成该调用。您必须选择“允许生成异步操作”和“生成基于任务的操作”。请参阅步骤6 here以供参考。

一旦你拥有了代理权限,就可以使用这样的异步版本:

private async void LoadAsyncData() {
    IsBusy = true;
    Pets = await _petService.GetPetsAsync(); // true async
    IsBusy = false;
}

答案 3 :(得分:1)

  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;       
  }

在GetPetsAsync方法中,启动了一个可以等待的任务,如图所示。如果设置变量(var pets),则表示正在执行闭包。完成此操作后,宠物就是可观察收集的结果,系统将不会返回,直到任务完成或出现故障。您显示的petService是同步的。

答案 4 :(得分:0)

如果您的许多命令都是异步的,请尝试ReactiveUI。您可以使用它的ICommand实现,它可以处理所有事情:在命令执行时禁用命令执行,将结果封送到UI线程等。

然后它看起来像(在SubViewModel构造函数中):

    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;