views:

53

answers:

1

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.

+2  A: 

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.

Ray Burns
Wow this is a complicated solution to a rather simple thing to want to do. Couple questions: how do I use CommandManager.RequerySuggested (it's a static event, what exactly do I check in it?), and what is the third attached property you mention, besides Command and CommandParameter?
chaiguy
Oh I see, this "internal-use-only attached property"... couldn't I just subscribe to DataContextChanged?
chaiguy
Got it working!! w00t! Thanks :) Still curious about RequerySuggested tho... what is that exactly?
chaiguy
Glad it worked for you. I didn't think of subscribing to DataContextChanged. That's better than using an extra attached property. I'll update my answer and also add some explanation about RequerySuggested.
Ray Burns
Alright, I was still having a bit of trouble with CanExecute still being called, but now I've somehow got it working perfectly so I think I'll tiptoe away and not touch anything, ever, again... (don't breathe!) lol thanks again.
chaiguy