I am looking for an implementation of RelayCommand
. The original implementation that I considered was the classic one (lets call it implementation A)
public class RelayCommand : ICommand
{
private readonly Predicate<object> canExecute;
private readonly Action<object> execute;
private EventHandler canExecuteEventhandler;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
this.canExecuteEventhandler += value;
}
remove
{
this.canExecuteEventhandler -= value;
}
}
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return this.canExecute == null ? true : this.canExecute(parameter);
}
[DebuggerStepThrough]
public void Execute(object parameter)
{
this.execute(parameter);
}
public void InvokeCanExecuteChanged()
{
if (this.canExecute != null)
{
if (this.canExecuteEventhandler != null)
{
this.canExecuteEventhandler(this, EventArgs.Empty);
}
}
}
}
This is the implementation that I have used since I started developing in Silverlight around 2009. I have also used it in WPF applications.
Lately I understood that it has a memory leak problem in cases where the views that bind to the command have shorter life span than the command itself. Apparently when a button binds to the command, it of course registers to the CanExecuteChanged
event handler but never unregistered. The default event handlers hold strong reference to the delegate, which holds a strong reference to the button itself, therefore the RelayCommand
keeps the button alive and that's a memory leak.
Another implementation that I have found uses the CommandManager
. The CommandManager
exposes a a RequerySuggested
event and internally only hold weak references to the delegates. So the definition of the event can be implemented as follows (implementation B)
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
So that every delegate is passed to the static event handler instead of being held by the relay command itself. My problem with this implementation is that it relies on the CommandManager
to know when to raise the event. Also, when RaiseCanExecuteChanged
is called, the command manager raises this event for all RelayCommands
, not specifically the one that initiated the event.
The last implementation I found was from MvvmLight where the event is defined as such (implementation C):
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
{
// add event handler to local handler backing field in a thread safe manner
EventHandler handler2;
EventHandler canExecuteChanged = _requerySuggestedLocal;
do
{
handler2 = canExecuteChanged;
EventHandler handler3 = (EventHandler)Delegate.Combine(handler2, value);
canExecuteChanged = System.Threading.Interlocked.CompareExchange<EventHandler>(
ref _requerySuggestedLocal,
handler3,
handler2);
}
while (canExecuteChanged != handler2);
CommandManager.RequerySuggested += value;
}
}
remove
{
if (_canExecute != null)
{
// removes an event handler from local backing field in a thread safe manner
EventHandler handler2;
EventHandler canExecuteChanged = this._requerySuggestedLocal;
do
{
handler2 = canExecuteChanged;
EventHandler handler3 = (EventHandler)Delegate.Remove(handler2, value);
canExecuteChanged = System.Threading.Interlocked.CompareExchange<EventHandler>(
ref this._requerySuggestedLocal,
handler3,
handler2);
}
while (canExecuteChanged != handler2);
CommandManager.RequerySuggested -= value;
}
}
}
So in addition to the command manager it also holds the delegate locally and does some magic trick to support thread safety.
My questions are:
- Which of these implementations actually solve the memory leak problem.
- Is there an implementation that solves the problem without relying on the
CommandManager
? - Is the trick that is done in implementation C really necessary to avoid thread safety related bugs and how does it solve it?