Упрощение RelayCommand/DelegateCommand в WPF MVVM ViewModels
Если вы используете MVVM и используете команды, вы часто будете видеть свойства ICommand в ViewModel, которые поддерживаются частными полями RelayCommand или DelegateCommand, как в этом примере из первоначальной статьи MVVM на MSDN:
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 ответов
Я обнаружил, что вам нужен оригинальный путь из 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, поэтому ViewModels, не имеющие каких-либо команд, позволит избежать дополнительных выделений.
Это точно так же, как если бы вы предложили, скажем, целочисленное свойство, которое вычисляет некоторое постоянное значение. Вы можете либо рассчитать его для каждого вызова метода get, либо создать его при первом вызове, а затем кэшировать его, чтобы вернуть кэшированное значение для последующих вызовов. Таким образом, если метод get вызывается не более одного раза, это вообще не имеет значения, если он вызывается часто, вы потеряете некоторую (не очень) производительность, но у вас не будет реальных проблем.
Мне лично нравится сокращать MSDN-путь следующим образом:
RelayCommand _saveCommand;
public ICommand SaveCommand
{
get
{
return _saveCommand ?? (_saveCommand = new RelayCommand(param => this.Save(),
param => this.CanSave ));
}
}
Одна вещь, которую я делаю, это позволяет Visual Studio печатать за меня. Я только что создал фрагмент кода, который позволяет мне создавать RelayCommand, набрав
rc Tab Сохранить Enter
rc - это вкладка ярлыка фрагмента кода, которая загружает набираемый вами текст и создает все остальные формулировки.
Когда вы посмотрите на один фрагмент кода и создадите свой собственный, вы никогда не вернетесь:)
Для получения дополнительной информации о создании фрагментов кода: http://msdn.microsoft.com/en-us/library/ms165394.aspx
Когда вы выставляете свойство ICommand в своей модели представления, и у него нет вспомогательного поля, это нормально, если вы только привязываетесь к этому полю один раз.
Метод GetCommand CommandWrapper вернет команду, если она уже создана.
Почему бы тебе не написать просто:
private readonly RelayCommand _saveCommand = new RelayCommand(param => this.Save(),
param => this.CanSave );;
public ICommand SaveCommand { get { return _saveCommand; } }
Когда вы предоставляете свойство ICommand для вашей модели представления, и у него нет вспомогательного поля, это нормально, если вы только связываетесь с этим полем один раз. По сути, когда ваша форма загружается и выполняет начальные привязки, это единственный раз, когда она получит доступ к свойству get вашей команды.
Есть много случаев, когда вы будете связывать команду только один раз.
Если вы привяжете одну и ту же команду к нескольким элементам управления, вам понадобится поле поддержки.