Introduction
Before starting off, I must say this is my first article on CodeProject. I think there are lots of things that need improving.
I assume you have basic understanding of MVVM (Model View View-Model) architecture in WPF (Windows Presentation Foundation) and the ICommand
interface. PRISM is a great framework to build enteprise level software.
ICommand
is implemented by DelegateCommand
or RelayCommand
. Both have some advantages and some tradeoffs. My proposal is sort of a hybrid between the two.
Background
There are tons of material online and many questions on StackOverflow that provide good starting points for MVVM and WPF.
Using the Code
Vanilla implementation of ICommand
will look something like this:
public class RelayCommand : ICommand
{
private readonly Action<object> execute;
private readonly Func<object, bool> canExecute;
public RelayCommandBase(Action<object> execute, Func<object, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
The thing to note is that CommandManager.RequerySuggested
is a weak event that is fired whenever LayoutUpdated
of the control is invoked and the control has Command
binded to it, example a button. If CanExecute
evaluates to false
the button is disabled, else it's enabled.
This leads to evaluation of the CanExecute
function whenever the layout is updated. This, in retrospect is correct behaviour as WPF or the control has no other way to know under what conditions the CanExecute
should be re-evaluated.
DelegateCommand
in PRISM does away with continuous updation by providing RaiseCanExecuteChanged method. The method is to be called on the setter of the property that effects the command. This involves calling many commands and becomes error prone in case of viewmodels with multiple commands and properties.
Another approach is having the property say it wants to reevaluate all commands or single command.
public class SampleViewModel : ViewModelBase
{
public SampleViewModel()
{
base.RegisterCommand(() => HelloCommand, HelloCommand);
}
public RelayCommandBase HelloCommand
{
get
{
return Get(() => HelloCommand,
new RelayCommand(ExecuteHelloCommand, CanExecuteHelloCommand));
}
}
[EffectsCommand] public string Text
{
get { return Get(() => Text); }
set { Set(() => Text, value); }
}
private bool CanExecuteHelloCommand()
{
return Text == "Hello";
}
private void ExecuteHelloCommand()
{
MessageBox.Show("executed !!");
}
}
First, we create an attribute for this purpose. A property can specify the command name it effects or all commands if no command name is used.
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public class EffectsCommandAttribute : Attribute
{
private readonly string commandName;
public EffectsCommandAttribute()
{ }
public EffectsCommandAttribute(string commandName)
{
this.commandName = commandName;
}
public string CommandName
{
get { return this.commandName; }
}
}
Now we have specialized ViewModelBase
to take care of this attribute.
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
{
...
InvokeCommandCanExecuteChanged(propertyName);
...
}
protected virtual void InvokeCommandCanExecuteChanged(string propertyName)
{
foreach (var item in commandPropList)
{
if (item.PropertyName == propertyName)
{
if (item.CommandName != null)
{
if (commandRegistry.ContainsKey(item.CommandName))
{
commandRegistry[item.CommandName].OnCanExecuteChanged();
}
else
{
}
}
else
{
foreach (var cmditem in commandRegistry)
{
cmditem.Value.OnCanExecuteChanged();
}
}
}
}
}
private List<EffectCommandProperty>
commandPropList = new List<EffectCommandProperty>();
private void GetPropertyEffectingCommands()
{
var props = System.ComponentModel.TypeDescriptor.GetProperties(this).Cast
<PropertyDescriptor>().Where
(d => d.Attributes[typeof(EffectsCommandAttribute)] != null);
foreach (var item in props)
{
EffectCommandProperty p = new EffectCommandProperty();
p.PropertyName = item.Name;
p.CommandName = (item.Attributes[typeof
(EffectsCommandAttribute)] as EffectsCommandAttribute).CommandName;
commandPropList.Add(p);
}
}
private Dictionary<string, RelayCommandBase>
commandRegistry = new Dictionary<string, RelayCommandBase>();
private class EffectCommandProperty
{
internal string PropertyName { get; set; }
internal string CommandName { get; set; }
}
}
We also have method to register for command collection.
protected void RegisterCommand<T>
(Expression<Func<T>> commandExpression, T command)
where T : RelayCommandBase
{
if (command == null)
throw new ArgumentNullException("command");
var commandName = GetPropertyName(commandExpression);
commandRegistry[commandName] = command;
}
All the reevaluation is now restricted to setter of the ViewModelBase
.
I hope this is of some use. No doubt that you can make a lot of improvements in the code.
Points of Interest
Extension points that I can think of:
- Reduce loop in
InvokeCommandCanExecuteChanged
, it does not look good nor efficient if property count increases.
- Auto assimilation of commands rather than using
RegisterCommand
.
private void RegisterAllCommandProperties()
{
foreach (PropertyDescriptor item in System.ComponentModel.TypeDescriptor.GetProperties(this))
{
var value = item.GetValue(this);
if(typeof(RelayCommandBase).IsAssignableFrom(value.GetType()))
{
commandRegistry[item.Name] = value as RelayCommandBase;
}
}
}
History
- 5 Jun, 2014 - First draft