【问题标题】:ICommand.CanExecute being passed null even though CommandParameter is set即使设置了 CommandParameter,ICommand.CanExecute 也被传递为 null
【发布时间】:2010-06-12 02:17:11
【问题描述】:

我有一个棘手的问题,我将ContextMenu 绑定到一组ICommand 派生对象,并通过样式在每个MenuItem 上设置CommandCommandParameter 属性:

<ContextMenu
    ItemsSource="{Binding Source={x:Static OrangeNote:Note.MultiCommands}}">
    <ContextMenu.Resources>
        <Style
            TargetType="MenuItem">
            <Setter
                Property="Header"
                Value="{Binding Path=Title}" />
            <Setter
                Property="Command"
                Value="{Binding}" />
            <Setter
                Property="CommandParameter"
                Value="{Binding Source={x:Static OrangeNote:App.Screen}, Path=SelectedNotes}" />
...

然而,虽然ICommand.Execute( object ) 得到了应有的选定注释集,但ICommand.CanExecute( object )(在创建菜单时调用)被传递为空。我已经检查并且在调用之前选择的笔记集合被正确地实例化(实际上它在它的声明中被分配了一个值,所以它永远不是null)。我不明白为什么 CanEvaluate 会通过null

【问题讨论】:

  • 我遇到了完全相同的问题。我的解决方案是在命令参数之后绑定命令,只需将命令参数的设置器放在命令设置器之前,然后绑定的参数突然被传递给CanExecute的第一次调用。

标签: c# wpf data-binding contextmenu icommand


【解决方案1】:

我已确定 ContextMenu 中至少有两个错误导致其 CanExecute 调用在不同情况下不可靠。设置命令后,它会立即调用 CanExecute。以后的调用是不可预测的,当然也不可靠。

有一次我花了整整一夜试图找出它会失败的确切条件并寻找解决方法。最后我放弃并切换到触发所需命令的 Click 处理程序。

我确实确定我的问题之一是更改 ContextMenu 的 DataContext 可能会导致 CanExecute 在新的 Command 或 CommandParameter 被绑定之前被调用。

我知道这个问题的最佳解决方案是使用您自己的 Command 和 CommandBinding 附加属性,而不是使用内置属性:

  • 设置附加的 Command 属性后,订阅 MenuItem 上的 Click 和 DataContextChanged 事件,同时订阅 CommandManager.RequerySuggested。

  • 当 DataContext 发生变化、RequerySuggested 出现或您的两个附加属性中的任何一个发生变化时,使用 Dispatcher.BeginInvoke 安排调度程序操作,该操作将调用您的 CanExecute() 并更新 MenuItem 上的 IsEnabled。

  • 当 Click 事件触发时,执行 CanExecute 操作,如果通过,则调用 Execute()。

用法和普通的 Command 和 CommandParameter 一样,但是使用附加的属性来代替:

<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" />
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" />

此解决方案有效并绕过了 ContextMenu 的 CanExecute 处理中的所有错误问题。

希望有一天微软会修复 ContextMenu 的问题,并且不再需要这种解决方法。我有一个复制案例坐在这里的某个地方,我打算提交给 Connect。或许我应该参与并实际去做。

什么是 RequerySuggested,为什么要使用它?

RequerySuggested 机制是 RoutedCommand 有效处理 ICommand.CanExecuteChanged 的​​方式。在非 RoutedCommand 世界中,每个 ICommand 都有自己的 CanExecuteChanged 订阅者列表,但对于 RoutedCommand,订阅 ICommand.CanExecuteChanged 的​​任何客户端实际上都会订阅 CommandManager.RequerySuggested。这个更简单的模型意味着任何时候 RoutedCommand 的 CanExecute 可能发生变化,只需调用 CommandManager.InvalidateRequerySuggested(),这将执行与触发 ICommand.CanExecuteChanged 相同的操作,但同时在后台线程上对所有 RoutedCommand 执行此操作。此外,RequerySuggested 调用组合在一起,因此如果发生许多更改,CanExecute 只需调用一次。

我建议您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged 的​​原因是: 1. 每次您的 Command 附加属性的值更改时,您不需要代码来删除旧订阅并添加新订阅, 2. CommandManager.RequerySuggested 具有内置的弱引用功能,允许您设置事件处理程序并仍然被垃圾收集。对 ICommand 执行相同操作需要您实现自己的弱引用机制。

另一方面,如果您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged,您将只能获得 RoutedCommands 的更新。我专门使用 RoutedCommands,所以这对我来说不是问题,但我应该提到,如果你使用常规 ICommands,有时你应该考虑做一些额外的工作,即弱订阅 ICommand.CanExecutedChanged。请注意,如果您这样做,您也不需要订阅 RequerySuggested,因为 RoutedCommand.add_CanExecutedChanged 已经为您完成了这项工作。

