WPF ViewModel命令无法执行问题

WPF ViewModel命令无法执行问题,wpf,binding,command,contextmenu,icommand,Wpf,Binding,Command,Contextmenu,Icommand,我在视图模型上使用关联菜单命令时遇到一些困难 我正在为视图模型中的每个命令实现ICommand接口,然后在视图(MainWindow)的资源中创建ContextMenu,并使用MVVMToolkit中的CommandReference访问当前的DataContext(ViewModel)命令 当我调试应用程序时,除了在创建窗口时,命令上的CanExecute方法似乎没有被调用,因此我的上下文菜单项没有像我预期的那样被启用或禁用 我制作了一个简单的示例(),它表明了我的实际应用,并总结如下。任何帮

我在视图模型上使用关联菜单命令时遇到一些困难

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

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

我制作了一个简单的示例(),它表明了我的实际应用,并总结如下。任何帮助都将不胜感激

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

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

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

此外,您可能会发现它并不总是有效的,在这种情况下,需要调用
CommandManager.invalidateRequestSuggested()
,才能将命令管理器踢出屁股


如果您发现这需要很长时间才能完成Will的回答,下面是
CanExecuteChanged
事件的“标准”实现:

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}
(摘自Josh Smith的<代码>RelayCommand课程)


顺便说一下,你应该考虑使用<代码> RelayCube 或 DealTraceMoMand <代码>:你会很快厌倦为每个命令创建新的命令类,ViewModels…

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

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

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

但是,通过中的CommandReference绑定相同的命令 ContextMenu的操作方式不同

这是CommandReference实现中的一个bug。从这两点出发:

  • 建议ICommand.CanExecuteChanged的实现者只保留对处理程序的弱引用(请参阅)
  • 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);
        }
    
    为了实现相同的行为,我还必须包含EventHandler 来自Josh Smith的RelayCommand,在CommandReference中,但在doing中 因此,我必须注释掉OnCommandChanged中的一些代码 方法。我不完全确定它为什么会在那里,也许是 防止事件内存泄漏(猜测!)

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


    更新:提交

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

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

    更多信息:

    好东西。。。我一直在想今天该怎么做+1.
        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);
        }
    
    <MenuItem Header="Cut" Command="Cut" CommandTarget="
          {Binding Path=PlacementTarget, 
          RelativeSource={RelativeSource FindAncestor, 
          AncestorType={x:Type ContextMenu}}}"/>