简化WPF MVVM ViewModels中的RelayCommand/DelegateCommand

19

如果你正在使用MVVM并使用命令,你经常会看到ViewModel上的ICommand属性由私有的RelayCommand或DelegateCommand字段支持,就像这个例子一样,来自于MSDN上关于原始MVVM文章:

RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

然而,这样做会产生很多冗余代码,导致设置新命令变得很繁琐(我与一些老练的WinForms开发人员合作,他们对所有这些打字都感到反感)。因此,我想简化它并进行了一些研究。我在get{}块的第一行设置了断点,发现它只在应用程序首次加载时被触发 - 我稍后可以随意启动多个命令,这个断点永远不会被触发。因此,我想简化这个过程以从我的ViewModels中删除一些代码,并注意到以下代码可以起到同样的作用:

public ICommand SaveCommand
{
    get
    {
        return new RelayCommand(param => this.Save(), param => this.CanSave );
    }
}

然而,我对C#或垃圾回收器的了解不足,无法确定这是否会导致问题,例如在某些情况下生成过多的垃圾。这会造成任何问题吗?

6个回答

18
这与提供一个(比如整数)属性来计算某个常量值完全相同。您可以在每次调用get方法时进行计算,也可以在第一次调用时创建它并缓存它,以便返回后续调用的缓存值。因此,如果getter最多只被调用一次,那么它根本没有任何区别;如果经常被调用,您将失去一些(不多)性能,但不会遇到实际问题。
我个人喜欢像MSDN一样缩写它:
RelayCommand _saveCommand;
public ICommand SaveCommand
{
  get
  {
    return _saveCommand ?? (_saveCommand = new RelayCommand(param => this.Save(),
                                                            param => this.CanSave ));
  }
}

1
不错的调用。我喜欢你使用空合并运算符的方式。我总是忘记它的存在。 - A.R.

8
我发现如果你有多个调用相同命令的控件,则需要从MSDN获取原始方式,否则每个控件都将新建自己的RelayCommand。我没有意识到这一点,因为我的应用程序每个命令只有一个控件。
为了简化ViewModels中的代码,我将创建一个命令包装类,该类存储(并延迟实例化)所有RelayCommands,并将其放入我的ViewModelBase类中。这样,用户不必直接实例化RelayCommand或DelegateCommand对象,也不需要了解它们的任何信息。
    /// <summary>
    /// Wrapper for command objects, created for convenience to simplify ViewModel code
    /// </summary>
    /// <author>Ben Schoepke</author>
    public class CommandWrapper
    {
    private readonly List<DelegateCommand<object>> _commands; // cache all commands as needed

    /// <summary>
    /// </summary>
    public CommandWrapper()
    {
        _commands = new List<DelegateCommand<object>>();
    }

    /// <summary>
    /// Returns the ICommand object that contains the given delegates
    /// </summary>
    /// <param name="executeMethod">Defines the method to be called when the command is invoked</param>
    /// <param name="canExecuteMethod">Defines the method that determines whether the command can execute in its current state.
    /// Pass null if the command should always be executed.</param>
    /// <returns>The ICommand object that contains the given delegates</returns>
    /// <author>Ben Schoepke</author>
    public ICommand GetCommand(Action<object> executeMethod, Predicate<object> canExecuteMethod)
    {
        // Search for command in list of commands
        var command = (_commands.Where(
                            cachedCommand => cachedCommand.ExecuteMethod.Equals(executeMethod) &&
                                             cachedCommand.CanExecuteMethod.Equals(canExecuteMethod)))
                                             .FirstOrDefault();

        // If command is found, return it
        if (command != null)
        {
            return command;
        }

        // If command is not found, add it to the list
        command = new DelegateCommand<object>(executeMethod, canExecuteMethod);
        _commands.Add(command);
        return command;
    }
}

这个类也是由ViewModelBase类进行惰性实例化的,因此没有任何命令的ViewModel将避免额外的分配。


4
如果你真的很担心内存使用,那么只实例化你将要使用的命令岂不是更合理?例如,如果你有一个虚拟机暴露出3个命令,而只有一个被使用,那就把其余的都删掉。如果你正在使用所有的虚拟机命令(这应该是的),那么采用懒加载的系统会消耗更多的内存和处理器时间,这就背离了优化的初衷,尤其是对于嵌入式系统而言。 - A.R.

7
我做的一件事是让Visual Studio为我打字。 我只创建了一个代码片段,允许我通过键入rc Tab Save Enter来创建RelayCommand。
rc是代码片段快捷方式 tab加载您要输入的文本,并创建所有其他措辞。
一旦您查看了一个代码片段并创建了自己的代码片段,您将永远不会回头 :)
有关创建代码片段的更多信息:http://msdn.microsoft.com/en-us/library/ms165394.aspx

1
我不知道为什么没有其他人提到这一点。这是我所做的,也是处理命令样板代码的最佳方式。所有那些lazy->lazy的东西实际上在长期来看并不能节省任何时间或代码。 - A.R.

1
为什么不只写成这样:


private readonly RelayCommand _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );;

public ICommand SaveCommand { get { return _saveCommand; } }

我猜是为了防止不必要地创建对象,但我并不真正知道它使用的内存或创建Command的其他副作用。 - jpsstavares
我们希望使用延迟实例化来节省内存。最终我在ViewModelBase类中添加了一些代码,该类存储RelayCommands的List<>并具有名为GetCommand()的方法。因此,当我们实现一个ViewModel时,我们只需要创建一个调用GetCommand()的ICommand属性,其中包含执行和canExecute委托,而ViewModel实现者不需要知道任何关于RelayCommand/Delegate command的信息。 - Ben Schoepke
5
由于Command的内存占用非常低,我不会开始优化Command。 - jbe
1
明白了。我正在开发嵌入式系统,所以必须尽量减少内存使用,特别是在像我们的ViewModelBase这样频繁使用的基类中。 - Ben Schoepke

1
当你在视图模型上公开ICommand属性且没有支持字段时,只要你只绑定一次,那么这是可以的。
如果已经创建了CommandWrapper的命令,则GetCommand方法将返回该命令。

0

当在您的ViewModel上公开ICommand属性并且它没有后备字段时,这是可以的,只要您仅绑定一次。基本上,当表单加载并执行初始绑定时,这是唯一访问命令的get属性的时间。

有很多情况下,您只会绑定命令一次。

如果将同一命令绑定到多个控件,则需要后备字段。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接