我的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>
答案 0 :(得分:21)
要完成Will的回答,这是CanExecuteChanged
事件的“标准”实现:
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
(来自Josh Smith的RelayCommand
班)
顺便说一句,您应该考虑使用RelayCommand
或DelegateCommand
:您很快就会厌倦为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实现中的一个错误。它遵循以下两点:
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