2010-06-12 10 views
7

tengo un problema complicado donde estoy atando un ContextMenu a un conjunto de objetos derivada de ICommand, y el establecimiento de las CommandCommandParameter y propiedades de cada MenuItem a través de un estilo:ICommand.CanExecute se pasa nula a pesar de que está establecido CommandParameter

<ContextMenu 
    ItemsSource="{Binding Source={x:Static OrangeNote:Note.MultiCommands}}"> 
    <ContextMenu.Resources> 
     <Style 
      TargetType="MenuItem"> 
      <Setter 
       Property="Header" 
       Value="{Binding Path=Title}" /> 
      <Setter 
       Property="Command" 
       Value="{Binding}" /> 
      <Setter 
       Property="CommandParameter" 
       Value="{Binding Source={x:Static OrangeNote:App.Screen}, Path=SelectedNotes}" /> 
... 

Sin embargo, mientras que pasa el conjunto de notas seleccionadas como debería, ICommand.CanExecute(object) (que se llama cuando se crea el menú) se pasa nulo. Lo he verificado y la colección de notas seleccionada está correctamente instanciada antes de que se realice la llamada (de hecho, se le ha asignado un valor en su declaración, por lo que nunca es null). No puedo entender por qué se está aprobando CanEvaluate null.

+0

que tenían el mismo problema. Mi solución fue enlazar el comando después del parámetro de comando simplemente colocando el setter del parámetro de comando antes del setter del comando y de repente el parámetro enlazado fue pasado a la primera llamada de 'CanExecute'. – Cubinator73

Respuesta

7

He determinado que hay al menos dos errores en ContextMenu que hacen que sus llamadas CanExecute no sean confiables en diferentes circunstancias. Llama a CanExecute inmediatamente cuando se establece el comando. Las llamadas posteriores son impredecibles y ciertamente no confiables.

Pasé toda una noche tratando de rastrear las condiciones precisas bajo las cuales fallaría y buscando una solución alternativa. Finalmente me rendí y cambié a los manejadores de Click que disparaban los comandos deseados.

Decidí que uno de mis problemas era que cambiar el DataContext del ContextMenu puede hacer que se llame a CanExecute antes de que se vincule el nuevo Command o CommandParameter.

La mejor solución que conozco a este problema es utilizar sus propias propiedades adjuntas para el mando y CommandBinding en lugar de utilizar el incorporado en los:

  • Cuando su propiedad Command adjunta se establece, inscríbase en el Los eventos Click y DataContextChanged en el MenuItem, y también se suscriben a CommandManager.RequerySuggested.

  • Cuando el DataContext cambia, RequerySuggested entra, o cambia cualquiera de sus dos propiedades adjuntas, programe una operación de despachador usando Dispatcher.BeginInvoke que llamará a su CanExecute() y actualizará IsEnabled en el elemento de menú.

  • Cuando el evento Click se dispara, haga lo de CanExecute y, si pasa, llame a Execute().

uso es igual Comando regular y CommandParameter, pero utilizando las propiedades adjuntas en su lugar:

<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" /> 
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" /> 

Esta solución funciona y pasa por todos los problemas con los errores en el manejo de CanExecute ContextMenu.

Esperemos que algún día Microsoft solucione los problemas con ContextMenu y esta solución ya no será necesaria. Tengo un caso de reproducción aquí en alguna parte que tengo la intención de enviar a Connect. Tal vez debería subirme al balón y hacerlo realmente.

¿Qué es RequerySuggested, y por qué usarlo?

El mecanismo RequerySuggested es la forma en que RoutedCommand maneja eficientemente ICommand.CanExecuteChanged.En el mundo no enrutado, cada ICommand tiene su propia lista de suscriptores a CanExecuteChanged, pero para RoutedCommand, cualquier cliente que se suscriba a ICommand.CanExecuteChanged se suscribirá a CommandManager.RequerySuggested. Este modelo más simple significa que cada vez CanExecute de un RoutedCommand puede cambiar, todo lo que es necesario es llamar CommandManager.InvalidateRequerySuggested(), que va a hacer las mismas cosas que disparar ICommand.CanExecuteChanged pero hacerlo para todos los RoutedCommands al mismo tiempo y en un subproceso en segundo plano. Además, las invocaciones RequerySuggested se combinan juntas de modo que si se producen muchos cambios, CanExecute solo necesita ser llamado una vez.

Las razones que recomendé se suscribe a CommandManager.RequerySuggested en lugar de ICommand.CanExecuteChanged es: 1. Usted no necesita código para eliminar su suscripción de edad y añadir uno nuevo cada vez que el valor de su propiedad adjunta Comando cambia cambios y 2. CommandManager.RequerySuggested tiene una función de referencia débil incorporada que le permite configurar su controlador de eventos y seguir siendo un elemento no deseado. Hacer lo mismo con ICommand requiere que implemente su propio mecanismo de referencia débil.

