13

我的视图模型上的上下文菜单命令有些困难。

我正在为 View Model 中的每个命令实现 ICommand 接口,然后在 View (MainWindow) 的资源中创建一个 ContextMenu,并使用 MVVMToolkit 中的 CommandReference 来访问当前的 DataContext (ViewModel) 命令。

当我调试应用程序时,似乎除了在创建窗口时没有调用命令上的 CanExecute 方法,因此我的 Context MenuItems 没有像我预期的那样被启用或禁用。

我已经制作了一个简单的示例(附在此处),它表明了我的实际应用并在下面进行了总结。任何帮助将不胜感激!

这是视图模型

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

    }
}

DisplayValueCommand 是这样的:

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
}

最后,视图在 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>
4

5 回答 5

21

为了完成 Will 的回答,这里是事件的“标准”实现CanExecuteChanged

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

(来自 Josh Smith 的RelayCommand课堂)

顺便说一句,您可能应该考虑使用RelayCommandor DelegateCommand:您很快就会厌倦为 ViewModel 的每个命令创建新的命令类...

于 2010-04-06T20:41:25.730 回答
4

您必须跟踪 CanExecute 的状态何时更改并触发 ICommand.CanExecuteChanged 事件。

此外,您可能会发现它并不总是有效,在这些情况下,CommandManager.InvalidateRequerySuggested()需要调用来将命令管理器踢到屁股上。

如果您发现这花费的时间太长,请查看此问题的答案。

于 2010-04-06T20:32:15.740 回答
2

感谢您的快速回复。例如,如果您将命令绑定到窗口中的标准按钮(可以通过其 DataContext 访问视图模型),这种方法确实有效;正如您在 ICommand 实现类上所建议的那样,使用 CommandManager 或使用 RelayCommand 和 DelegateCommand 时,CanExecute 会被非常频繁地调用。

但是,通过 ContextMenu 中的 CommandReference 绑定相同的命令不会以相同的方式起作用。

为了实现相同的行为,我还必须在 CommandReference 中包含来自 Josh Smith 的 RelayCommand 的 EventHandler,但这样做我必须注释掉 OnCommandChanged 方法中的一些代码。我不完全确定它为什么在那里,也许它正在防止事件内存泄漏(猜测!)?

  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
    }
于 2010-04-06T21:38:51.847 回答
1

但是,通过 ContextMenu 中的 CommandReference 绑定相同的命令不会以相同的方式起作用。

这是 CommandReference 实现中的一个错误。从这两点得出:

  1. 建议 ICommand.CanExecuteChanged 的​​实现者只保留对处理程序的弱引用(请参阅此答案)。
  2. ICommand.CanExecuteChanged 的​​消费者应该期望 (1),因此应该持有对他们向 ICommand.CanExecuteChanged 注册的处理程序的强引用

RelayCommand 和 DelegateCommand 的常见实现遵循(1)。CommandReference 实现在订阅 newCommand.CanExecuteChanged 时不遵守 (2)。因此,处理程序对象被收集,之后 CommandReference 不再获得它所依赖的任何通知。

修复方法是在 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);
    }

为了实现相同的行为,我还必须在 CommandReference 中包含来自 Josh Smith 的 RelayCommand 的 EventHandler,但这样做我必须注释掉 OnCommandChanged 方法中的一些代码。我不完全确定它为什么在那里,也许它正在防止事件内存泄漏(猜测!)?

请注意,您将订阅转发到 CommandManager.RequerySuggested 的方法也消除了该错误(开始时没有更多未引用的处理程序),但它妨碍了 CommandReference 功能。与 CommandReference 关联的命令可以自由地直接引发 CanExecuteChanged(而不是依赖 CommandManager 发出重新查询请求),但此事件将被吞没并且永远不会到达绑定到 CommandReference 的命令源。这也应该回答您关于为什么通过订阅 newCommand.CanExecuteChanged 来实现 CommandReference 的问题。

更新:在 CodePlex 上提交了一个问题

于 2012-06-07T00:30:45.730 回答
1

对我来说,一个更简单的解决方案是在 MenuItem 上设置 CommandTarget。

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

更多信息: http: //www.wpftutorial.net/RoutedCommandsInContextMenu.html

于 2017-11-23T15:21:26.050 回答