摘要

我正在开发一个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; }
}


在此代码中,messengerConfigureReportPath闭包中的自由变量。如果我注释掉了对它的引用(如我在本片段中所做的那样),那么该命令可以很好地执行。但是,如果我取消注释这些引用并尝试以相同的方式调用该命令,则什么也不会发生。

对于它的价值,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仅维护对ExecuteCanExecute委托的弱引用。对于GalaSoft.MvvmLight.CommandWpf.RelayCommandGalaSoft.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保留其委托作为强引用而不是弱引用。请谨慎使用此选项,因为它确实会增加内存泄漏的可能性。它取决于您的代表实际捕获的内容。关键是要跟踪被捕获的对象。

09-10 05:30
查看更多