[英]WPF: Binding a ContextMenu to an MVVM Command
假设我有一个 Window,其属性返回一个 Command(实际上,它是一个 UserControl,在 ViewModel 类中带有一个 Command,但让我们尽可能简单地重现问题)。
以下工作:
<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”的绑定源。 BindingExpression:Path=MyCommand; 数据项=空; 目标元素是 'MenuItem' (Name=''); 目标属性是“命令”(类型“ICommand”)
为什么? 我该如何解决这个问题? 使用DataContext
不是一个选项,因为这个问题发生在可视化树的下方,其中 DataContext 已经包含正在显示的实际数据。 我已经尝试使用{RelativeSource FindAncestor, ...}
代替,但这会产生类似的错误消息。
问题是 ContextMenu 它不在可视化树中,因此您基本上必须告诉 Context Menu 使用哪个数据上下文。
查看这篇博文,其中包含 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>
web.archive.org万岁! 这是丢失的博客文章:
绑定到 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 属性访问它。 然后,您可以通过菜单项向下传递此上下文。
我承认这不是世界上最优雅的解决方案。 但是,它胜过在后面的代码中设置内容。 如果有人有更好的方法来做到这一点,我很乐意听到。
我发现它对我不起作用,因为菜单项是嵌套的,这意味着我必须遍历一个额外的“父级”才能找到 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>
这意味着如果你最终得到一个带有子菜单等的复杂上下文菜单,你不需要继续向每个级别的命令添加“父级”。
- 编辑 -
还想出了这个替代方法,在每个绑定到窗口/用户控件的 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>
根据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>
如果(像我一样)你讨厌丑陋的复杂绑定表达式,这里有一个简单的代码隐藏解决方案来解决这个问题。 这种方法仍然允许您在 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;
}
}
}
2020年的答案:
我将这个答案留在这里给任何在谷歌上搜索过这个问题的人,因为这是第一个出现的搜索结果。 这对我有用并且比其他建议的解决方案更简单:
<MenuItem Command="{Binding YourCommand}" CommandTarget="{Binding Path=PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
如此处所述:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.