关于C#:在WPF MVVM ViewModel中简化RelayCommand / DelegateCommand

Simplifying RelayCommand/DelegateCommand in WPF MVVM ViewModels

如果您正在执行MVVM并使用命令,则通常会在ViewModel上看到由私有RelayCommand或DelegateCommand字段支持的ICommand属性,例如来自MSDN上原始MVVM文章的以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

但是,这很繁琐,并且设置新命令相当繁琐(我与一些经验丰富的WinForms开发人员合作,他们讨厌这种输入)。 因此,我想简化一下并稍作讨论。 我在get {}块的第一行设置了一个断点,并看到它只有在我的应用程序首次加载时才被命中-我以后可以触发任意数量的命令,而这个断点永远都不会被命中-所以我 想简化此操作以从我的ViewModels中消除一些混乱,并注意到以下代码的工作原理相同:

1
2
3
4
5
6
7
public ICommand SaveCommand
{
    get
    {
        return new RelayCommand(param => this.Save(), param => this.CanSave );
    }
}

但是,我对C#或垃圾收集器了解不足,无法确定这是否会引起问题,例如在某些情况下生成过多的垃圾。 这会带来什么问题吗?


这与提供一个(例如,整数)属性来计算某个常数值的方式完全相同。
您可以为get方法上的每个调用计算它,也可以在第一个调用上创建它,然后对其进行缓存,以便为以后的调用返回缓存的值。
因此,如果该吸气剂最多被调用一次,那么它根本没有任何区别,如果经常调用它,那么您将失去一些(不是很多)性能,但是不会遇到真正的麻烦。

我个人喜欢这样缩写MSDN:

1
2
3
4
5
6
7
8
9
RelayCommand _saveCommand;
public ICommand SaveCommand
{
  get
  {
    return _saveCommand ?? (_saveCommand = new RelayCommand(param => this.Save(),
                                                            param => this.CanSave ));
  }
}


我发现,如果您有多个调用相同命令的控件,则需要使用MSDN的原始方法,否则每个控件将新建自己的RelayCommand。我没有意识到这一点,因为我的应用程序每个命令只有一个控件。

因此,为了简化ViewModels中的代码,我将创建一个命令包装器类,该类将存储(并延迟实例化)所有RelayCommands并将其放入ViewModelBase类中。这样,用户不必直接实例化RelayCommand或DelegateCommand对象,也不需要了解任何有关它们的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    /// <summary>
    /// Wrapper for command objects, created for convenience to simplify ViewModel code
    /// </summary>
    /// 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>
    /// 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类懒惰地实例化,因此没有任何命令的ViewModels将避免额外的分配。


我要做的一件事是让Visual Studio为我做打字。我刚刚创建了一个代码段,该代码段使我可以通过键入以下内容来创建RelayCommand

rc Tab Save Enter

rc是代码段快捷方式
选项卡会加载您键入所需内容的文本,并创建所有其他措辞。

一旦您查看了一个代码段并创建了自己的代码段,就永远不会回去:)

有关创建代码段的更多信息:http://msdn.microsoft.com/zh-cn/library/ms165394.aspx


When you expose the ICommand property on your viewmodel and it doesn't have a backing field, this is ok, just as long as you only bind to this field once.

CommandWrapper的GetCommand方法将返回命令(如果已创建)。


你为什么不只写:

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

public ICommand SaveCommand { get { return _saveCommand; } }


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

很多时候只绑定一次命令。

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