Команды 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>
Ответы
Ответ 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