example commandmanager wpf binding command contextmenu icommand
adjunta aquí

commandmanager - icommand wpf



Comandos ViewModel de WPF Problema CanExecute (5)

Tengo algunas dificultades con los comandos del menú contextual en mi modelo de visualización.

Estoy implementando la interfaz de ICommand para cada comando dentro del Modelo de Vista, luego creo un ContextMenu dentro de los recursos de la Vista (MainWindow) y usando una Referencia de Comando de MVVMToolkit para acceder a los Comandos actuales de DataContext (ViewModel).

Cuando depuro la aplicación, parece que no se está llamando al método CanExecute en el comando, excepto en la creación de la ventana, por lo tanto, mis elementos contextuales del menú no se están habilitando o deshabilitados como hubiera esperado.

He preparado una muestra simple ( adjunta aquí ) que es indicativa de mi aplicación real y se resume a continuación. ¡Cualquier ayuda sería muy apreciada!

Este es el 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); } } }

El DisplayValueCommand es tal:

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 }

Y finalmente, la vista se define en 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>


Gracias por las respuestas rápidas. Este enfoque funciona si vincula los comandos a un Botón estándar en la Ventana (que tiene acceso al Modelo de Vista a través de su DataContext), por ejemplo; CanExecute se muestra con bastante frecuencia cuando se usa CommandManager como se sugiere en ICommand implementando clases o usando RelayCommand y DelegateCommand.

Sin embargo, vincular los mismos comandos a través de CommandReference en ContextMenu no actúa de la misma manera.

Para el mismo comportamiento, también debo incluir el EventHandler del RelayCommand de Josh Smith, dentro de CommandReference, pero al hacerlo debo comentar algún código del Método OnCommandChanged. No estoy del todo seguro de por qué está allí, tal vez está impidiendo las pérdidas de memoria del evento (¡en una suposición!)?

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 }


Para completar la respuesta de Will, aquí hay una implementación "estándar" del evento CanExecuteChanged :

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

(de la clase RelayCommand de Josh Smith)

Por cierto, probablemente deberías considerar usar RelayCommand o DelegateCommand : rápidamente te cansarás de crear nuevas clases de comando para cada comando de tu ViewModels ...


Sin embargo, vincular los mismos comandos a través de CommandReference en ContextMenu no actúa de la misma manera.

Eso es un error en la implementación de CommandReference. Se sigue de estos dos puntos:

  1. Se recomienda que los implementadores de ICommand.CanExecuteChanged contengan solo referencias débiles a los manejadores (vea esta respuesta ).
  2. Los consumidores de ICommand.CanExecuteChanged deben esperar (1) y, por lo tanto, deben contener referencias sólidas a los manejadores que registran con ICommand.CanExecuteChanged

Las implementaciones comunes de RelayCommand y DelegateCommand cumplen (1). La implementación de CommandReference no cumple con (2) cuando se suscribe a newCommand.CanExecuteChanged. Por lo tanto, el objeto controlador se recopila y después de eso, CommandReference ya no recibe ninguna notificación con la que estaba contando.

La solución es mantener una referencia fuerte al controlador en 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); }

Para el mismo comportamiento, también debo incluir el EventHandler del RelayCommand de Josh Smith, dentro de CommandReference, pero al hacerlo debo comentar algún código del Método OnCommandChanged. No estoy del todo seguro de por qué está allí, tal vez está impidiendo las pérdidas de memoria del evento (¡en una suposición!)?

Tenga en cuenta que su enfoque de reenvío de suscripción a CommandManager.RequerySuggested también elimina el error (para empezar, no hay más controladores sin referencias), pero perjudica la funcionalidad de CommandReference. El comando con el que CommandReference está asociado es libre de generar CanExecuteChanged directamente (en lugar de confiar en CommandManager para que emita una solicitud de consulta), pero este evento se tragará y nunca llegará al origen del comando vinculado a CommandReference. Esto también debería responder a su pregunta sobre por qué CommandReference se implementa suscribiéndose a newCommand.CanExecuteChanged.

ACTUALIZACIÓN: presentado un problema en CodePlex


Debe realizar un seguimiento de cuándo ha cambiado el estado de CanExecute y desencadenar el evento ICommand.CanExecuteChanged.

Además, es posible que no siempre funcione, y en estos casos se requiere una llamada a CommandManager.InvalidateRequerySuggested() para patear al administrador de comandos en el asno.

Si considera que esto lleva demasiado tiempo, revise la respuesta a esta pregunta.


Una solución más fácil para mí fue establecer CommandTarget en MenuItem.

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

Más información: http://www.wpftutorial.net/RoutedCommandsInContextMenu.html