2010-04-06 17 views
12

Tengo algunas dificultades con los comandos del menú contextual en mi modelo de visualización.Comandos de WPF ViewModel CanExecute problema

Estoy implementando la interfaz ICommand para cada comando dentro del Modelo de Vista, luego creando 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 el método CanExecute en el comando no se llama excepto en la creación de la ventana, por lo tanto, mis ítems contextuales no se habilitan o deshabilitan como yo esperaba.

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

Este es el modelo de vista

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, por último, 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> 
+1

bueno ... He estado buscando la manera de hacer esto hoy! +1 – kevchadders

Respuesta

21

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

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

(de la clase de Josh Smith RelayCommand)

Por cierto, probablemente debería considerar usar RelayCommand o DelegateCommand: rápidamente se cansará de crear nuevas clases de comando para cada c ommand de usted ViewModels ...

4

Hay que mantener un registro de cuando el estado de CanExecute ha cambiado y desencadena el evento ICommand.CanExecuteChanged.

Además, puede encontrar que no siempre funciona, y en estos casos se requiere una llamada a CommandManager.InvalidateRequerySuggested() para patear al administrador de comandos en el trasero.

Si usted encuentra que esto toma demasiado tiempo, check out the answer to this question.

2

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, vinculando los mismos comandos a través de CommandReference en el ContextMenu no actúe de la misma manera.

Para obtener 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 
    } 
+0

Consulte [mi respuesta] (http://stackoverflow.com/a/10924171/435522) – alexei

1

Sin embargo, la unión de los mismos comandos a través de un CommandReference en el ContextMenu no actúan de la misma manera.

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

  1. Se recomienda que los ejecutores de ICommand.CanExecuteChanged sostienen únicas referencias débiles a los manipuladores (véase this answer).
  2. Los consumidores de ICommand.CanExecuteChanged deben esperar (1) y por lo tanto debe contener fuertes referencias a los controladores que se registren con ICommand.CanExecuteChanged

Las implementaciones comunes de RelayCommand y DelegateCommand rigen por (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 fuerte referencia 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); 
    } 

A fin de que el mismo comportamiento, también debe incluir el manejador de sucesos de RelayCommand de Josh Smith, dentro CommandReference, pero al hacer , así que debo comentar algún código dentro del método OnCommandChanged . No estoy del todo seguro de por qué está allí, tal vez es que previene fugas de memoria de eventos (¡en una suposición!)?

en cuenta que su enfoque de desvío de suscripción a CommandManager.RequerySuggested también elimina el error (no hay manejador de no más sin referencias para empezar), pero que perjudica al funcionamiento 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 an issue on CodePlex

+0

gracias, esto resolvió mi problema – Tobias

0

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

<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

Cuestiones relacionadas