即使已设置CommandParameter,仍会传递null到ICommand.CanExecute。

10

我有一个棘手的问题,我正在将一个 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。我已经检查过,在调用之前所选音符的集合已经被正确实例化(实际上在其声明中被赋值,因此它永远不会为null)。我无法弄清楚为什么 CanEvaluate 被传入了 null


1
我曾经遇到过完全相同的问题。我的解决方案是在命令参数后绑定命令,只需在命令的设置器之前放置命令参数的设置器,突然间绑定的参数就被传递给了第一次调用 CanExecute - Cubi73
3个回答

11

我已经确定在ContextMenu中至少存在两个错误,导致其CanExecute调用在不同情况下不可靠。它会在设置Command时立即调用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处理中出现的所有问题。

希望有一天Microsoft能够解决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已经为您完成了这项工作。


哇,这是一个对于想要做的事情而言相当简单的解决方案,但却很复杂。有几个问题:我该如何使用CommandManager.RequerySuggested(它是一个静态事件,我应该检查其中的什么内容?),以及除了Command和CommandParameter之外,你提到的第三个附加属性是什么? - devios1
1
哦,我明白了,这个“仅限内部使用的附加属性”……我能不能只订阅DataContextChanged事件呢? - devios1
搞定了!!耶!谢谢 :) 不过还是很好奇RequerySuggested是什么东西...它具体是什么? - devios1
很高兴它对你有用。我没有想到订阅DataContextChanged。这比使用额外的附加属性更好。我会更新我的答案,并添加一些关于RequerySuggested的解释。 - Ray Burns
好的,我仍然有一点问题,CanExecute仍然被调用,但现在它完美地工作了,所以我想我会悄悄地走开,不再碰任何东西,永远不要再碰了...(不要呼吸!)哈哈,再次感谢。 - devios1

10

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

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。


对我而言完美运行。 - Artiom

1
我在一个 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 );

结果

InProgresstrue 时,删除命令不可用。

enter image description here

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>

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