Why is CanExecute invoked after the command source is removed from the UI?

.net, binding, c#, mvvm, wpf

Solution

The issue is that the command source (i.e. the button) does not unsubscribed from `CanExecuteChanged` of the command it is bound to, so that whenever `CommandManager.RequerySuggested` fires, `CanExecute` fires as well, long after the command source is gone.

To solve this I implemented `IDisposable` on `RelayCommand`, and added the necessary code so that whenever a model object is removed, and so is removed from the UI, Dispose() is invoked on all its `RelayCommand`.

This is the modified `RelayCommand` (the original is here):

public class RelayCommand : ICommand, IDisposable
{
    #region Fields

    List<EventHandler> _canExecuteSubscribers = new List<EventHandler>();
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    #endregion // Constructors

    #region ICommand

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            _canExecuteSubscribers.Add(value);
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
            _canExecuteSubscribers.Remove(value);
        }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand

    #region IDisposable

    public void Dispose()
    {
        _canExecuteSubscribers.ForEach(h => CanExecuteChanged -= h);
        _canExecuteSubscribers.Clear();
    }

    #endregion // IDisposable
}

Wherever I use the above, I track all instantiated RelayCommands so I can invoke `Dispose()` when the time comes:

Dictionary<string, RelayCommand> _relayCommands 
    = new Dictionary<string, RelayCommand>();

public ICommand SomeCmd
{
    get
    {
        RelayCommand command;
        string commandName = "SomeCmd";
        if (_relayCommands.TryGetValue(commandName, out command))
            return command;
        command = new RelayCommand(
            param => {},
            param => true);
        return _relayCommands[commandName] = command;
    }
}

void Dispose()
{
    foreach (string commandName in _relayCommands.Keys)
        _relayCommands[commandName].Dispose();
    _relayCommands.Clear();
}

Problem

I am trying to understand why CanExecute is invoked on a command source that has been removed from the UI. Here is a simplified program to demonstrate: ``` <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="350" Width="525"> <StackPanel> <ListBox ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <Button Content="{Binding Txt}" Command="{Binding Act}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Content="Remove first item" Click="Button_Click" /> </StackPanel> </Window> ``` Code-behind: ``` public partial class MainWindow : Window { public class Foo { static int _seq = 0; int _txt = _seq++; RelayCommand _act; public bool Removed = false; public string Txt { get { return _txt.ToString(); } } public ICommand Act { get { if (_act == null) { _act = new RelayCommand( param => { }, param => { if (Removed) Console.WriteLine("Why is this happening?"); return true; }); } return _act; } } } public ObservableCollection<Foo> Items { get; set; } public MainWindow() { Items = new ObservableCollection<Foo>(); Items.Add(new Foo()); Items.Add(new Foo()); Items.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged); DataContext = this; InitializeComponent(); } void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Remove) foreach (Foo foo in e.OldItems) { foo.Removed = true; Console.WriteLine("Removed item marked 'Removed'"); } } void Button_Click(object sender, RoutedEventArgs e) { Items.RemoveAt(0); Console.WriteLine("Item removed"); } } ``` When I click the "Remove first item" button one time, I get this output: ``` Removed item marked 'Removed' Item removed Why is this happening? Why is this happening? ``` "Why is this happening?" keeps being printed each time I click on some empty part of the window. Why is this happening? And what can or should I do to prevent CanExecute from being invoked on removed command sources? Note: RelayCommand can be found here. Answers to Michael Edenfield questions: Q1: Callstack of when CanExecute is invoked on removed button: WpfApplication1.exe!WpfApplication1.MainWindow.Foo.get_Act.AnonymousMethod__1(object param) Line 30 WpfApplication1.exe!WpfApplication1.RelayCommand.CanExecute(object parameter) Line 41 + 0x1a bytes PresentationFramework.dll!MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource(System.Windows.Input.ICommandSource commandSource) + 0x8a bytes PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 bytes PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(object sender, System.EventArgs e) + 0x5 bytes PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(System.Collections.Generic.List handlers) + 0xac bytes PresentationCore.dll!System.Windows.Input.CommandManager.RaiseRequerySuggested(object obj) + 0xf bytes Q2: Also, does this keep happening if you remove all of the buttons from the list (not just the first?) Yes.

Original source

Related problems