WPF:将 ContextMenu 绑定到 MVVM 命令
Posted
技术标签:
【中文标题】WPF:将 ContextMenu 绑定到 MVVM 命令【英文标题】:WPF: Binding a ContextMenu to an MVVM Command 【发布时间】:2011-04-04 18:04:54 【问题描述】:假设我有一个带有返回 Command 的属性的 Window(实际上,它是 ViewModel 类中带有 Command 的 UserControl,但让我们尽可能简单地重现问题)。
以下作品:
<Window x:Class="Window1" ... x:Name="myWindow">
<Menu>
<MenuItem Command="Binding MyCommand, ElementName=myWindow" Header="Test" />
</Menu>
</Window>
但以下不起作用。
<Window x:Class="Window1" ... x:Name="myWindow">
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Command="Binding MyCommand, ElementName=myWindow" Header="Test" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
我得到的错误信息是
System.Windows.Data 错误:4:找不到与引用“ElementName=myWindow”进行绑定的源。绑定表达式:路径=我的命令;数据项=空;目标元素是'MenuItem'(名称='');目标属性是“命令”(输入“ICommand”)
为什么?我该如何解决这个问题?使用DataContext
不是一个选项,因为这个问题发生在可视化树的下方,其中 DataContext 已经包含正在显示的实际数据。我已经尝试改用RelativeSource FindAncestor, ...
,但这会产生类似的错误消息。
【问题讨论】:
+1 用于编辑您的解决方案,您应该将其作为单独的答案 【参考方案1】:问题在于 ContextMenu 它不在可视化树中,因此您基本上必须告诉 Context 菜单要使用哪个数据上下文。
查看this blogpost,Thomas Levesque 提供了一个非常好的解决方案。
他创建了一个继承 Freezable 的类 Proxy 并声明了一个 Data 依赖属性。
public class BindingProxy : Freezable
protected override Freezable CreateInstanceCore()
return new BindingProxy();
public object Data
get return (object)GetValue(DataProperty);
set SetValue(DataProperty, value);
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
然后可以在 XAML 中声明(在可视化树中已知正确 DataContext 的位置):
<Grid.Resources>
<local:BindingProxy x:Key="Proxy" Data="Binding" />
</Grid.Resources>
并在可视化树外的上下文菜单中使用:
<ContextMenu>
<MenuItem Header="Test" Command="Binding Source=StaticResource Proxy, Path=Data.MyCommand"/>
</ContextMenu>
【讨论】:
在我尝试了大约 10 种不同的方法(来自 SO 和其他地方)之后,这终于起作用了。非常感谢这个干净且非常简单但非常棒的答案! :) 这是最佳解决方案 这是一个非常好的解决方案。我将绑定代理设置为强类型(数据属性和依赖属性不是 typeof(object) 而是 typeof(MyViewModel)。这样我必须通过代理绑定的地方有更好的智能感知。【参考方案2】:为web.archive.org 万岁!这里是the missing blog post:
绑定到 WPF 上下文菜单中的 MenuItem
2008 年 10 月 29 日,星期三 — jtango18
因为 WPF 中的 ContextMenu 不存在于 您的页面/窗口/控件本身,数据绑定可能有点棘手。 我在网上到处搜索过这个,而且最 常见的答案似乎是“只需在后面的代码中执行”。错误的!一世 没有回到 XAML 的美妙世界 在后面的代码中做事。
这是我的示例,它将允许您绑定到一个字符串 作为窗口的属性存在。
public partial class Window1 : Window
public Window1()
MyString = "Here is my string";
public string MyString
get;
set;
<Button Content="Test Button" Tag="Binding RelativeSource=RelativeSource AncestorType=x:Type Window">
<Button.ContextMenu>
<ContextMenu DataContext="Binding Path=PlacementTarget.Tag, RelativeSource=RelativeSource Self" >
<MenuItem Header="Binding MyString"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
重要的部分是按钮上的标签(虽然你可以像 轻松设置按钮的 DataContext)。这存储了对 父窗口。 ContextMenu 能够访问这个 通过它的 PlacementTarget 属性。然后,您可以传递此上下文 向下浏览您的菜单项。
我承认这不是世界上最优雅的解决方案。 但是,它胜过在后面的代码中设置东西。如果有人有 更好的方法,我很想听听。
【讨论】:
奇怪的是,我设置了DataContext
的MenuItem
,但它不起作用。正如您所描述的,一旦我将其更改为设置在ContextMenu
上,它就开始工作了。感谢您发布此内容。【参考方案3】:
我发现它对我不起作用,因为菜单项是嵌套的,这意味着我必须遍历一个额外的“父级”才能找到 PlacementTarget。
更好的方法是找到 ContextMenu 本身作为 RelativeSource,然后绑定到它的放置目标。此外,由于标签是窗口本身,并且您的命令位于视图模型中,因此您还需要设置 DataContext。
我最终得到了这样的结果
<Window x:Class="Window1" ... x:Name="myWindow">
...
<Grid Tag="Binding ElementName=myWindow">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Command="Binding PlacementTarget.Tag.DataContext.MyCommand,
RelativeSource=RelativeSource Mode=FindAncestor,
AncestorType=ContextMenu"
Header="Test" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
这意味着,如果您最终得到一个带有子菜单等的复杂上下文菜单。您不需要继续为每个级别的命令添加“父级”。
-- 编辑--
还提出了这种替代方法,在每个绑定到 Window/Usercontrol 的 ListBoxItem 上设置一个标签。我最终这样做了,因为每个 ListBoxItem 都由它们自己的 ViewModel 表示,但我需要菜单命令通过控件的*** ViewModel 执行,但将它们的列表 ViewModel 作为参数传递。
<ContextMenu x:Key="BookItemContextMenu"
Style="StaticResource ContextMenuStyle1">
<MenuItem Command="Binding Parent.PlacementTarget.Tag.DataContext.DoSomethingWithBookCommand,
RelativeSource=RelativeSource Mode=FindAncestor,
AncestorType=ContextMenu"
CommandParameter="Binding"
Header="Do Something With Book" />
</MenuItem>>
</ContextMenu>
...
<ListView.ItemContainerStyle>
<Style TargetType="x:Type ListBoxItem">
<Setter Property="ContextMenu" Value="StaticResource BookItemContextMenu" />
<Setter Property="Tag" Value="Binding ElementName=thisUserControl" />
</Style>
</ListView.ItemContainerStyle>
【讨论】:
【参考方案4】:基于HCLs answer,这是我最终使用的:
<Window x:Class="Window1" ... x:Name="myWindow">
...
<Grid Tag="Binding ElementName=myWindow">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Command="Binding Parent.PlacementTarget.Tag.MyCommand,
RelativeSource=RelativeSource Self"
Header="Test" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Window>
【讨论】:
这真的有效吗?我一直试图让这个工作,并且使用 snoop 似乎命令被评估一次并且从未真正更新过。 PlacementTarget 在上下文菜单被实际激活之前为空,此时 Parent.PlacementTarget.Tag 是有效的,但命令永远不会动态更新(从我在 Snoop 中可以看到) 这实际上是唯一对我有用的东西,我已经尝试了来自整个网站的 10-15 条建议。【参考方案5】:如果(像我一样)你讨厌丑陋的复杂绑定表达式,这里有一个简单的代码隐藏解决方案来解决这个问题。这种方法仍然允许您在 XAML 中保留干净的命令声明。
XAML:
<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
<MenuItem Command="Save"/>
<Separator></Separator>
<MenuItem Command="Close"/>
...
后面的代码:
private void ContextMenu_ContextMenuOpening(object sender, ContextMenuEventArgs e)
foreach (var item in (sender as ContextMenu).Items)
if(item is MenuItem)
//set the command target to whatever you like here
(item as MenuItem).CommandTarget = this;
【讨论】:
【参考方案6】:2020 年的答案:
我将把这个答案留给在谷歌上搜索过这个问题的其他人,因为这是显示的第一个搜索结果。 这对我有用,并且比其他建议的解决方案更简单:
<MenuItem Command="Binding YourCommand" CommandTarget="Binding Path=PlacementTarget, RelativeSource=RelativeSource AncestorType=x:Type ContextMenu"/>
如此处所述:
https://wpf.2000things.com/2014/06/19/1097-getting-items-in-context-menu-to-correctly-use-command-binding/
【讨论】:
以上是关于WPF:将 ContextMenu 绑定到 MVVM 命令的主要内容,如果未能解决你的问题,请参考以下文章
WPF ContextMenu:MenuItem 图标可见性绑定错误
[WPF]解决模板中ContextMenu绑定CommandParameter的问题
WPF ContextMenu 在MVVM模式中无法绑定 Command的解决办法