摘要
我正在开发一个WPF应用程序,该应用程序位于工具托盘中,并且其中包含一小段代码,这些代码展现出许多异常的行为(对我而言)。我已经差不多了,所以我要继续下一个。
本质上,我的问题是,当我尝试调用它时,我的WPF命令之一的回调似乎不会执行,除非我在其闭包中注释掉了所有对自由变量的引用。我在下面发布了代码,以解释我的意思。
细节
右键单击应用程序的工具栏图标时,它会弹出一个上下文菜单,其中包含我绑定到由视图模型类公开的命令的项目。这些绑定在App.xaml
中定义:
<ContextMenu x:Shared="false" x:Key="SysTrayMenu">
<MenuItem Header="Configure Report Path..." Command="{Binding ConfigureReportPathCommand}" />
<MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
</ContextMenu>
我将相应的视图模型定义为:
public class CounterIconViewModel : ViewModelBase
{
public CounterIconViewModel(IMessenger messenger)
{
void ConfigureReportPath()
{
var browseDialog = new VistaFolderBrowserDialog { ShowNewFolderButton = false };
// Passing a new window is necessary to keep the dialog alive for some reason.
if (browseDialog.ShowDialog(new Window()) != true)
{
return;
}
// Command doesn't execute unless I comment out the line below.
//messenger.Send(browseDialog.SelectedPath, "ReportPath");
}
ConfigureReportPathCommand = new RelayCommand(ConfigureReportPath);
ExitApplicationCommand = new RelayCommand(Application.Current.Shutdown);
}
public ICommand ConfigureReportPathCommand { get; }
public ICommand ExitApplicationCommand { get; }
}
在此代码中,
messenger
是ConfigureReportPath
闭包中的自由变量。如果我注释掉了对它的引用(如我在本片段中所做的那样),那么该命令可以很好地执行。但是,如果我取消注释这些引用并尝试以相同的方式调用该命令,则什么也不会发生。对于它的价值,sorted out one issue with it提供了
RelayCommand
。我尝试过的
我尝试将
messenger
存储为字段,并在闭包中引用该字段:private readonly IMessenger _messenger;
public CounterIconViewModel(IMessenger messenger)
{
_messenger = messenger;
void ConfigureReportPath()
{
var browseDialog = new VistaFolderBrowserDialog { ShowNewFolderButton = false };
// Passing a new window is necessary to keep the dialog alive for some reason.
if (browseDialog.ShowDialog(new Window()) != true)
{
return;
}
_messenger.Send(browseDialog.SelectedPath, "ReportPath");
}
ConfigureReportPathCommand = new RelayCommand(ConfigureReportPath);
ExitApplicationCommand = new RelayCommand(Application.Current.Shutdown);
}
这很好,所以如果没有别的我可以依靠。
我也刚刚尝试在视图模型构造函数中创建局部变量,并在闭包中引用它。有了这个,我似乎可以验证该命令中是否使用了任何可用变量(如果与
messenger
只是有些时髦,则无法执行),该命令将无法执行:public CounterIconViewModel(IMessenger messenger)
{
var foo = "bar";
void ConfigureReportPath()
{
var browseDialog = new VistaFolderBrowserDialog { ShowNewFolderButton = false };
// Passing a new window is necessary to keep the dialog alive for some reason.
if (browseDialog.ShowDialog(new Window()) != true)
{
return;
}
// Also needs to be commented out to allow command to execute.
//var bar = foo.Length;
}
ConfigureReportPathCommand = new RelayCommand(ConfigureReportPath);
ExitApplicationCommand = new RelayCommand(Application.Current.Shutdown);
}
更新资料
我刚刚尝试过的另一件事是将Messenger从构造函数传递给嵌套方法作为参数:
public CounterIconViewModel(IMessenger messenger)
{
void ConfigureReportPath(IMessenger nestedMessenger)
{
// ...
}
ConfigureReportPathCommand = new RelayCommand(() => ConfigureReportPath(messenger));
}
这将阻止无论我是否引用
nestedMessenger
都触发该命令。如果我改为将参数设置为字符串并执行new RelayCommand(() => ConfigureReportPath("foo"))
,则效果很好。 最佳答案
因此,这里发生了几件事。
首先,重要的是要知道,默认情况下,MVVM Light仅维护对Execute
和CanExecute
委托的弱引用。对于GalaSoft.MvvmLight.CommandWpf.RelayCommand
和GalaSoft.MvvmLight.Command.RelayCommand
类都是如此。最终,这就是为什么未调用您的执行委托的原因,因为该命令仅包含对它的弱引用,垃圾收集器正在清理它。
那么问题来了,为什么要清理它?
首先,我们需要了解编写new RelayCommand(ConfigureReportPath)
时发生的情况。 RelayCommand
构造函数参数是Action
,因此编译器将方法转换为Action委托。这等效于:
new RelayCommand(new Action(ConfigureReportPath));
在IL中,您将看到通话
ldtfn <method pointer>
newobj instance void [mscorlib]System.Action::.ctor(object, native int)
查看文档here。顶部附近有一条隐藏线:
... lambda表达式在声明时会转换为委托。局部函数仅在用作委托时才转换为委托。
这里的关键是因为您使用局部函数作为
RelayCommand
的参数,它们始终将转换为委托(Action
)类型。接下来的问题是局部函数。您会注意到,如果将您的ConfigureReportPath转换为视图模型上的实例方法,您的问题也将消失(这也需要将IMessenger实例存储在字段中)。
当你写:
public CounterIconViewModel(IMessenger messenger)
{
void ConfigureReportPath()
{
...
messenger.Send(...);
}
ConfigureReportPathCommand = new RelayCommand(ConfigureReportPath);
}
实际编译的内容是这样的(请注意,编译过程正在生成IL;我正在转换回大致的C#等效项)
public class CounterIconViewModel
{
private sealed class Generated
{
public IMessenger messenger;
void ConfigureReportPath()
{
...
messenger.Send(...);
}
}
public CounterIconViewModel(IMessenger messenger)
{
var generated = new Generated();
generated.messenger = messenger;
ConfigureReportPathCommand = new RelayCommand(new Action(generated.ConfigureReportPath));
}
}
通过这种形式,正在发生的事情更加明显。生成的内部类包含命令的实际执行方法。隐式
Action
是唯一拥有对其引用的内容,而RelayCommand
是唯一拥有对其引用的内容。由于Action
仅持有对RelayCommand
的弱引用,因此垃圾回收器可以在执行离开视图模型的构造函数后自由清理它。另一种看待它的方法是,好像您首先将其作为委托编写:
public CounterIconViewModel(IMessenger messenger)
{
ConfigureReportPathCommand = new RelayCommand(() =>
{
...
messenger.Send(...);
});
}
同样,以这种形式,为什么要对委托进行垃圾回收更为明显。因为唯一捕获的是构造函数参数,所以一旦超出范围,就可以清除委托。
我们还要看看将
Action
存储在字段中时会发生什么(就像第二次尝试一样)。public class CounterIconViewModel
{
private readonly IMessenger _messenger;
public CounterIconViewModel(IMessenger messenger)
{
_messenger = messenger;
void ConfigureReportPath()
{
...
_messenger.Send(...);
}
ConfigureReportPathCommand = new RelayCommand(ConfigureReportPath);
}
}
在这种情况下,本地函数需要访问包含类型的私有字段。这将导致生成本地函数,如下所示:
public class CounterIconViewModel
{
private readonly IMessenger _messenger;
private void ConfigureReportPath()
{
...
_messenger.Send(...);
}
public CounterIconViewModel(IMessenger messenger)
{
_messenger = messenger;
ConfigureReportPathCommand = new RelayCommand(new Action(ConfigureReportPath);
}
}
由于局部函数是在视图模型上作为实例方法创建的,因此只要您的视图模型可以使用,它就将一直存在。除了Roselyn中的实现本身之外,我从没有找到关于如何期望编译器处理局部函数的良好文档。如果有人知道这样的文档,我很乐意看到。
潜在解决方案
只需将本地函数重新编写为视图模型上的实例方法即可。我倾向于这种方法,因为我认为它最容易阅读,并且出现最少的怪异问题。
在
IMessenger
上使用可选参数。 RelayCommand
上还有一个可选的布尔参数,名为“ keepTargetAlive”。它默认为RelayCommand
,但如果将其设置为true,则会导致false
保留其委托作为强引用而不是弱引用。请谨慎使用此选项,因为它确实会增加内存泄漏的可能性。它取决于您的代表实际捕获的内容。关键是要跟踪被捕获的对象。