来自 lambda 的带有构造函数参数的 RelayCommand

RelayCommand from lambda with constructor parameters

如果在 XAML 文件中,我从以下 class 中将 Button 绑定到 "Command",则单击 Button 不会导致执行 DoIt:

class Thing()
{
  public Thing(Foo p1)
  {
    Command = new RelayCommand(() => DoIt(p1));
  }

  private DoIt(Foo p)
  {
    p.DoSomething();
  }

  public ICommand Command { get; private set; }
}

但是,如果我从 p1 初始化一个字段并将该字段作为参数传递给 lambda 中的方法调用,它确实有效:

class Thing()
{
  private Foo field;
  public Thing(Foo p1)
  {
    field = p1;
    Command = new RelayCommand(() => DoIt(field));
  }

  private DoIt(Foo p)
  {
    p.DoSomething();
  }

  public ICommand Command { get; private set; }
}

为什么前者失败,而后者却如预期的那样工作?

可能相关:How do closures work behind the scenes? (C#)

编辑:澄清一下,以下内容也适用于我。但是,我还是很想知道为什么第二个例子如我所料,而第一个却没有。

class Thing()
{
  private Foo field;
  public Thing(Foo p1)
  {
    field = p1;
    Command = new RelayCommand(DoIt);
    //Command = new RelayCommand(() => DoIt()); Equivalent?
  }

  private DoIt()
  {
    field.DoSomething();
  }

  public ICommand Command { get; private set; }
}

你的问题是调用方法 DoIt 是在另一个由 lamda 表达式创建的匿名方法中。你的表情

() => DoIt(p1);

创建一个不带参数的匿名方法(因为第一个大括号中没有提供变量)。

我建议您使用 mvvm-light 中的通用构造函数来创建命令:

class Thing
{
    public Thing()
    {
       Command = new GalaSoft.MvvmLight.Command.RelayCommand<bool>(DoIt);
    }

    private void DoIt(bool p)
    {
       p.DoSomething(p);
    }

    public System.Windows.Input.ICommand Command { get; private set; }
}

然后只需将 Button 绑定到 "Command"。

这是一个老问题,但我最近偶然发现了这个话题,值得回答。

这种奇怪行为的原因源于 RelayCommand 的 MVVM Light 实现。 execute 和 canexecute 处理程序在中继命令中存储为 WeakAction _executeWeakFunc<bool> _canExecuteWeakAction 试图在命令仍被 UI 出于某种原因引用时允许 GC 清理视图模型。

跳过一些细节,最重要的是:将视图模型方法分配为处理程序非常有效,因为只要视图模型保持活动状态,WeakAction 就会保持活动状态。对于动态创建的Action,情况就不同了。如果对该动作的唯一引用在 RelayCommand 内部,则只存在弱引用,GC 可以随时收集该动作,将整个 RelayCommand 变成一块死砖。

好的,是时候了解详情了。 WeakAction 的实现并不是盲目地存储对操作的弱引用——这会导致许多引用消失。相反,存储了弱 Delegate.Target 引用和 Delegate.MethodInfo 的组合。对于静态方法,该方法将通过强引用存储。

现在,这导致了三类 lambda:

  1. 静态方法:() => I_dont_access_anything_nonstatic()将存储为强引用
  2. 闭包成员变量:() => DoIt(field)闭包方法将在视图模型中创建class,动作目标是视图模型,只要视图模型保持活动状态,就会保持活动状态。
  3. 局部变量闭包:() => DoIt(p1)闭包将创建一个单独的class实例来存储捕获的变量。这个单独的实例将是操作目标,不会有任何强引用 - GC 在某个时候清理

重要提示: 据我所知,这种行为可能会随着 Roslyn 的变化而改变: 所以今天的工作代码有可能与案例 (2)使用 Roslyn 变成非工作代码。但是,我没有测试这个假设,它可能会完全不同。