2010-04-06 3 views
12

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

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

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

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

Это 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> 
+1

хороший материал ... я искал, как сделать это сегодня! +1 – kevchadders

ответ

21

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

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

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

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

4

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

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

Если вы обнаружите, что это занимает слишком много времени, check out the answer to this question.

2

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

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

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

Пожалуйста, см. [Мой ответ] (http://stackoverflow.com/a/10924171/435522) – alexei

1

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

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

  1. Рекомендуется, чтобы исполнители ICommand.CanExecuteChanged держать только слабые ссылки на обработчик (см this answer).
  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.

UPDATE: представленный an issue on CodePlex

+0

спасибо, это решило мою проблему – Tobias

0

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

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

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