2010-08-27 240 views
17

假设我有一个窗口,其中包含一个返回Command的属性(事实上,它是一个带有ViewModel类中的Command的UserControl,但让我们尽可能简单地重现问题)。WPF:将ContextMenu绑定到MVVM命令

以下工作:

<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 Error: 4 : Cannot find source for binding with reference 'ElementName=myWindow'. BindingExpression:Path=MyCommand; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')

为什么?我该如何解决这个问题?使用DataContext不是一个选项,因为此问题发生在DataContext已包含正在显示的实际数据的可视化树下。我已经尝试使用{RelativeSource FindAncestor, ...}来代替,但这会产生类似的错误消息。

+0

+1与您的解决方案编辑,你应该让一个单独的答案 – jan 2012-05-10 15:42:55

+0

@jan:好主意,做了。 – Heinzi 2012-05-10 16:28:17

回答

16

的问题是,该文本菜单它不是在可视化树,所以你基本上要讲述使用哪个数据上下文的上下文菜单。

检查出this blogpost与一个非常好的解决方案托马斯莱维斯克。

他创建了继承Freezable并声明数据依赖项属性的类Proxy。

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> 
+0

这个__finally__在我尝试过10种不同的方法(从SO和其他地方)后工作。非常感谢这个干净,非常简单,但非常真棒的答案! :) – Yoda 2016-10-19 15:55:31

+0

这是**最好的解决方案** – n00b101 2016-12-27 07:04:22

+0

这是一个非常好的解决方案。我使我的绑定代理强类型化(数据属性和依赖属性不是typeof(对象),但typeof(MyViewModel)。这种方式有更好的智能感知,我必须通过代理绑定。 – Michael 2017-11-07 21:41:41

6

查看this来自Justin Taylor的文章提供了一种解决方法。

更新
不幸的是,引用的博客没有更多的可用。我试图在另一个SO答案中解释程序。它可以发现here

+0

我发布了缺少的博客文章作为另一个答案。 – mydogisbox 2012-08-08 18:48:56

+0

@mydogisbox +1完美! – HCL 2012-08-08 19:11:51

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> 
+1

这实际上工作吗?我一直试图让这个工作,并使用snoop似乎该命令得到评估一次,从来没有实际更新。 PlacementTarget为null,直到上下文菜单被实际激活,此时Parent.PlacementTarget.Tag是有效的,但命令永远不会动态更新(从我在Snoop中可以看到的) – nrjohnstone 2014-02-19 02:21:56

+0

这实际上是唯一适用于我和我的已经尝试了来自全网站的10-15条建议。 – 2015-04-12 11:08:15

13

华友世纪web.archive.org!这里是the missing blog post

Binding to a MenuItem in a WPF Context Menu

Wednesday, October 29, 2008 — jtango18

Because a ContextMenu in WPF does not exist within the visual tree of your page/window/control per se, data binding can be a little tricky. I have searched high and low across the web for this, and the most common answer seems to be “just do it in the code behind”. WRONG! I didn’t come in to the wonderful world of XAML to be going back to doing things in the code behind.

Here is my example to that will allow you to bind to a string that exists as a property of your window.

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> 

The important part is the Tag on the button(although you could just as easily set the DataContext of the button). This stores a reference to the parent window. The ContextMenu is capable of accessing this through it’s PlacementTarget property. You can then pass this context down through your menu items.

I’ll admit this is not the most elegant solution in the world. However, it beats setting stuff in the code behind. If anyone has an even better way to do this I’d love to hear it.

+0

奇怪的是,我已经设置了'MenuItem'的'DataContext',它不起作用。正如您所描述的那样,只要我将其更改为设置在ContextMenu上,它就开始工作。感谢您发布此信息。 – 2015-11-03 18:30:38

7

我发现它不工作,我由于被嵌套的菜单项,这意味着我不得不遍历了一个额外的“家长”,找到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> 
2

如果(像我一样)对丑陋的复杂绑定表达式有反感,这里有一个简单的代码隐藏解决方案来解决这个问题。这种方法仍然允许您在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; 
     } 
    } 
}