WPF ViewModel Команды CanExecute проблема - PullRequest
12 голосов
/ 07 апреля 2010

У меня возникли некоторые трудности с командами контекстного меню в моей модели представления.

Я реализую интерфейс ICommand для каждой команды в модели представления, затем создаю ContextMenu в ресурсах представления (MainWindow) и использование CommandReference из MVVMToolkit для доступа к текущим командам DataContext (ViewModel).

Когда я отлаживаю приложение, создается впечатление, что метод CanExecute в команде не вызывается, кроме как при созданииокно, поэтому мои элементы контекстного меню не включаются и не отключаются, как я ожидал.

Я подготовил простой пример ( прикреплен здесь ), который указывает на мое фактическое применениеи резюмируется ниже.Любая помощь будет принята с благодарностью!

Это ViewModel

namespace WpfCommandTest
{
    public class MainWindowViewModel
    {
        private List<string> data = new List<string>{ "One", "Two", "Three" };

        // This is to simplify this example - normally we would link to
        // Domain Model properties
        public List<string> TestData
        {
            get { return data; }
            set { data = value; }
        }

        // Bound Property for listview
        public string SelectedItem { get; set; }

        // Command to execute
        public ICommand DisplayValue { get; private set; }

        public MainWindowViewModel()
        {
            DisplayValue = new DisplayValueCommand(this);
        }

    }
}

Команда DisplayValueCommand такова:

public class DisplayValueCommand : ICommand
{
    private MainWindowViewModel viewModel;

    public DisplayValueCommand(MainWindowViewModel viewModel)
    {
        this.viewModel = viewModel;
    }

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        if (viewModel.SelectedItem != null)
        {
            return viewModel.SelectedItem.Length == 3;
        }
        else return false;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        MessageBox.Show(viewModel.SelectedItem);
    }

    #endregion
}

И, наконец, представление определено в Xaml:

<Window x:Class="WpfCommandTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfCommandTest"
    xmlns:mvvmtk="clr-namespace:MVVMToolkit"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>

        <mvvmtk:CommandReference x:Key="showMessageCommandReference" Command="{Binding DisplayValue}" />

        <ContextMenu x:Key="listContextMenu">
            <MenuItem Header="Show MessageBox" Command="{StaticResource showMessageCommandReference}"/>
        </ContextMenu>

    </Window.Resources>

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <ListBox ItemsSource="{Binding TestData}" ContextMenu="{StaticResource listContextMenu}" 
                 SelectedItem="{Binding SelectedItem}" />
    </Grid>
</Window>

Ответы [ 5 ]

21 голосов
/ 07 апреля 2010

Чтобы завершить ответ Уилла, вот «стандартная» реализация события CanExecuteChanged:

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

(из класса RelayCommand Джоша Смита)

Кстати, вывероятно, стоит подумать об использовании RelayCommand или DelegateCommand: вы быстро устанете создавать новые классы команд для каждой вашей команды ViewModels ...

4 голосов
/ 07 апреля 2010

Вы должны отслеживать, когда изменился статус CanExecute, и инициировать событие ICommand.CanExecuteChanged.

Кроме того, вы можете обнаружить, что это не всегда работает, и в этих случаях требуется вызов CommandManager.InvalidateRequerySuggested(), чтобы пнуть менеджер команд в задницу.

Если вы обнаружите, чтоэто занимает слишком много времени, проверьте ответ на этот вопрос.

2 голосов
/ 07 апреля 2010

Спасибо за быстрые ответы. Этот подход работает, если вы, например, привязываете команды к стандартной кнопке в окне (которая имеет доступ к модели представления через свой DataContext); CanExecute вызывается довольно часто при использовании CommandManager, как вы предлагаете в классах реализации ICommand или с помощью RelayCommand и DelegateCommand.

Однако, связывание тех же команд через CommandReference в ContextMenu не действуйте таким же образом.

