WPF ViewModel 命令可以执行问题
我在视图模型上使用上下文菜单命令时遇到一些困难。
我正在为视图模型中的每个命令实现 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
为了完成 Will 的回答,这里是
CanExecuteChanged
事件的“标准”实现:(来自 Josh Smith 的
RelayCommand
类)顺便说一句,您可能应该考虑使用
RelayCommand
或DelegateCommand
:您很快就会厌倦为 ViewModel 的每个命令创建新的命令类...To complete Will's answer, here's a "standard" implementation of the
CanExecuteChanged
event :(from Josh Smith's
RelayCommand
class)By the way, you should probably consider using
RelayCommand
orDelegateCommand
: you'll quickly get tired of creating new command classes for each and every command of you ViewModels...您必须跟踪 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.
感谢您的快速回复。例如,如果您将命令绑定到窗口中的标准按钮(可以通过其 DataContext 访问视图模型),则此方法确实有效;当使用 CommandManager(如您在 ICommand 实现类上建议的那样)或使用 RelayCommand 和 DelegateCommand 时,CanExecute 会被频繁调用。
但是,通过 ContextMenu 中的 CommandReference 绑定相同的命令
不要以同样的方式行事。
为了实现相同的行为,我还必须在 CommandReference 中包含来自 Josh Smith 的 RelayCommand 的 EventHandler,但为此我必须注释掉 OnCommandChanged 方法中的一些代码。我不完全确定它为什么在那里,也许它是为了防止事件内存泄漏(猜测!)?
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!)?
这是 CommandReference 实现中的一个错误。从这两点可以得出结论:
和 DelegateCommand 的常见实现遵守 (1)。 CommandReference 实现在订阅 newCommand.CanExecuteChanged 时不遵守 (2)。因此,处理程序对象被收集,之后 CommandReference 不再收到它所依赖的任何通知。
解决方法是在 CommandReference 中保留对处理程序的强引用:
请注意,将订阅转发到 CommandManager.RequerySuggested 的方法也消除了该错误(不再有未引用的处理程序),但它妨碍了 CommandReference 功能。与 CommandReference 关联的命令可以直接引发 CanExecuteChanged(而不是依赖 CommandManager 发出重新查询请求),但此事件将被吞没,并且永远不会到达绑定到 CommandReference 的命令源。这也应该回答您关于为什么 CommandReference 是通过订阅 newCommand.CanExecuteChanged 来实现的问题。
更新:提交CodePlex 上的问题
That's a bug in CommandReference implementation. It follows from these two points:
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:
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
对我来说,一个更简单的解决方案是在 MenuItem 上设置 CommandTarget。
更多信息:http://www.wpftutorial.net/RoulatedCommandsInContextMenu.html
An easier solution for me, was to set the CommandTarget on the MenuItem.
More info: http://www.wpftutorial.net/RoutedCommandsInContextMenu.html