【讨论】:

  • 哇,这是一个复杂的解决方案,解决了一件相当简单的事情。几个问题:我如何使用 CommandManager.RequerySuggested(这是一个静态事件,我究竟要检查什么?),除了 Command 和 CommandParameter,你提到的第三个附加属性是什么?
  • 哦,我明白了,这个“仅供内部使用的附加属性”……我不能只订阅 DataContextChanged 吗?
  • 搞定了!! w00t!谢谢 :) 仍然对 RequerySuggested 感到好奇……那到底是什么?
  • 很高兴它对你有用。我没想过订阅 DataContextChanged。这比使用额外的附加属性要好。我将更新我的答案并添加一些关于 RequerySuggested 的解释。
  • 好吧,我仍然在调用 CanExecute 时遇到了一些问题,但现在我已经让它完美地工作了,所以我想我会踮起脚尖,再也不碰任何东西...(不要呼吸!)哈哈,再次感谢。
【解决方案2】:

我认为这与此处记录的连接问题有关:

https://connect.microsoft.com/VisualStudio/feedback/details/504976/command-canexecute-still-not-requeried-after-commandparameter-change?wa=wsignin1.0

我的解决方法如下:

  1. 创建一个带有附加依赖属性和绑定命令参数的静态类
  2. 创建自定义界面,用于在自定义命令上手动引发 CanExecuteChanged
  3. 在每个需要知道参数变化的命令中实现接口。

    public interface ICanExecuteChanged : ICommand
    {
        void RaiseCanExecuteChanged();
    }
    
    public static class BoundCommand
    {
        public static object GetParameter(DependencyObject obj)
        {
            return (object)obj.GetValue(ParameterProperty);
        }
    
        public static void SetParameter(DependencyObject obj, object value)
        {
            obj.SetValue(ParameterProperty, value);
        }
    
        public static readonly DependencyProperty ParameterProperty = DependencyProperty.RegisterAttached("Parameter", typeof(object), typeof(BoundCommand), new UIPropertyMetadata(null, ParameterChanged));
    
        private static void ParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var button = d as ButtonBase;
            if (button == null)
            {
                return;
            }
    
            button.CommandParameter = e.NewValue;
            var cmd = button.Command as ICanExecuteChanged;
            if (cmd != null)
            {
                cmd.RaiseCanExecuteChanged();
            }
        }
    }
    

命令实现:

    public class MyCustomCommand : ICanExecuteChanged
    {
        public void Execute(object parameter)
        {
            // Execute the command
        }

        public bool CanExecute(object parameter)
        {
            Debug.WriteLine("Parameter changed to {0}!", parameter);
            return parameter != null;
        }

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged()
        {
            EventHandler temp = this.CanExecuteChanged;
            if (temp != null)
            {
                temp(this, EventArgs.Empty);
            }
        }
    }

Xaml 用法:

    <Button Content="Save"
        Command="{Binding SaveCommand}"
        my:BoundCommand.Parameter="{Binding Document}" />

这是我能想到的最简单的修复方法,它适用于 MVVM 风格的实现。 您还可以在 BoundCommand 参数更改中调用 CommandManager.InvalidateRequerySuggested() 以便 它也适用于 RoutedCommands。

【讨论】:

  • 对我来说效果很好
【解决方案3】:

我在DataGrid 上遇到了这种情况,我需要上下文菜单来识别是根据所选行启用还是禁用特定命令。我发现是,传递给命令的对象是空的,并且它只对所有行执行一次,无论是否有更改。

我所做的是在特定命令上调用RaiseCanExecuteChanged,这将在网格的选择更改事件中触发启用或禁用。


private void MyGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    VM.DeleteItem.RaiseCanExecuteChanged();
}

命令绑定赋值

VM.DeleteItem 
    = new OperationCommand((o) => MessageBox.Show("Delete Me"),
                           (o) => (myGrid.SelectedItem as Order)?.InProgress == false );

结果

InProgress 所在的true 删除命令未启用

XAML

<DataGrid AutoGenerateColumns="True"
        Name="myGrid"
        ItemsSource="{Binding Orders}"
        SelectionChanged="MyGrid_OnSelectionChanged">
    <DataGrid.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Copy"   Command="{Binding CopyItem}"/>
            <MenuItem Header="Delete" Command="{Binding DeleteItem}" />
        </ContextMenu>
    </DataGrid.ContextMenu>
</DataGrid>

【讨论】:

    猜你喜欢
    • 2016-02-06
    • 2015-11-05
    • 1970-01-01
    • 1970-01-01
    • 2017-05-16
    • 1970-01-01
    • 2012-08-26
    • 1970-01-01
    • 2014-11-23
    相关资源
    最近更新 更多