为什么在从 UI 中删除命令源后调用 CanExecute
本文关键字:调用 CanExecute 命令 删除 UI 为什么 | 更新日期: 2023-09-27 18:31:56
我试图理解为什么在已从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?
注意:中继命令可以在这里找到。
迈克尔·伊登菲尔德问题的答案:
Q1:在删除按钮上调用CanExecute时的调用堆栈:
WPF应用程序1.exe!WpfApplication1.MainWindow.Foo.get_Act.匿名方法__1(对象参数) 第 30 行 WPF应用程序1.exe!wpfApplication1.RelayCommand.CanExecute(object parameter) 第 41 行 + 0x1a 字节 PresentationFramework.dll!MS.Internal.Command.CommandHelpers.CanExecuteCommandSource(System.Windows.Input.ICommandSource commandSource) + 0x8a 字节 演示框架.dll!System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute() + 0x18 bytes PresentationFramework.dll!System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(object sender, System.EventArgs e) + 0x5 bytes 演示核心.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(System.Collections.Generic.List handlers) + 0xac 字节 演示核心.dll!System.Windows.Input.CommandManager.RaiseRequerySuggested(object obj) + 0xf bytes
Q2:另外,如果您从列表中删除所有按钮(而不仅仅是第一个按钮),这种情况会继续发生吗?
是的。
问题是命令源(即按钮)不会取消订阅它绑定到的命令CanExecuteChanged
,因此每当CommandManager.RequerySuggested
触发时,CanExecute
也会触发,在命令源消失很久之后。
为了解决这个问题,我在RelayCommand
上实现了IDisposable
,并添加了必要的代码,以便每当从UI中删除模型对象时,都会调用Dispose()在其所有RelayCommand
.
这是修改后的RelayCommand
(原文在这里):
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();
}
使用 lambda 表达式和您似乎正在触发的事件存在已知问题。我不愿称其为"错误",因为我对内部细节的了解不够,无法知道这是否是预期的行为,但这对我来说肯定是违反直觉的。
此处的关键指示是调用堆栈的这一部分:
PresentationCore.dll!System.Windows.Input.CommandManager.CallWeakReferenceHandlers(
System.Collections.Generic.List handlers) + 0xac bytes
"弱"事件是一种挂接不使目标对象保持活动状态的事件的方法;这里使用它是因为您将 lamba 表达式作为事件处理程序传递,因此包含该方法的"对象"是内部生成的匿名对象。问题在于,传递给事件add
处理程序的对象与传入remove
事件的表达式实例不同,它只是一个功能相同的对象,因此它不会从事件中取消订阅。
有几种解决方法,如以下问题中所述:
用于 lambda 的弱事件处理程序模型
在 C# 中使用 Lambda 取消挂钩事件
使用 lambda 作为事件处理程序会导致内存泄漏吗?
对于您的情况,最简单的方法是将 CanExecute 和 Execute 代码移动到实际方法中:
if (_act == null) {
_act = new RelayCommand(this.DoCommand, this.CanDoCommand);
}
private void DoCommand(object parameter)
{
}
private bool CanDoCommand(object parameter)
{
if (Removed)
Console.WriteLine("Why is this happening?");
return true;
}
或者,如果您可以安排对象从 lambda 构造Action<>
和Func<>
委托一次,将它们存储在变量中,并在创建RelayCommand
时使用这些变量,它将强制使用相同的实例。IMO,对于您的情况,这可能比需要的要复杂得多。