即使设置了CommandParameter,ICommand.CanExecute也会传递null

dev*_*os1 7 c# data-binding wpf contextmenu icommand

我有一个棘手的问题,我ContextMenu将一个ICommand-derived对象绑定到一个,并通过一个样式设置CommandCommandParameter属性MenuItem:

<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}" />
...
Run Code Online (Sandbox Code Playgroud)

然而,虽然ICommand.Execute( object )传递了应该选择的一组注释,ICommand.CanExecute( object )(在创建菜单时调用)将被传递为null.我已经检查过并且在调用之前正确地实例化了所选的注释集合(实际上它在其声明中被赋值,因此它永远不会null).我无法弄清楚CanEvaluate为什么会通过null.

Ray*_*rns 8

我已经确定ContextMenu中至少存在两个错误导致其CanExecute调用在不同情况下不可靠.设置命令时立即调用CanExecute.以后的电话是不可预测的,当然不可靠.

我花了整整一夜的时间试图找出它失败的确切条件并寻找解决方法.最后,我放弃并切换到点击处理程序,这些处理程序触发了所需的命令.

我确定我的一个问题是更改ContextMenu的DataContext会导致在绑定新的Command或CommandParameter之前调用CanExecute.

我知道这个问题的最佳解决方案是使用自己的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=... }" />
Run Code Online (Sandbox Code Playgroud)

此解决方案可以解决ContextMenu的CanExecute处理中的错误所带来的所有问题.

希望有一天微软将解决ContextMenu的问题,这种解决方法将不再是必要的.我有一个责备案例坐在这里,我打算提交给Connect.也许我应该接受球并且实际上做到了.

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

RequerySuggested机制是RoutedCommand有效处理ICommand.CanExecuteChanged的方法.在非RoutedCommand世界中,每个ICommand都有自己的CanExecuteChanged订户列表,但对于RoutedCommand,任何订阅ICommand.CanExecuteChanged的客户端实际上都会订阅CommandManager.RequerySuggested.这个更简单的模型意味着只要RoutedCommand的CanExecute可能发生变化,所有必要的就是调用CommandManager.InvalidateRequerySuggested(),它将执行与发出ICommand.CanExecuteChanged相同的功能,但同时为所有RoutedCommands和后台线程执行此操作.此外,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已经为您执行了此操作.


Fla*_*DOA 8

我相信这与此处记录的连接问题有关:

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();
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

命令实现:

    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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

Xaml用法:

    <Button Content="Save"
        Command="{Binding SaveCommand}"
        my:BoundCommand.Parameter="{Binding Document}" />
Run Code Online (Sandbox Code Playgroud)

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