c# wpf data-binding contextmenu icommand

c# - ICommand.CanExecute se pasa null aunque se establece CommandParameter



wpf data-binding (3)

Creo que esto está relacionado con el problema de conexión registrado 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 enlazado
  2. Cree una interfaz personalizada para subir 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(); } } }

Implementación del comando:

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); } } }

Uso de Xaml:

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

Esta es la solución más simple que pude encontrar y funciona bien para las implementaciones de estilo MVVM. También podría llamar a CommandManager.InvalidateRequerySuggested () en el parámetro BoundCommand para que también funcione con RoutedCommands.

Tengo un problema complicado en el que estoy vinculando un ContextMenu a un conjunto de objetos derivados de ICommand y estableciendo las propiedades Command y CommandParameter en cada elemento de menú a través de un estilo:

<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 ICommand.Execute( object ) pasa el conjunto de notas seleccionadas como debería, ICommand.CanExecute( object ) (que se invoca cuando se crea el menú) pasa a ser nulo. Revisé y la colección de notas seleccionada se creó correctamente antes de realizar la llamada (de hecho, se le asignó un valor en su declaración, por lo que nunca es null ). No puedo entender por qué CanEvaluate se está pasando null .


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 para este problema es utilizar sus propias propiedades adjuntas para Command y CommandBinding en lugar de usar las integradas:

  • Cuando se establece la propiedad Command asociada, suscríbase a los eventos Click y DataContextChanged en MenuItem, y también suscríbase a CommandManager.RequerySuggested.

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

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

El uso es como el Comando y el Parámetro de comando normales, pero en su lugar usa las propiedades adjuntas:

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

Esta solución funciona y evita todos los problemas con los errores en el manejo CanExecute de 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 manera 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 que un CanExecute de RoutedCommand puede cambiar, todo lo que se necesita es llamar a CommandManager.InvalidateRequerySuggested (), que hará lo mismo que activar ICommand.CanExecuteChanged pero lo hará para todos los RoutedCommands simultáneamente y en una cadena de fondo. 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 por las que le recomendé suscribirse a CommandManager.RequerySuggested en lugar de ICommand.CanExecuteChanged es: 1. No necesita código para eliminar su suscripción anterior y agregar una nueva cada vez que el valor de su propiedad adjunta al Comando cambia los cambios, y 2. CommandManager.RequerySuggested tiene una característica 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 te suscribes a CommandManager.RequerySuggested en lugar de ICommand.CanExecuteChanged es que solo obtendrás 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.


Me encontré con esta situación en un DataGrid donde necesitaba el menú contextual para reconocer si habilitar o deshabilitar comandos específicos dependiendo 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 a RaiseCanExecuteChanged en comandos específicos que RaiseCanExecuteChanged o deshabilitarían en el evento de selección de cuadrícula cambiado.

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

La asignación de enlace de comando

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

Resultado

Donde un InProgress es true comando de eliminación no está habilitado

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>