Morgan Skinner, an Application Development Consultant with Microsoft UK, had a post on his blog yesterday complaining at the quantity of boilerplate code needed to create commands in ViewModels for WPF and Silverlight. I share his irk about that.
He shows a clever technique involving ICustomTypeDescriptor that avoids creating properties (and hence backing fields) for each of the commands. But I think he goes too far. Without the properties on the ViewModel, its tests are going to have a hard time knowing what commands it exposes; and accessing them through code isn’t going to be as straightforward as a property access.
So I propose an alternative technique. It boils down to this:
public class MyViewModel : ViewModelBase { public ICommand Demonstrate { get { return Commands.GetOrCreateCommand( () => this.Demonstrate, DoDemonstration, IsFeelingDemonstrative); } } private void DoDemonstration() { MessageBox.Show("Ta da!"); } private bool IsFeelingDemonstrative() { return DateTime.Today.DayOfWeek == DayOfWeek.Friday; } }
As you can see, ViewModelBase is providing a Commands property to manage all the commands. This neatly avoids having to create a field for each one. Commands is an instance of CommandHolder which is equally simple:
public class ViewModelBase : INotifyPropertyChanged { CommandHolder _commandHolder = new CommandHolder(); // snipped Property Change stuff protected CommandHolder Commands { get { return (_commandHolder ?? (_commandHolder = new CommandHolder())); } } }
public class CommandHolder { Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>(); public ICommand GetOrCreateCommand<T>( Expression<Func<T>> commandNameExpression, Action executeCommandAction, Func<bool> canExecutePredicate) { return GetOrCreateCommand( commandNameExpression, parameter => executeCommandAction(), parameter => canExecutePredicate()); } public ICommand GetOrCreateCommand<T>( Expression<Func<T>> commandNameExpression, Action<object> executeCommandAction, Func<object, bool> canExecutePredicate) { var commandName = SymbolHelpers.GetPropertyName(commandNameExpression); if (!_commands.ContainsKey(commandName)) { var command = new DelegateCommand(executeCommandAction, canExecutePredicate); _commands.Add(commandName, command); } return _commands[commandName]; } }
What do you think? Are my concerns about testa-reada-bility warranted? Does my technique eliminate enough of the boilerplate?
I’ve put a full sample on MSDN code gallery.
10 comments:
Hmmm, interesting, although I'd like you to mention how lifetime issues with those viewmodels and the delegates held by the CommandHolder are avoided, or am I losing sight?
Cheers, your _son_ :-)
Roman,
Good to hear from you!
Since the lifetime of the CommandHolder is bound to the ViewModel (through its base class), I don't see that there will be any lifetime issues caused by the delegates. If the CommandHolder instance was static, that would be a different matter.
Oh, that's correct, I never noticed the CommandHolder instance inside the ViewModel. Grr, next time will read thoroughly :-)
I haven't decided if I like this yet or not, but another option is C# 4's dynamic objects. Even less boilerplate with something like:
public class MyViewModel
{
private dynamic commands = new ExpandoObject();
public dynamic Commands { get { return commands; } }
public MyViewModel()
{
Commands.TestCommand = new DelegateCommand(x => MessageBox.Show("Test", "TaDa!"));
}
}
I'm new to WPF/MVVM
When I run your example (thanks for sharing btw) the button is disabled
@Anonymous, which day did you run it on? Take a look at the expression that determines whether the button is enabled (in IsFeelingDemonstrative) ...
my opinion: over-engineered. adding obscurity and a slight bit of complexity without gaining enough in return.
@Scott That is an awesome refactoring. I've put the Commands property on my base ViewModel, and now the amount of code in the concrete ViewModels is even smaller, and everything is refactoring-friendly.
@Scott That is an awesome refactoring. I've put the Commands property on my base ViewModel, and now the amount of code in the concrete ViewModels is even smaller, and everything is refactoring-friendly.
This use of CanExecute in this implementation did not work for me, in cases where the CanExecute trigger was a changeable property within the same WPF window.
I was able to get it working by replacing:
public event EventHandler CanExecuteChanged;
protected void InvokeCanExecuteChanged(){ EventHandler handler = CanExecuteChanged; if (handler != null) handler(this, EventArgs.Empty);}
With:
public event EventHandler CanExecuteChanged{ add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; }}
Post a Comment