Подтвердить что ты не робот

Команды WPM ViewModel CanExecute

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

Я реализую интерфейс ICommand для каждой команды в Model View, а затем создаю ContextMenu в ресурсах View (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>
4b9b3361

Ответ 1

Завершить Ответ будет отвечать "стандартная" реализация события CanExecuteChanged:

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Для такого же поведения я должен также включить EventHandler из Josh Smith 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
    }

Ответ 4

Однако, связывание одних и тех же команд с помощью CommandReference в ContextMenu не действуют одинаково.

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

  • Рекомендуется, чтобы исполнители ICommand.CanExecuteChanged сохраняли только слабые ссылки на обработчики (см. этот ответ).
  • Потребители 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 от Josh Smith RelayCommand, в CommandReference, но в выполнении поэтому я должен прокомментировать какой-то код из OnCommandChanged Метод. Я не совсем уверен, почему он там, возможно, это предотвращение утечек памяти событий (при догадках!)?

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

UPDATE: отправлено проблема с CodePlex

Ответ 5

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

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

Дополнительная информация: http://www.wpftutorial.net/RoutedCommandsInContextMenu.html