Hey guys,
I am having a couple of issues with my RelayCommand implementation in WPF. This is a modified version of Josh Smith’s original version which is quite well known. However there is a slightly annoying issue which I am trying to solve, in relation with the CanExecuteChanged event.
The well-known implementation is to use the CommandManager in WPF to raise the RequerySuggested event directly. This is a suitable implementation for RoutedCommands, however some other commands may be used for non-UI related tasks, which causes an issue. The well-known implementation in WPF looks like this:
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (_canExecute != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
In Silverlight, we don’t have the CommandManager so instead, we raise the CanExecuteChanged event manually:
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
The WPF implementation has the advantage to call CanExecute automagically when the user interacts with the UI. I am not very fond of this actually (I prefer explicit actions rather than magic, and also the CanExecute method gets called a LOT of times) but many people rely on this feature in WPF. However if anyone else subscribes to the CanExecuteChanged event, they will not be notified because the event is directly wired to the CommandManager (only).
In consequence, I am thinking of the following implementation that combines both the WPF and SL implementations. However I am not 100% sure about it. Won’t it cause the CanExecute method to be executed too often (even more often than what the CommandManager does)? Thoughts?
private EventHandler _canExecuteChanged;
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
{
CommandManager.RequerySuggested += value;
_canExecuteChanged = value;
}
}
remove
{
if (_canExecute != null)
{
CommandManager.RequerySuggested -= value;
_canExecuteChanged = null;
}
}
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
var handler = _canExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
Cheers,
Laurent
using System; using System.Windows.Input; namespace WpfApplication1 { public class DelegateCommand<T> : ICommand { private readonly Func<T, bool> canExecuteFunc; private readonly Action<T> executeAction; private readonly ICommandRequeryStrategy requeryStrategy; public DelegateCommand(Action<T> executeAction, RequeryStrategy requeryStrategy = RequeryStrategy.CommandManager) : this(executeAction, val => true, requeryStrategy) { } public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecuteFunc, RequeryStrategy requeryStrategy = RequeryStrategy.CommandManager) { this.executeAction = executeAction; this.canExecuteFunc = canExecuteFunc; //determine command requery strategy switch(requeryStrategy) { case RequeryStrategy.CommandManager: this.requeryStrategy = new CommandManagerRequeryStrategy(); break; case RequeryStrategy.Manual: this.requeryStrategy = new ManualRequeryStrategy(this); break; default: throw new ArgumentOutOfRangeException("requeryStrategy"); } } /// <summary> /// Triggers the <see cref="CanExecuteChanged"/> event. /// </summary> public void RaiseCanExecuteChangedEvent() { requeryStrategy.SuggestRequery(); } /// <summary> /// Fires when changes occur that affect whether /// or not the command should execute. /// </summary> /// <remarks>Listeners are forwarded to the active /// <see cref="ICommandRequeryStrategy"/>.</remarks> public event EventHandler CanExecuteChanged { add { requeryStrategy.AddRequeryListener(value); } remove { requeryStrategy.RemoveRequeryListener(value); } } public bool CanExecute(object parameter) { return canExecuteFunc((T)parameter); } public void Execute(object parameter) { executeAction((T)parameter); } } public enum RequeryStrategy { CommandManager, Manual } public interface ICommandRequeryStrategy { void AddRequeryListener(EventHandler listener); void RemoveRequeryListener(EventHandler listener); void SuggestRequery(); } public class ManualRequeryStrategy : ICommandRequeryStrategy { private event EventHandler requerySuggested; private readonly ICommand sender; public ManualRequeryStrategy(ICommand sender) { this.sender = sender; } public void AddRequeryListener(EventHandler listener) { requerySuggested += listener; } public void RemoveRequeryListener(EventHandler listener) { requerySuggested -= listener; } public void SuggestRequery() { if (requerySuggested != null) requerySuggested(sender, EventArgs.Empty); } } public class CommandManagerRequeryStrategy : ICommandRequeryStrategy { public void AddRequeryListener(EventHandler listener) { CommandManager.RequerySuggested += listener; } public void RemoveRequeryListener(EventHandler listener) { CommandManager.RequerySuggested -= listener; } public void SuggestRequery() { CommandManager.InvalidateRequerySuggested(); } } }
I would say instead of passing in an enum, have the constructor take an ICommandRequeryStrategy. You get the added benefit of configuring the strategy from an IoC provider (if you choose). You don’t have to remember to add a new type to the enum and a new case to the switch/case statement. This is the Open-Closed Principle at its core. You can extend the functionality of your DelegateCommand to support a new RequeryStrategy, without modifying its internals.
Just to get back to the original question, what do you guys think about doing both, i.e. using the CommandManager AND manually raising the event. This would do the trick of both updating the UI and also handling non-UI commands, but what would the bad side effects be (if any)? It’s kind of hard to be simpler, and given how many events the CommandManager raises anyway, one more from time to time is not such a big deal… or is it?
Cheers,
Laurent
You could provide a default value. I can already think of one additional Strategy. RequeryOnPropertyChangedStrategy would subscribe to INPC and fire RequerySuggested whenever a propertychanged event occurs. Less chatty than command manager but doesn’t require as much interaction from the user.
public DelegateCommand(Action<T> executeAction, ICommandRequeryStrategy requeryStrategy = new CommandManagerRequeryStrategy())
We explicitly decided to avoid any interplay with the routed commands in Prism because of the complexities and weirdness that can result from focus interplay that I wrote about here as a follow on to my MSDN Magazine article after getting some late tech review comments from the guys on the WPF team:
http://briannoyes.net/2008/11/09/TheTruthAboutRoutedCommandsRouting.aspx
as well as for the excessive re-polling that happens with every focus change and repainting.
I think it is better to not tie into CommandManager at all. RaiseCanExecuteChanged at the appropriate times when the things that are tied to the command change. Simple as that. Yes it puts a little more burden on the developer to understand what their command enablement is coupled to, but if they are not capable of figuring that out, they are probably not qualified to be doing the job. J
My 2c.
Brian