WPF ViewModel命令CanExecute问题

时间:2010-04-06 20:10:00

标签: wpf binding command contextmenu icommand

我的View Model上的Context Menu命令有些困难。

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

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

我已经制作了一个简单的样本(attached here),它表明了我的实际应用,并总结如下。任何帮助将不胜感激!

这是ViewModel

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>

5 个答案:

答案 0 :(得分:21)

要完成Will的回答,这是CanExecuteChanged事件的“标准”实现:

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

(来自Josh Smith的RelayCommand班)

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

答案 1 :(得分:4)

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

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

如果您发现这需要太长时间,check out the answer to this question.

答案 2 :(得分:2)

感谢您的快速回复。如果您将命令绑定到Window中的标准Button(例如,可以通过其DataContext访问View Model),则此方法可以正常工作;如您在ICommand实现类或使用RelayCommand和DelegateCommand上所建议的那样使用CommandManager时,可以非常频繁地调用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
    }

答案 3 :(得分:1)

  

但是,通过CommandReference绑定相同的命令   ContextMenu不会以同样的方式行事。

这是CommandReference实现中的一个错误。它遵循以下两点:

  1. 建议ICommand.CanExecuteChanged的实施者只保留对处理程序的弱引用(参见this answer)。
  2. ICommand.CanExecuteChanged的消费者应该期望(1)因此应该对他们使用ICommand.CanExecuteChanged
  3. 注册的处理程序进行强引用

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

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

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

    更新:已提交an issue on CodePlex

答案 4 :(得分: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