我正在学习WPF和MVVM,当我尝试为viewmodel编写单元测试时遇到了问题,viewmodel的命令调用async
方法。这个问题在this问题中有详细描述。这个问题也有一个解决方案:用一个额外的等待方法编写一个新的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();
//}
}
我的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";
}
}
据我所知,First和Second变体都应该调用不同的构造函数,但会导致相同的结果。但是,只有第二种变体以我期望的方式工作。第一个表现奇怪,例如,如果我按下按钮,绑定到Command
一次,它可以正常工作,但如果我尝试在几秒钟后再次按下它,它就什么都不做。
我对async
和await
的理解远非完整。请解释一下为什么实例化Command
属性的两种变体表现得如此不同。
P.S。:只有当我从RelayCommand
继承时,这种行为才会引人注目。一个新创建的类实现ICommand
并具有相同的两个构造函数,可以按预期工作。
答案 0 :(得分:15)
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闭包的问题。我怀疑它与this one regarding lambda closures with Messenger
的基本问题相同。