2010-06-12 69 views
7

我有,我是一个ContextMenu绑定到一组ICommand来源的对象,并通过样式设置每个MenuItemCommandCommandParameter性能棘手的问题:ICommand.CanExecute传递空,即使CommandParameter设置

<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.CanExecute(object)(在菜单创建时调用)将通过空值。我已经检查过,选定的笔记集合在调用之前已经正确实例化(实际上它在其声明中被分配了一个值,所以它永远不会是null)。我无法弄清楚为什么CanEvaluate正在通过null

+0

我有完全相同的问题。我的解决方案是在命令参数后面绑定命令,只需将command参数的setter放在命令的setter之前,并且突然将绑定参数传递给CanExecute的第一个调用。 – Cubinator73 2016-08-30 15:15:13

回答

7

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

我花了整整一个晚上,一旦试图追查它会失败并寻找解决方法的确切条件。最后,我放弃了并切换到触发所需命令的Click处理程序。

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

我知道这个问题是使用自己的附加属性为指挥的CommandBinding而不是使用内置的那些最好的解决办法:

  • 当您连接命令属性设置,订阅单击MenuItem上的DataContextChanged事件,并订阅CommandManager.RequerySuggested。

  • 当DataContext更改,RequerySuggested进来或两个附加属性发生更改时,使用Dispatcher.BeginInvoke调度您的CanExecute()并更新MenuItem上的IsEnabled。

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

用法就像普通的命令和CommandParameter,但使用附加的属性,而不是:

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

此解决方案,并绕过所有与文本菜单的CanExecute处理的错误的问题。

希望有一天微软将解决ContextMenu的问题,并且此解决方法将不再需要。我有一个repro案件,坐在这里我打算提交给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已经为你做了这个。

+0

哇这是一个复杂的解决方案,想要做一件相当简单的事情。几个问题:我如何使用CommandManager.RequerySuggested(这是一个静态事件,我究竟在什么地方检查它?),除了Command和CommandParameter外,你提到的第三个附属属性是什么? – devios1 2010-06-12 14:30:52

+1

噢,我明白了,这个“仅供内部使用的附加属性”......我不能只订阅DataContextChanged吗? – devios1 2010-06-12 15:09:47

+0

得到它的工作! w00t!谢谢:)仍然好奇RequerySuggested寿......那究竟是什么? – devios1 2010-06-12 15:37:41

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(); 
         } 
        } 
    } 
    

命令执行:

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一起使用 。

+0

对我来说工作得很好 – Artiom 2015-06-11 15:03:54

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 delete命令未启用

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> 
相关问题