La otra cara de esto es que si se suscribe a CommandManager.RequerySuggested en lugar de ICommand.CanExecuteChanged es que solo obtendrá actualizaciones para RoutedCommands. Yo uso RoutedCommands exclusivamente así que esto no es un problema para mí, pero debería haber mencionado que si usa ICommands regulares a veces debería considerar hacer el trabajo extra de suscribirse débilmente a ICommand.CanExecutedChanged. Tenga en cuenta que si hace esto, no necesita suscribirse a RequerySuggested también, ya que RoutedCommand.add_CanExecutedChanged ya lo hace por usted.

+0

Wow, esta es una solución complicada a algo bastante simple de querer hacer. Preguntas para parejas: ¿cómo uso CommandManager.RequerySuggested (es un evento estático, qué es exactamente lo que controlo?) Y cuál es la tercera propiedad adjunta que menciona, además de Command y CommandParameter? – devios1

+1

Oh veo, esta "propiedad adjunta de uso interno solamente" ... ¿no podría simplemente suscribirme a DataContextChanged? – devios1

+0

Lo tengo funcionando! w00t! Gracias :) Todavía tengo curiosidad acerca de RequerySuggested tho ... ¿qué es eso exactamente? – devios1

8

creo que esto está relacionado con el problema de conexión registra aquí:

https://connect.microsoft.com/VisualStudio/feedback/details/504976/command-canexecute-still-not-requeried-after-commandparameter-change?wa=wsignin1.0

Mi solución es la siguiente:

  1. crear una clase estática con una propiedad de dependencia adjunta un parámetro de comando de cota
  2. Crear una interfaz personalizada para generar manualmente CanExecuteChanged en un comando personalizado
  3. Implemente la interfaz en cada comando que necesita saber sobre los cambios de parámetros.

    public interface ICanExecuteChanged : ICommand 
    { 
        void RaiseCanExecuteChanged(); 
    } 
    
    public static class BoundCommand 
    { 
        public static object GetParameter(DependencyObject obj) 
        { 
         return (object)obj.GetValue(ParameterProperty); 
        } 
    
        public static void SetParameter(DependencyObject obj, object value) 
        { 
         obj.SetValue(ParameterProperty, value); 
        } 
    
        public static readonly DependencyProperty ParameterProperty = DependencyProperty.RegisterAttached("Parameter", typeof(object), typeof(BoundCommand), new UIPropertyMetadata(null, ParameterChanged)); 
    
        private static void ParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
        { 
         var button = d as ButtonBase; 
         if (button == null) 
         { 
          return; 
         } 
    
         button.CommandParameter = e.NewValue; 
         var cmd = button.Command as ICanExecuteChanged; 
         if (cmd != null) 
         { 
          cmd.RaiseCanExecuteChanged(); 
         } 
        } 
    } 
    

ejecución de comandos:

public class MyCustomCommand : ICanExecuteChanged 
    { 
     public void Execute(object parameter) 
     { 
      // Execute the command 
     } 

     public bool CanExecute(object parameter) 
     { 
      Debug.WriteLine("Parameter changed to {0}!", parameter); 
      return parameter != null; 
     } 

     public event EventHandler CanExecuteChanged; 

     public void RaiseCanExecuteChanged() 
     { 
      EventHandler temp = this.CanExecuteChanged; 
      if (temp != null) 
      { 
       temp(this, EventArgs.Empty); 
      } 
     } 
    } 

Xaml Uso:

<Button Content="Save" 
     Command="{Binding SaveCommand}" 
     my:BoundCommand.Parameter="{Binding Document}" /> 

Ésta es la solución más simple que podría subir con y funciona un convite para implementaciones de estilo MVVM. También puede llamar CommandManager.InvalidateRequerySuggested() en el cambio de parámetro BoundCommand por lo que trabajó con RoutedCommands también.

+0

Funcionó bien para mí – Artiom

1

me encontré con esta situación en un DataGrid donde tenía el menú contextual para reconocer si se activa o desactiva los comandos específicos en función de la fila seleccionada. Lo que encontré fue que sí, el objeto pasado al comando era nulo y que solo se ejecutó una vez para todas las filas, independientemente de si hubo un cambio o no.

Lo que hice fue llamar RaiseCanExecuteChanged sobre los comandos específicos que pondrían en marcha una activar o desactivar en caso de cambio de la selección de la red.


private void MyGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    VM.DeleteItem.RaiseCanExecuteChanged(); 
} 

La unión instrucción de asignación

VM.DeleteItem 
    = new OperationCommand((o) => MessageBox.Show("Delete Me"), 
          (o) => (myGrid.SelectedItem as Order)?.InProgress == false); 

Resultado

Cuando un InProgress es true comando Eliminar no está habilitado

enter image description here

XAML

<DataGrid AutoGenerateColumns="True" 
     Name="myGrid" 
     ItemsSource="{Binding Orders}" 
     SelectionChanged="MyGrid_OnSelectionChanged"> 
    <DataGrid.ContextMenu> 
     <ContextMenu> 
      <MenuItem Header="Copy" Command="{Binding CopyItem}"/> 
      <MenuItem Header="Delete" Command="{Binding DeleteItem}" /> 
     </ContextMenu> 
    </DataGrid.ContextMenu> 
</DataGrid> 
Cuestiones relacionadas