ElementName Binding from MenuItem in ContextMenu

Deadly 提交于 2019-11-26 03:26:47

问题


Has anybody else noticed that Bindings with ElementName do not resolve correctly for MenuItem objects that are contained within ContextMenu objects? Check out this sample:

<Window x:Class=\"EmptyWPF.Window1\"
    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"
    Title=\"Window1\" Height=\"300\" Width=\"300\"
    x:Name=\"window\">
    <Grid x:Name=\"grid\" Background=\"Wheat\">
        <Grid.ContextMenu>
            <ContextMenu x:Name=\"menu\">
                <MenuItem x:Name=\"menuItem\" Header=\"Window\" Tag=\"{Binding ElementName=window}\" Click=\"MenuItem_Click\"/>
                <MenuItem Header=\"Grid\" Tag=\"{Binding ElementName=grid}\" Click=\"MenuItem_Click\"/>
                <MenuItem Header=\"Menu\" Tag=\"{Binding ElementName=menu}\" Click=\"MenuItem_Click\"/>
                <MenuItem Header=\"Menu Item\" Tag=\"{Binding ElementName=menuItem}\" Click=\"MenuItem_Click\"/>
            </ContextMenu>
        </Grid.ContextMenu>
        <Button Content=\"Menu\" 
                HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\" 
                Click=\"MenuItem_Click\" Tag=\"{Binding ElementName=menu}\"/>
        <Menu HorizontalAlignment=\"Center\" VerticalAlignment=\"Bottom\">
            <MenuItem x:Name=\"anotherMenuItem\" Header=\"Window\" Tag=\"{Binding ElementName=window}\" Click=\"MenuItem_Click\"/>
            <MenuItem Header=\"Grid\" Tag=\"{Binding ElementName=grid}\" Click=\"MenuItem_Click\"/>
            <MenuItem Header=\"Menu\" Tag=\"{Binding ElementName=menu}\" Click=\"MenuItem_Click\"/>
            <MenuItem Header=\"Menu Item\" Tag=\"{Binding ElementName=anotherMenuItem}\" Click=\"MenuItem_Click\"/>
        </Menu>
    </Grid>
</Window>

All of the bindings work great except for the bindings contained within the ContextMenu. They print an error to the Output window during runtime.

Any one know of any work arounds? What\'s going on here?


回答1:


I found a much simpler solution.

In the code behind for the UserControl:

NameScope.SetNameScope(contextMenu, NameScope.GetNameScope(this));



回答2:


As said by others, the 'ContextMenu' is not contained in the visual tree and an 'ElementName' binding won't work. Setting the context menu's 'NameScope' as suggested by the accepted answer only works if the context menu is not defined in a 'DataTemplate'. I have solved this by using the {x:Reference} Markup-Extension which is similar to the 'ElementName' binding but resolves the binding differently, bypassing the visual tree. I consider this to be far more readable than using 'PlacementTarget'. Here is an example:

<Image Source="{Binding Image}">       
    <Image.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Delete" 
                      Command="{Binding Source={x:Reference Name=Root}, Path=DataContext.RemoveImage}"
                      CommandParameter="{Binding}" />
        </ContextMenu>
    </Image.ContextMenu>
</Image>

According to the MSDN-documentation

x:Reference is a construct defined in XAML 2009. In WPF, you can use XAML 2009 features, but only for XAML that is not WPF markup-compiled. Markup-compiled XAML and the BAML form of XAML do not currently support the XAML 2009 language keywords and features.

whatever that means... Works for me, though.




回答3:


Here's another xaml-only workaround. (This also assumes you want what's inside the DataContext, e.g., you're MVVMing it)

Option one, where the parent element of the ContextMenu is not in a DataTemplate:

Command="{Binding PlacementTarget.DataContext.MyCommand, 
         RelativeSource={RelativeSource AncestorType=ContextMenu}}"

This would work for OP's question. This won't work if you are inside of a DataTemplate. In these cases, the DataContext is often one of many in a collection, and the ICommand you wish to bind to is a sibling property of the collection within the same ViewModel (the DataContext of the Window, say).

In these cases, you can take advantage of the Tag to temporarily hold the parent DataContext which contains both the collection AND your ICommand:

class ViewModel
{
    public ObservableCollection<Derp> Derps { get;set;}
    public ICommand DeleteDerp {get; set;}
} 

and in the xaml

<!-- ItemsSource binds to Derps in the DataContext -->
<StackPanel
    Tag="{Binding DataContext, ElementName=root}">
    <StackPanel.ContextMenu>
        <ContextMenu>
            <MenuItem
                Header="Derp"                       
                Command="{Binding PlacementTarget.Tag.DeleteDerp, 
                RelativeSource={RelativeSource 
                                    AncestorType=ContextMenu}}"
                CommandParameter="{Binding PlacementTarget.DataContext, 
                RelativeSource={RelativeSource AncestorType=ContextMenu}}">
            </MenuItem>



回答4:


Context menus are tricky to bind against. They exist outside the visual tree of your control, hence they can't find your element name.

Try setting the datacontext of your context menu to its placement target. You have to use RelativeSource.

<ContextMenu 
   DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}"> ...



回答5:


After experimenting a bit, I discovered one work around:

Make top level Window/UserControl implement INameScope and set NameScope of ContextMenu to the top level control.

public class Window1 : Window, INameScope
{
    public Window1()
    {
        InitializeComponent();
        NameScope.SetNameScope(contextMenu, this);
    }

    // Event handlers and etc...

    // Implement INameScope similar to this:
    #region INameScope Members

    Dictionary<string, object> items = new Dictionary<string, object>();

    object INameScope.FindName(string name)
    {
        return items[name];
    }

    void INameScope.RegisterName(string name, object scopedElement)
    {
        items.Add(name, scopedElement);
    }

    void INameScope.UnregisterName(string name)
    {
        items.Remove(name);
    }

    #endregion
}

This allows the context menu to find named items inside of the Window. Any other options?




回答6:


I'm not sure why resort to magic tricks just to avoid a one line of code inside the eventhandler for the mouse click you already handle:

    private void MenuItem_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        // this would be your tag - whatever control can be put as string intot he tag
        UIElement elm = Window.GetWindow(sender as MenuItem).FindName("whatever control") as UIElement;
    }


来源:https://stackoverflow.com/questions/1013558/elementname-binding-from-menuitem-in-contextmenu

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!