人们为什么在ICommands上使用CommandManager.InvalidateRequerySuggested()?

19

我正在自己制作一些自定义ICommand的实现,我看到很多实现都像这样:

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

protected void RaiseCanExecuteChanged()
{        
    CommandManager.InvalidateRequerySuggested();
}

据我所见,这段代码优化得很差,因为调用RaiseCanExecuteChanged()将触发UI中的所有命令检查它们的ICommand.CanExecute状态,而通常我们只希望其中一个验证它。

我记得曾经阅读过关于一些WPF ICommands的主要代码,比如RoutedCommand,对于它们来说是有意义的,因为它们希望在某个控件失去焦点或其他类似情况时自动重新验证所有ICommand,但我仍然不明白为什么人们会为自己的ICommand实现重复这种模式。

我所考虑的代码是简单的事件调用,例如:

public event EventHandler CanExecuteChanged;

protected void RaiseCanExecuteChanged()
{        
    CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

我测试过这个方法是有效的,那么为什么网上的所有示例都没有实现这么简单的东西?难道我错过了什么吗?

我读过在普通事件中使用强引用可能导致内存泄漏问题的文章,在这种情况下,CommandManager只使用WeakReferences,这种方式可以确保视图被垃圾回收,但是,是否有一些解决方案不会在内存占用和性能之间做出妥协呢?


https://dev59.com/53E95IYBdhLWcg3whOHK - ASh
5个回答

7
为什么网上的所有示例都没有实现这么简单的东西?我有什么遗漏的吗?
我猜主要是因为懒惰...您提出的确实是更好(更有效)的实现。但是,它还不完整:您仍然需要订阅 CommandManager.RequerySuggested 以在命令上引发 CanExecuteChanged

为什么?我测试过了,它可以正常工作... 我错过了哪些情况? - Michel Feinstein
@mFeinstein,但它应该。这就是调用CommandManager.InvalidateRequerySuggested()的整个目的:确保重新评估所有命令的CanExecute()。你说得对,命令本身不应该调用它,但其他代码可以(而且确实)调用它。 - Thomas Levesque
嗯,是的,那是显而易见的答案,但我不熟悉内部的WPF引擎,可能还有很多其他方面,特别是在CommandManager方面。在我了解系统更多信息之前,我通常会尽量不做任何假设。 - Michel Feinstein
3
就我所知,这是我的典型实现:https://gist.github.com/thomaslevesque/13c3e69f6c5ae219b945fc07b75843fc - Thomas Levesque
1
@mFeinstein 这并不重要,因为“RequerySuggested”只保留了一个弱引用。 - Thomas Levesque
显示剩余11条评论

4
非常简单 - 如果在ICommand.CanExecute()中执行了大量的工作,那么你非常糟糕地使用了Commands。如果你遵循这条规则,实际上调用CommandManager.InvalidateRequerySuggested()不应该有任何严重的性能影响。
实用主义来看,这比你建议的实现要容易得多。
个人而言,我更喜欢在特定的ViewModel中调用CommandManager.InvalidateRequerySuggested(),当属性变化时,这样对用户的反馈是即时的(例如:一旦表单完成/有效,立即启用按钮)。

很抱歉,我不明白您的观点,一个五行代码的解决方案怎么可能比三行代码的“编程上更容易”呢? - Michel Feinstein
这更简单,因为您不必让每个更改都调用RaiseCanExecuteChanged来影响该属性的每个Command - toadflakz
4
从这个意义上讲,是可以的,但我认为它在概念上有缺陷。RaiseCanExecuteChanged 是为了使 ViewModel 能够通知特定的 Command(仅限它),其 CanExecute 状态已更改。而 CommandManager 则是通知所有命令。我无法想象任何编程情况下,激活所有对象以激活一个对象是被视为良好实践的。这就像在街上大喊大叫,让每个人都打开门,而不是按你要找的那扇门的门铃......但我仍然理解这种简单性,如果你保持了简单的 CanExecute 并且只有少量命令。 - Michel Feinstein
我可以。尝试使用文字处理应用程序的文本/段落格式化栏。如果您的文档和格式命令主机是完全分离的“ViewModel”类,则应使用“CommandManager.InvalidateRequerySuggested”,因为您不希望有多个“ViewModel”/“Command”类的依赖关系,并且必须手动维护这些关系。 - toadflakz
1
在这种情况下,我认为您需要一种特殊类型的命令,它的行为方式就像这样,而不是所有命令都应该像这样。 - Michel Feinstein

1
这个问题很古老了,现在是2019年,但我发现使用CommandManager.InvalidateRequerySuggested()的另一个原因。
我为WPF应用编写了自己的自定义ICommand类,在其中直接调用CanExecuteChanged。
public void RaiseCanExecuteChanged()
{
    CanExecuteChanged?.Invoke(this, null); 
}

我的WPF应用程序大量使用不同的线程,当以上方法被从另一个线程而非主UI线程调用时,它不会抛出错误,只是被忽略了。更糟糕的是,我发现调用方法内的所有代码行都被跳过,这导致了奇怪的结果。
我不确定原因是什么,但我猜测原因是CanExecuteChanged导致了UI的更改,而UI不能从另一个线程中更改。
然而,当我将我的ICommand更改为CommandManager.InvalidateRequerySuggested()时,问题就解决了。似乎CommandManager.InvalidateRequerySuggested()可以从任何线程调用,UI仍然会更新。
public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

public void RaiseCanExecuteChanged()
{
    CommandManager.InvalidateRequerySuggested();
}

我认为这可能是一个有价值的答案,因为在找到这个解决方案之前,我调试了3个小时。找到这个问题的难点在于,在调试过程中没有抛出任何错误,代码只是被跳过了。非常奇怪的行为。


1
这是对此答案的回复。确实,CanExecuteChanged?.Invoke(this, null);必须由主UI线程调用。
只需按以下方式编写即可:
public void RaiseCanExecuteChanged()
{
    Application.Current.Dispatcher.Invoke(() => CanExecuteChanged?.Invoke(this, null)); 
}

这可以解决您的问题,您只需重新查询一个命令。但是确实应该尽可能使您的CanExecute方法快速执行,因为它将定期执行。 最好让CanExecute只包含一个return foo;,其中foo是在调用CommandManager.InvalidateRequerySuggested();之前可以设置的字段。

0

我长期以来一直使用更简单的解决方案,并没有遇到任何问题。我已经在.NET 4.7上进行了测试,事件已被取消订阅。因此,我认为使用CommandManager没有任何意义。

public class CanExecuteTestViewModel : INotifyPropertyChanged
{
    public CanExecuteTestViewModel()
    {
        AddCommand = new MyCommand(_ => Item = new Item());
        RemoveCommand = new MyCommand(_ => Item = null);
        TestCommand = new MyCommand(delegate { });
    }

    public MyCommand AddCommand { get; }
    public MyCommand RemoveCommand { get; }
    public MyCommand TestCommand { get; }

    private Item _item;
    public Item Item { get { return _item; } set { _item = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Item))); } }

    public event PropertyChangedEventHandler PropertyChanged;
}
public class Item { }

