c# - 为什么从 UI 中删除命令源后调用 CanExecute?

标签 c# .net wpf binding mvvm

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

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

当我单击“删除第一项”按钮一次时,我得到以下输出:

Removed item marked 'Removed'
Item removed
Why is this happening?
Why is this happening?

“为什么会这样?”每次我点击窗口的某个空白部分时,都会不断打印。

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

注意:可以找到 RelayCommand here .

Michael Edenfield 问题的答案:

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

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:另外,如果您从列表中删除所有按钮(不仅仅是第一个按钮),这种情况是否会继续发生?

是的。

最佳答案

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

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

这是修改后的 RelayCommand(原来是 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
}

无论我在哪里使用上面的代码,我都会跟踪所有实例化的 RelayCommand,这样我就可以在时机成熟时调用 Dispose():

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

关于c# - 为什么从 UI 中删除命令源后调用 CanExecute?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10280763/

相关文章:

c# - AutoMapper:将空源对象映射到十进制

c# - 局部函数和 SOLID 原则 C#

wpf - ComboBox 在底部有额外的空间

c# - WPF C# 应用程序产生两个主窗口的问题

c# - WPF Expander - 当 GridSplitter 用于手动调整行大小时,行无法正确折叠

针对字符串的 c# 单元测试日期时间

c# - 如何从一个字符串中只读取 20 个字符并与其他字符串进行比较?

c# - 调整大小时按钮消失

c# - 桌面应用程序中的 Application Insights 不会发送性能计数器

c# - C# .NET 中的 FLV/F4V 对话库?