ICommand.CanExecute передается нулевым, даже если установлен CommandParameter - PullRequest
7 голосов
/ 12 июня 2010

У меня есть сложная проблема, когда я связываю 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.Я проверил, и выбранная коллекция заметок должным образом создается перед вызовом (на самом деле ему присваивается значение в объявлении, поэтому оно никогда не будет null).Я не могу понять, почему CanEvaluate проходит мимо null.

Ответы [ 3 ]

8 голосов
/ 04 февраля 2011

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

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.

8 голосов
/ 12 июня 2010

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

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

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

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

  • Когда установлено ваше присоединенное свойство Command, подпишитесь на события Click и DataContextChanged в MenuItem, а также подпишитесь на CommandManager.RequerySuggested.

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

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

Использование аналогично обычным Command и CommandParameter, но вместо этого используется присоединенное свойство:

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

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

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

Что такое RequerySuggested и зачем его использовать?

Механизм RequerySuggested - это способ RoutedCommand для эффективной обработки ICommand.CanExecuteChanged. В мире, отличном от RoutedCommand, каждая ICommand имеет свой собственный список подписчиков CanExecuteChanged, но для RoutedCommand любой клиент, подписавшийся на ICommand.CanExecuteChanged, фактически подписывается на CommandManager.RequerySuggested. Эта более простая модель означает, что каждый раз, когда CanExecute RoutedCommand может измениться, все, что необходимо, это вызвать 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 голос
/ 04 февраля 2017

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

Что я сделал, так это позвонил 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 Удалить командуне включено

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>
...