我在实现异步中继命令时犯了什么错误

本文关键字:什么 错误 命令 实现 异步 | 更新日期: 2023-09-27 18:21:59

我目前正在学习WPF和MVVM,当我尝试为视图模型编写单元测试时遇到了一个问题,视图模型的命令调用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();
    //}
}

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

据我了解,第一个和第二个变体都应该调用不同的构造函数,但会导致相同的结果。但是,只有第二个变体按照我期望的方式工作。第一个行为很奇怪,例如,如果我按下绑定到Command一次的按钮,它可以正常工作,但是如果我尝试在几秒钟后第二次按下它,它什么也没做。

我对asyncawait的理解远未完成。请解释为什么实例化 Command 属性的两种变体的行为如此不同。

附言:只有当我从RelayCommand继承时,这种行为才很明显。实现ICommand并具有相同两个构造函数的新创建的类按预期工作。

我在实现异步中继命令时犯了什么错误

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

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

[CompilerGenerated]
private void <.ctor>b__0()
{
  this.CommandExecute();
}

这工作正常,因为视图模型不符合垃圾回收条件。

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

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
  public Func<Task> asyncExecute;
  public void <.ctor>b__0()
  {
    this.asyncExecute();
  }
}

这一次,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;
}

注意这实际上与async无关;这纯粹是一个lambda闭包的问题。我怀疑这与关于 Messenger 的 lambda 闭包的根本问题相同。