ICommand.CanExecute being passed null even though CommandParameter is set

半世苍凉 提交于 2019-12-04 21:16:25

问题


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.


回答1:


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.




回答2:


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:

  1. Create a static class with an attached dependency property a bound command parameter
  2. Create a custom interface for manually raising CanExecuteChanged on a custom command
  3. 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.




回答3:


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>


来源:https://stackoverflow.com/questions/3027224/icommand-canexecute-being-passed-null-even-though-commandparameter-is-set

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!