为什么在从 UI 中删除命令源后会调用 CanExecute? [英] Why is CanExecute invoked after the command source is removed from the UI?

查看:48
本文介绍了为什么在从 UI 中删除命令源后会调用 CanExecute?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想了解为什么在已从 UI 中删除的命令源上调用 CanExecute.这是一个简化的程序来演示:

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>

代码隐藏:

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.

为什么会这样?我可以或应该做什么来防止 CanExecute 在已删除的命令源上被调用?

Why is this happening? And what can or should I do to prevent CanExecute from being invoked on removed command sources?

注意:可以在此处找到 RelayCommand.

Michael Edenfield 问题的答案:

Q1:在移除的按钮上调用 CanExecute 时的调用堆栈:

Q1: Callstack of when CanExecute is invoked on removed button:

WpfApplication1.exe!WpfApplication1.MainWindow.Foo.get_Act.AnonymousMethod__1(object param) 第 30 行WpfApplication1.exe!WpfApplication1.RelayCommand.CanExecute(object parameter) Line 41 + 0x1a bytesPresentationFramework.dll!MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource(System.Windows.Input.ICommandSource commandSource) + 0x8a 字节PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 字节 PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(object sender, System.EventArgs e) + 0x5 字节PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(System.Collections.Generic.List handlers) + 0xac 字节PresentationCore.dll!System.Windows.Input.CommandManager.RaiseRequerySuggested(object obj) + 0xf 字节

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

问题 2:此外,如果您从列表中删除所有按钮(不仅仅是第一个?),这种情况是否会继续发生?

Q2: Also, does this keep happening if you remove all of the buttons from the list (not just the first?)

是的.

推荐答案

问题是命令源(即按钮)没有取消订阅它所绑定的命令的CanExecuteChanged,所以每当 CommandManager.RequerySuggested 触发时,CanExecute 也会触发,在命令源消失很久之后.

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.

为了解决这个问题,我在 RelayCommand 上实现了 IDisposable,并添加了必要的代码,这样每当一个模型对象被移除时,Dispose() 被调用在其所有 RelayCommand 上.

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.

这是修改后的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
}

无论我在哪里使用上述,我都会跟踪所有实例化的 RelayCommands,以便我可以在时机成熟时调用 Dispose():

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();
}

这篇关于为什么在从 UI 中删除命令源后会调用 CanExecute?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