WPF ViewModel Commands CanExecute issue

后端 未结 5 756
情书的邮戳
情书的邮戳 2020-12-09 11:38

I\'m having some difficulty with Context Menu commands on my View Model.

I\'m implementing the ICommand interface for each command within the View Model, then creati

相关标签:
5条回答
  • 2020-12-09 12:07

    An easier solution for me, was to set the CommandTarget on the MenuItem.

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

    More info: http://www.wpftutorial.net/RoutedCommandsInContextMenu.html

    0 讨论(0)
  • 2020-12-09 12:21

    You have to keep track of when the status of CanExecute has changed and fire the ICommand.CanExecuteChanged event.

    Also, you might find that it doesn't always work, and in these cases a call to CommandManager.InvalidateRequerySuggested() is required to kick the command manager in the ass.

    If you find that this takes too long, check out the answer to this question.

    0 讨论(0)
  • 2020-12-09 12:24

    Thank you for the speedy replies. This approach does work if you are binding the commands to a standard Button in the Window (which has access to the View Model via its DataContext), for example; CanExecute is shown to be called quite frequently when using the CommandManager as you suggest on ICommand implementing classes or by using RelayCommand and DelegateCommand.

    However, binding the same commands via a CommandReference in the ContextMenu do not act in the same way.

    In order for the same behaviour, I must also include the EventHandler from Josh Smith's RelayCommand, within CommandReference, but in doing so I must comment out some code from within the OnCommandChanged Method. I'm not entirely sure why it is there, perhaps it is preventing event memory leaks (at a guess!)?

      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 讨论(0)
  • 2020-12-09 12:28

    To complete Will's answer, here's a "standard" implementation of the CanExecuteChanged event :

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

    (from Josh Smith's RelayCommand class)

    By the way, you should probably consider using RelayCommand or DelegateCommand : you'll quickly get tired of creating new command classes for each and every command of you ViewModels...

    0 讨论(0)
  • 2020-12-09 12:33

    However, binding the same commands via a CommandReference in the ContextMenu do not act in the same way.

    That's a bug in CommandReference implementation. It follows from these two points:

    1. It is recommended that the implementers of ICommand.CanExecuteChanged hold only weak references to the handlers (see this answer).
    2. Consumers of ICommand.CanExecuteChanged should expect (1) and hence should hold strong references to the handlers they register with ICommand.CanExecuteChanged

    The common implementations of RelayCommand and DelegateCommand abide by (1). The CommandReference implementation doesn't abide by (2) when it subscribes to newCommand.CanExecuteChanged. So the handler object is collected and after that CommandReference no longer gets any notifications that it was counting on.

    The fix is to hold a strong ref to the handler in 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);
        }
    

    In order for the same behaviour, I must also include the EventHandler from Josh Smith's RelayCommand, within CommandReference, but in doing so I must comment out some code from within the OnCommandChanged Method. I'm not entirely sure why it is there, perhaps it is preventing event memory leaks (at a guess!)?

    Note that your approach of forwarding subscription to CommandManager.RequerySuggested also eliminates the bug (there's no more unreferenced handler to begin with), but it handicaps the CommandReference functionality. The command with which CommandReference is associated is free to raise CanExecuteChanged directly (instead of relying on CommandManager to issue a requery request), but this event would be swallowed and never reach the command source bound to the CommandReference. This should also answer your question as to why CommandReference is implemented by subscribing to newCommand.CanExecuteChanged.

    UPDATE: submitted an issue on CodePlex

    0 讨论(0)
提交回复
热议问题