Для того же поведения я должен также включить EventHandler из RelayCommand Джоша Смита в CommandReference, но при этом я должен закомментировать некоторый код из метода OnCommandChanged. Я не совсем уверен, почему он существует, возможно, он предотвращает утечки памяти событий (по-видимому!)?

  public class CommandReference : Freezable, ICommand
    {
        public CommandReference()
        {
            // Blank
        }

        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandReference), new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            if (Command != null)
                return Command.CanExecute(parameter);
            return false;
        }

        public void Execute(object parameter)
        {
            Command.Execute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandReference commandReference = d as CommandReference;
            ICommand oldCommand = e.OldValue as ICommand;
            ICommand newCommand = e.NewValue as ICommand;

            //if (oldCommand != null)
            //{
            //    oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;
            //}
            //if (newCommand != null)
            //{
            //    newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;
            //}
        }

        #endregion

        #region Freezable

        protected override Freezable CreateInstanceCore()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
1 голос
/ 23 ноября 2017

Более простым решением для меня было установить CommandTarget для MenuItem.

<MenuItem Header="Cut" Command="Cut" CommandTarget="
      {Binding Path=PlacementTarget, 
      RelativeSource={RelativeSource FindAncestor, 
      AncestorType={x:Type ContextMenu}}}"/>

Подробнее: http://www.wpftutorial.net/RoutedCommandsInContextMenu.html

1 голос
/ 07 июня 2012

Однако связывание тех же команд через CommandReference в ContextMenu не действует таким же образом.

Это ошибка в реализации CommandReference. Из этих двух пунктов следует:

  1. Рекомендуется, чтобы реализация ICommand.CanExecuteChanged содержала только слабые ссылки на обработчики (см. этот ответ ).
  2. Потребители ICommand.CanExecuteChanged должны ожидать (1) и, следовательно, должны содержать строгие ссылки на обработчики, которые они регистрируют в ICommand.CanExecuteChanged

Общие реализации RelayCommand и DelegateCommand соблюдают (1). Реализация CommandReference не соблюдает (2), когда подписывается на newCommand.CanExecuteChanged. Таким образом, объект-обработчик собирается и после этого CommandReference больше не получает никаких уведомлений, на которые он рассчитывал.

Исправление заключается в том, чтобы удерживать строгую ссылку на обработчик в CommandReference:

    private EventHandler _commandCanExecuteChangedHandler;
    public event EventHandler CanExecuteChanged;

    ...
    if (oldCommand != null)
    {
        oldCommand.CanExecuteChanged -= commandReference._commandCanExecuteChangedHandler;
    }
    if (newCommand != null)
    {
        commandReference._commandCanExecuteChangedHandler = commandReference.Command_CanExecuteChanged;
        newCommand.CanExecuteChanged += commandReference._commandCanExecuteChangedHandler;
    }
    ...

    private void Command_CanExecuteChanged(object sender, EventArgs e)
    {
        if (CanExecuteChanged != null)
            CanExecuteChanged(this, e);
    }

Для того же поведения, я должен также включить EventHandler из RelayCommand Джоша Смита, в CommandReference, но в процессе поэтому я должен закомментировать некоторый код из OnCommandChanged Метод. Я не совсем уверен, почему это там, возможно, это предотвращение утечек памяти событий (на догадку!)?

Обратите внимание, что ваш подход перенаправления подписки на CommandManager.RequerySuggested также устраняет ошибку (больше нет обработчика без ссылок для начала), но он препятствует функциональности CommandReference. Команда, с которой связан CommandReference, может вызывать CanExecuteChanged напрямую (вместо того, чтобы полагаться на CommandManager для выдачи запроса на запрос), но это событие будет проглочено и никогда не достигнет источника команды, привязанного к CommandReference. Это также должно ответить на ваш вопрос о том, почему CommandReference реализован путем подписки на newCommand.CanExecuteChanged.

ОБНОВЛЕНИЕ: отправлено проблема в CodePlex

...