我在实现异步RelayCommand时犯了什么错误?

And*_*nov 4 c# wpf mvvm mvvm-light async-await

我正在学习WPF和MVVM,当我尝试为viewmodel编写单元测试时遇到了问题,viewmodel的命令调用async方法.这个问题在这个问题中得到了很好的描述.这个问题也有一个解决方案:用一个额外的等待方法编写一个新的Command类,可以在单元测试中等待.但是因为我使用MvvmLight,所以我决定不写一个新类,而是继承内置RelayCommand类.但是,我似乎不明白如何正确地做到这一点.下面是一个说明我的问题的简化示例:

AsyncRelayCommand:

public class AsyncRelayCommand : RelayCommand
{
    private readonly Func<Task> _asyncExecute;

    public AsyncRelayCommand(Func<Task> asyncExecute)
        : base(() => asyncExecute())
    {
        _asyncExecute = asyncExecute;
    }

    public AsyncRelayCommand(Func<Task> asyncExecute, Action execute)
        : base(execute)
    {
        _asyncExecute = asyncExecute;
    }

    public Task ExecuteAsync()
    {
        return _asyncExecute();
    }

    //Overriding Execute like this fixes my problem, but the question remains unanswered.
    //public override void Execute(object parameter)
    //{
    //    _asyncExecute();
    //}
}
Run Code Online (Sandbox Code Playgroud)

我的ViewModel(基于默认的MvvmLight MainViewModel):

public class MainViewModel : ViewModelBase
{
    private string _welcomeTitle = "Welcome!";

    public string WelcomeTitle
    {
        get
        {
            return _welcomeTitle;
        }

        set
        {
            _welcomeTitle = value;
            RaisePropertyChanged("WelcomeTitle");
        }
    }

    public AsyncRelayCommand Command { get; private set; }
    public MainViewModel(IDataService dataService)
    {
        Command = new AsyncRelayCommand(CommandExecute); //First variant
        Command = new AsyncRelayCommand(CommandExecute, () => CommandExecute()); //Second variant
    }

    private async Task CommandExecute()
    {
        WelcomeTitle = "Command in progress";
        await Task.Delay(1500);
        WelcomeTitle = "Command completed";
    }
}
Run Code Online (Sandbox Code Playgroud)

据我所知,First和Second变体都应该调用不同的构造函数,但会产生相同的结果.但是,只有第二种变体以我期望的方式工作.第一个表现奇怪,例如,如果我按下按钮,绑定到Command一次,它可以正常工作,但如果我尝试在几秒钟后再次按下它,它就什么都不做.

我的理解asyncawait远未完成.请解释一下为什么实例化Command属性的两种变体表现得如此不同.

PS:这种行为只有在我继承时才会引人注目RelayCommand.实现ICommand并具有相同的两个构造函数的新创建的类按预期工作.

Ste*_*ary 15

好的,我想我发现了问题.RelayCommand使用a WeakAction来允许Action垃圾收集的所有者(目标).我不确定他们为什么做出这个设计决定.

因此,在() => CommandExecute()视图模型构造函数中的工作示例中,编译器在构造函数上生成一个如下所示的私有方法:

[CompilerGenerated]
private void <.ctor>b__0()
{
  this.CommandExecute();
}
Run Code Online (Sandbox Code Playgroud)

哪个工作正常,因为视图模型不符合垃圾回收的条件.

但是,在() => asyncExecute()构造函数中的奇行为示例中,lambda会关闭asyncExecute变量,从而导致为该闭包创建单独的类型:

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
  public Func<Task> asyncExecute;

  public void <.ctor>b__0()
  {
    this.asyncExecute();
  }
}
Run Code Online (Sandbox Code Playgroud)

这一次,实际的目标Action是一个实例<>c__DisplayClass2,它永远不会保存在任何地方.由于WeakAction只保存弱引用,因此该类型的实例符合垃圾回收的条件,这就是它停止工作的原因.

如果这个分析是正确的,那么你应该总是将一个本地方法传递给RelayCommand(即,不创建lambda闭包),或者捕获对Action自己产生的(强)引用:

private readonly Func<Task> _asyncExecute;
private readonly Action _execute;

public AsyncRelayCommand(Func<Task> asyncExecute)
    : this(asyncExecute, () => asyncExecute())
{
}

private AsyncRelayCommand(Func<Task> asyncExecute, Action execute)
    : base(execute)
{
  _asyncExecute = asyncExecute;
  _execute = execute;
}
Run Code Online (Sandbox Code Playgroud)

请注意,这实际上与此无关async; 这纯粹是一个关于lambda闭包的问题.我怀疑这与关于lambda闭包的Messenger基本问题是一样的.