WPF ViewModel 命令可以执行问题

发布于 2024-08-27 12:17:38 字数 2897 浏览 10 评论 0原文

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

我正在为视图模型中的每个命令实现 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);
        }

    }
}

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>

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 creating a ContextMenu within the resources of the View (MainWindow), and using a CommandReference from the MVVMToolkit to access the current DataContext (ViewModel) Commands.

When I debug the application, it appears that the CanExecute method on the command is not being called except at the creation of the window, therefore my Context MenuItems are not being enabled or disabled as I would have expected.

I've cooked up a simple sample (attached here) which is indicative of my actual application and summarised below. Any help would be greatly appreciated!

This is the 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);
        }

    }
}

The DisplayValueCommand is such:

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
}

And finally, the view is defined in 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>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

静待花开 2024-09-03 12:17:38

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

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

来自 Josh Smith 的 RelayCommand 类)

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

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...

遗失的美好 2024-09-03 12:17:38

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

此外,您可能会发现它并不总是有效,在这些情况下,需要调用 CommandManager.InvalidateRequerySuggested() 才能彻底解决命令管理器问题。

如果您发现这需要太长时间,查看以下问题的答案:这个问题。

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.

ゃ懵逼小萝莉 2024-09-03 12:17:38

感谢您的快速回复。例如,如果您将命令绑定到窗口中的标准按钮(可以通过其 DataContext 访问视图模型),则此方法确实有效;当使用 CommandManager(如您在 ICommand 实现类上建议的那样)或使用 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
    }

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
    }
别把无礼当个性 2024-09-03 12:17:38

但是,通过 CommandReference 绑定相同的命令
ContextMenu 的行为方式不同。

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

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

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

更新:提交CodePlex 上的问题

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

贱人配狗天长地久 2024-09-03 12:17:38

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

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

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

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文