Another RelayCommand
First thing first I want to point out that there are many implementations of the Relay/Delegate command out there to choose from. My two personal favorites are Josh Smiths RelayCommand and of course the DelegateCommand from the Prism Library. That said, both of them miss things that many of my projects have required and as a result I have had to either extend, or roll my own. So in this post I am going to add to the mix yet another implementation. I presented this implementation earlier this month as part of my MVVM discussion with the folks at the Mankato .Net User Group and a few people afterwards asked me about it so I have decided to make it public.
Use it if like or take and change it so it fits your needs more appropriately. Either way no warranty for you!
Why Relay
Relay commands are very basic implementations of the ICommand interface. WPF people may say what about RoutedCommand? Well, there are several reasons you don’t want to use RoutedCommands in our ViewModels, the most obvious is because they route! Aside from that they rely on the creation of a CommandBinding, and at the end of the day it’s just not real clean.
RelayCommand makes it very easy for our ViewModels to create command instances that our Views can bind to. At the same time RelayCommand with its delegate support allows our ViewModels to control the command implementation details. This helps make our code more testable, and limits the command class explosion that happens if you start creating a class for each command.
What Relay
So what do I want my RelayCommand to do? Below is a list of features that are not native to many other implementations that I find I cannot live without.
- Built-in support for Asynchronous processing (Fire and Forget, or with a Callback/Continuation)
- Supports ‘busy’ cursor behavior when running Synchronously
- Supports Predicate and Action delegate so ViewModels can own the implementation details
- Hooks into the CommandManager’s RequerySuggested event so ViewModels don’t have to raise the CanExecuteChanged event all the time.
So let’s take a look at the code that accomplishes this.
using System; using System.Threading.Tasks; using System.Windows.Input; namespace Commanding { public class RelayCommand : ICommand { #region Private Data Members /// <summary> /// Flag indicates that the command should be run on a seperate thread /// </summary> private readonly bool mRunOnBackGroudThread; /// <summary> /// Predicate that that evaluates if this command can be executed /// </summary> private readonly Predicate<object> mCanExecutePredicate; /// <summary> /// Action to be taken when this command is executed /// </summary> private readonly Action<object> mExecuteAction; /// <summary> /// Run when action method is complete and run on a seperate thread /// </summary> private readonly Action<object> mExecuteActionComplete; #endregion #region Constructor /// <summary> /// Initializes a new instance of the <see cref="RelayCommand"/> class. /// </summary> /// <param name="canExecutePredicate">The can execute predicate.</param> /// <param name="executeAction">The execute action.</param> /// <param name="executeActionComplete">The execute action complete.</param> public RelayCommand(Predicate<object> canExecutePredicate, Action<object> executeAction, Action<object> executeActionComplete) : this(canExecutePredicate, executeAction, true) { if (executeActionComplete == null) { throw new ArgumentNullException("executeActionComplete"); } mExecuteActionComplete = executeActionComplete; } /// <summary> /// Initializes a new instance of the <see cref="RelayCommand"/> class. /// </summary> /// <param name="canExecutePredicate">The can execute predicate.</param> /// <param name="executeAction">The execute action.</param> /// <param name="runOnBackGroundTread">if set to <c>true</c> [run on back ground tread].</param> public RelayCommand(Predicate<object> canExecutePredicate, Action<object> executeAction, bool runOnBackGroundTread) : this(canExecutePredicate, executeAction) { mRunOnBackGroudThread = runOnBackGroundTread; } /// <summary> /// Initializes a new instance of the <see cref="RelayCommand"/> class. /// </summary> /// <param name="canExecutePredicate">The can execute predicate.</param> /// <param name="executeAction">The execute action.</param> public RelayCommand(Predicate<object> canExecutePredicate, Action<object> executeAction) { if (canExecutePredicate == null) { throw new ArgumentNullException("canExecutePredicate"); } if (executeAction == null) { throw new ArgumentNullException("executeAction"); } mCanExecutePredicate = canExecutePredicate; mExecuteAction = executeAction; } /// <summary> /// Initializes a new instance of the <see cref="RelayCommand"/> class. /// </summary> /// <param name="executeAction">The execute action.</param> public RelayCommand(Action<object> executeAction) : this(n => true, executeAction) { } #endregion #region ICommand Members /// <summary> /// Occurs when changes occur that affect whether or not the command should execute. /// </summary> public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } /// <summary> /// Defines the method that determines whether the command can execute in its current state. /// </summary> /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param> /// <returns> /// true if this command can be executed; otherwise, false. /// </returns> public bool CanExecute(object parameter) { return mCanExecutePredicate(parameter); } /// <summary> /// Defines the method to be called when the command is invoked. /// </summary> /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param> public void Execute(object parameter) { using (new BusyCursor()) { if (!mRunOnBackGroudThread) { mExecuteAction(parameter); } else { if (mExecuteActionComplete != null) { //Run with continuation var context = TaskScheduler.FromCurrentSynchronizationContext(); Task.Factory.StartNew(mExecuteAction, parameter).ContinueWith(mExecuteActionComplete, context); } else { //Run as fire and forget Task.Factory.StartNew(mExecuteAction, parameter); } } } } #endregion } }
Constructors
There are several overloaded constructors for this class; each of them implies how the command should behave (Sync or Async). This allows our ViewModels to dictate how these commands should function upon construction time.
Delegates
One of the core goals of an implementation like this it to allow the details of each command to remain in our ViewModels, the Predicate, and Action delegates help us with this quite nicely. Making the parameters to these delegates generic could make this even more flexible but the this example I have left that out.
CanExecuteChanged
Looking at the CanExecuteChanged event, notice how the CommandManager.RequestSuggested event is being wrapped. This is very nice because we put the burden of raising the CanExecuteChagned event on the shoulders of WPF. Additionally the RequestSuggested event is static so it will only hold onto our handlers as a weak reference, so we are good from a memory leak perspective.
Execute
The magic all comes together here, some simple logic to determine how the supplied Action delegate will be executed. Maybe it’s synchronous (with a busy cursor) maybe it’s asynchronous. Maybe a callback is executed once the work put on a separate thread is complete, maybe its fire and forget. All this goodness is dependent on the constructor you decided to call when you created your instance.
Summary
So while not earth shattering, this is the implementation (or a slight variance of it) that I use in most of my WPF projects. The command supports the provider concept by using delegates, sync and async processing, and leverages the command manager to raise the CanExecuteChanged event handler in a memory safe way. Hopefully you will get some use out of this.
[…] few months ago I posted my flavor of the RelayCommand . RelayCommand is a simple implementation of the ICommand interface adding to it support […]