public class MyCommand : INotifyPropertyChanged, ICommand
{
    private readonly Action<object> _execute;

    public MyCommand(Action<object> execute)
    {
        _execute = execute;
    }

    private event EventHandler _canExecuteChanged;
    public event EventHandler CanExecuteChanged
    {
        add { _canExecuteChanged += value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SubscriberCount))); }
        remove { _canExecuteChanged -= value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SubscriberCount))); }
    }

    public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
    public bool CanExecute(object parameter) => true;
    public void Execute(object parameter) => _execute.Invoke(parameter);
    public event PropertyChangedEventHandler PropertyChanged;

    public int SubscriberCount { get { return _canExecuteChanged?.GetInvocationList().Length ?? 0; } }
}

和用户界面

<Window x:Class="WpfTest.CanExecuteTest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfTest"
        mc:Ignorable="d"
        x:Name="root"
        Height="450" Width="800">
    <Window.DataContext>
        <local:CanExecuteTestViewModel/>
    </Window.DataContext>
    <DockPanel>
        <TextBlock Text="{Binding TestCommand.SubscriberCount}" DockPanel.Dock="Top"/>
        <Button Content="Add" Command="{Binding AddCommand}" DockPanel.Dock="Top"/>
        <Button Content="Remove" Command="{Binding RemoveCommand}" DockPanel.Dock="Top"/>
        <ContentControl Content="{Binding Item}">
            <ContentControl.Resources>
                <DataTemplate DataType="{x:Type local:Item}">
                    <Button Content="Test" Command="{Binding DataContext.TestCommand, ElementName=root}"/>
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>
    </DockPanel>
</Window>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接