I have a tricky problem where I am binding a ContextMenu
to a set of ICommand
-derived objects, and setting the Command
and CommandParameter
properties on each MenuItem
via a style:
<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}" />
...
However, while ICommand.Execute( object )
gets passed the set of selected notes as it should, ICommand.CanExecute( object )
(which is called when the menu is created) is getting passed null. I've checked and the selected notes collection is properly instantiated before the call is made (in fact it's assigned a value in its declaration, so it is never null
). I can't figure out why CanEvaluate is getting passed null
.
I believe this is related to the connect issue logged here:
https://connect.microsoft.com/VisualStudio/feedback/details/504976/command-canexecute-still-not-requeried-after-commandparameter-change?wa=wsignin1.0
My workaround is as follows:
Implement the interface in each command that needs to know about parameter changes.
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();
}
}
}
Command implementation:
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 Usage:
<Button Content="Save"
Command="{Binding SaveCommand}"
my:BoundCommand.Parameter="{Binding Document}" />
This is the simplest fix I could come up with and it works a treat for MVVM style implementations. You could also call CommandManager.InvalidateRequerySuggested() in the BoundCommand parameter change so that it worked with RoutedCommands as well.
I have determined that there are at least two bugs in ContextMenu that causes its CanExecute calls to be unreliable in different circumstances. It calls CanExecute immediately when the Command is set. Later calls are unpredictable and certainly not reliable.
I spent a whole night once trying to track down the precise conditions under which it would fail and looking for a workaround. Finally I gave up and switched to Click handlers that fired the desired commands.
I did determine that one of my problems was that changing the DataContext of the ContextMenu can cause CanExecute to be called before the new Command or CommandParameter is bound.
The best solution I know of to this problem is use your own attached properties for Command and CommandBinding instead of using the built-in ones:
When your attached Command property is set, subscribe to the Click and DataContextChanged events on the MenuItem, and also subscribe to CommandManager.RequerySuggested.
When the DataContext changes, RequerySuggested comes in, or either of your two attached properties changes, schedule a dispatcher operation using Dispatcher.BeginInvoke that will call your CanExecute() and update IsEnabled on the MenuItem.
When the Click event fires, do the CanExecute thing and if it passes, call Execute().
Usage is just like regular Command and CommandParameter, but using the attached properties instead:
<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" />
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" />
This solution works and bypasses all the problems with the bugs in ContextMenu's CanExecute handling.
Hopefully someday Microsoft will fix the problems with ContextMenu and this workaround will no longer be necessary. I have a repro case sitting around here somewhere that I intend to submit to Connect. Perhaps I should get on the ball and actually do it.
What is RequerySuggested, and why use it?
The RequerySuggested mechanism is RoutedCommand's way of efficiently handling ICommand.CanExecuteChanged. In the non-RoutedCommand world, each ICommand has its own list of subscribers to CanExecuteChanged, but for RoutedCommand, any client subscribing to ICommand.CanExecuteChanged will actually subscribe to CommandManager.RequerySuggested. This simpler model means that any time a RoutedCommand's CanExecute may change, all that is necessary is to call CommandManager.InvalidateRequerySuggested(), which will do the same things as firing ICommand.CanExecuteChanged but do it for all RoutedCommands simultaneously and on a background thread. In addition, RequerySuggested invocations are combined together so that if many changes occur the CanExecute only needs to be called once.
The reasons I recommended you subscribe to CommandManager.RequerySuggested instead of ICommand.CanExecuteChanged is: 1. You don't need code to removing your old subscription and add a new one every time the value of your Command attached property changes changes, and 2. CommandManager.RequerySuggested has a weak reference feature built in that allows you to set your event handler and still be garbage collected. Doing the same with ICommand requires you to implement your own weak reference mechanism.
The flip side of this is that if you subscribe to CommandManager.RequerySuggested instead of ICommand.CanExecuteChanged is that you will only get updates for RoutedCommands. I use RoutedCommands exclusively so this is not an issue for me, but I should have mentioned that if you use regular ICommands sometimes you should consider doing the extra work of weakly subscribing to ICommand.CanExecutedChanged. Note that if you do this, you don't need to subscribe to RequerySuggested as well, since RoutedCommand.add_CanExecutedChanged already does this for you.
I ran into this situation on a DataGrid
where I needed the context menu to recognize whether to enable or disable specific commands depending on the selected row. What I found was that yes the object passed to the command was null and that it was only executed once for all the rows regardless of whether there was a change or not.
What I did was to call RaiseCanExecuteChanged
on specific commands which would trigger an enable or disable in the grid's selection changed event.
private void MyGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
VM.DeleteItem.RaiseCanExecuteChanged();
}
The command binding assignment
VM.DeleteItem
= new OperationCommand((o) => MessageBox.Show("Delete Me"),
(o) => (myGrid.SelectedItem as Order)?.InProgress == false );
Result
Where an InProgress
is true
delete command is not enabled
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With