我有一个棘手的问题,我将ContextMenu
绑定到一组ICommand
派生的对象,并在每个{Command
和CommandParameter
属性上设置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}" />
...
属性1}}通过风格:
ICommand.Execute( object )
然而,当ICommand.CanExecute( object )
传递所选择的注释集时,null
(在创建菜单时调用)将被传递为null。我已经检查过并且在调用之前正确地实例化了所选的注释集合(实际上它在其声明中被赋值,因此它永远不会是null
)。我无法弄清楚为什么CanEvaluate会通过{{1}}。
答案 0 :(得分: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=... }" />
此解决方案可以解决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已经为您执行此操作。
答案 1 :(得分:8)
我认为这与此处记录的连接问题有关:
我的解决方法如下:
在每个需要了解参数更改的命令中实现接口。
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。
答案 2 :(得分:1)
我在DataGrid
遇到了这种情况,我需要上下文菜单来识别是否根据所选行启用或禁用特定命令。我发现是的,传递给命令的对象是null,并且只对所有行执行一次,无论是否有更改。
我所做的是在特定命令上调用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>