Метод ICommand.CanExecute передается null, даже если установлен CommandParameter

у меня есть сложная проблема, когда я связываю 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}" />
...

Впрочем, в то время как ICommand.Execute( object ) получает передан набор выбранных нот, как это должно быть, ICommand.CanExecute( object ) (который вызывается при создании меню) получает переданное значение null. Я проверил, и выбранная коллекция notes правильно создана перед вызовом (на самом деле ей назначено значение в ее декларация, так что это никогда null). Я не могу понять, почему CanEvaluate передается null.

3 ответов


Я определил, что в ContextMenu есть по крайней мере две ошибки, которые заставляют его вызовы CanExecute быть ненадежными в разных обстоятельствах. Он вызывает CanExecute немедленно, когда команда установлена. Последующие звонки непредсказуемы и, конечно, ненадежны.

однажды я провел целую ночь, пытаясь отследить точные условия, при которых он потерпит неудачу и ищет обходной путь. Наконец, я сдался и переключился на обработчики щелчков, которые запускали желаемое команды.

Я определил, что одна из моих проблем заключалась в том, что изменение DataContext ContextMenu может вызвать CanExecute до привязки новой команды или CommandParameter.

лучшее решение, которое я знаю об этой проблеме, - использовать собственные вложенные свойства для Command и CommandBinding вместо использования встроенных:

  • если установлено свойство вложенной команды, подпишитесь на Click и DataContextChanged события в меню, а также подписаться на CommandManager.RequerySuggested.

  • когда DataContext изменяется, RequerySuggested входит или любое из ваших двух вложенных изменений свойств, запланируйте операцию диспетчера с помощью Диспетчера.BeginInvoke, который вызовет ваш CanExecute () и обновление IsEnabled на MenuItem.

  • когда событие Click срабатывает, выполните CanExecute, и если оно пройдет, вызовите Выполнять.)(

использование похоже на обычную команду и CommandParameter, но вместо этого использует вложенные свойства:

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

Это решение работает и обходит все проблемы с ошибками в обработке CanExecute ContextMenu.

надеюсь, когда-нибудь Microsoft исправит проблемы с ContextMenu, и это обходное решение больше не будет необходимо. У меня тут где-то лежит дело о повторном допросе, которое я намерен передать для связи. Возможно, мне стоит взять мяч и сделать это.

что RequerySuggested, и почему использует его?

механизм RequerySuggested-это способ routedcommand эффективно обрабатывать ICommand.CanExecuteChanged. В мире Non-RoutedCommand каждая ICommand имеет свой собственный список подписчиков на CanExecuteChanged, но для RoutedCommand любой клиент, подписавшийся на ICommand.CanExecuteChanged фактически подпишется на CommandManager.RequerySuggested. Этот более простая модель означает, что в любое время CanExecute RoutedCommand может измениться, все, что необходимо, это вызвать CommandManager.InvalidateRequerySuggested (), который будет делать то же самое, что и увольнение ICommand.CanExecuteChanged, но сделайте это для всех RoutedCommands одновременно и в фоновом потоке. Кроме того, вызовы RequerySuggested объединяются вместе, так что если происходит много изменений, CanExecute нужно вызвать только один раз.

причины, по которым я рекомендовал Вам подписаться В commandmanager.RequerySuggested вместо ICommand.CanExecuteChanged является следующим: 1. Вам не нужен код для удаления старой подписки и добавления новой каждый раз, когда значение вашей команды attached свойство изменяет изменения, и 2. В commandmanager.RequerySuggested имеет встроенную слабую справочную функцию, которая позволяет установить обработчик событий и по-прежнему собирать мусор. То же самое с ICommand требует от вас реализации собственного слабого ссылочного механизма.

обратная сторона это то, что если вы подписываетесь на CommandManager.RequerySuggested вместо ICommand.CanExecuteChanged, что вы будете получать обновления только для RoutedCommands. Я использую RoutedCommands исключительно, поэтому это не проблема для меня, но я должен был упомянуть, что если вы используете регулярные ICommands иногда, вы должны рассмотреть возможность выполнения дополнительной работы слабой подписки на ICommand.CanExecutedChanged. Обратите внимание, что если вы это сделаете, вам не нужно подписываться на RequerySuggested, так как RoutedCommand.add_CanExecutedChanged уже делает это за вас.


Я считаю, что это связано с проблемой подключения записываться здесь:

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. Вы также можете вызвать CommandManager.InvalidateRequerySuggested() в параметре BoundCommand измените так что это сработало с RoutedCommands, а также.


я столкнулся с этой ситуацией на DataGrid где мне нужно контекстное меню, чтобы узнать, включать или отключать определенные команды в зависимости от выбранной строки. Я обнаружил, что Yes объект, переданный команде, был 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 is true команда "удалить" не включено

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>