WPF: Binding a ContextMenu to an MVVM Command

43,971

Solution 1

The problem is that the ContextMenu it not in the visual tree, so you basically have to tell the Context menu about which data context to use.

Check out this blogpost with a very nice solution of Thomas Levesque.

He creates a class Proxy that inherits Freezable and declares a Data dependency property.

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));
}

Then it can be declared in the XAML (on a place in the visual tree where the correct DataContext is known):

<Grid.Resources>
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>

And used in the context menu outside the visual tree:

<ContextMenu>
    <MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>

Solution 2

Hurray for web.archive.org! Here is 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.

Solution 3

I found out it wasn't working for me due to the menu item being nested, which mean I had to traverse up an extra "Parent" to find the PlacementTarget.

A better way is to find the ContextMenu itself as the RelativeSource and then just bind to the placement target of that. Also since the tag is the window itself, and your command is in the viewmodel, you need to have the DataContext set as well.

I ended up with something like this

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

What this means is that if you end up with a complicated context menu with submenus etc.. you don't need to keep adding "Parent" to each levels Commands.

-- EDIT --

Also came up with this alternative to set a tag on every ListBoxItem that binds to the Window/Usercontrol. I ended up doing this because each ListBoxItem was represented by their own ViewModel but I needed the menu commands to execute via the top level ViewModel for the control, but pass their the list ViewModel as a parameter.

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

Solution 4

Based on HCLs answer, this is what I ended up using:

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

Solution 5

If (like me) you have an aversion to ugly complex binding expressions, here is a simple code-behind solution to this problem. This approach still allows you to keep clean command declarations in your XAML.

XAML:

<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
    <MenuItem Command="Save"/>
    <Separator></Separator>
    <MenuItem Command="Close"/>
    ...

Code behind:

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;
        } 
    }
}
Share:
43,971
Can Sahin
Author by

Can Sahin

Updated on June 19, 2020

Comments

  • Can Sahin
    Can Sahin almost 4 years

    Let's say I have a Window with a property returning a Command (in fact, it's a UserControl with a Command in a ViewModel class, but let's keep things as simple as possible to reproduce the problem).

    The following works:

    <Window x:Class="Window1" ... x:Name="myWindow">
        <Menu>
            <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
        </Menu>
    </Window>
    

    But the following does not work.

    <Window x:Class="Window1" ... x:Name="myWindow">
        <Grid>
            <Grid.ContextMenu>
                <ContextMenu>
                    <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
                </ContextMenu>            
            </Grid.ContextMenu>
        </Grid>
    </Window>
    

    The error message I get is

    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')

    Why? And how do I fix this? Using the DataContext is not an option, since this problem occurs way down the visual tree where the DataContext already contains the actual data being displayed. I already tried using {RelativeSource FindAncestor, ...} instead, but that yields a similar error message.

  • nrjohnstone
    nrjohnstone about 10 years
    Does this actually work? I've been trying to get this working, and using snoop it seems that the Command gets evaluated once and never actually updated. PlacementTarget is null until the context menu is actually activated, at which point Parent.PlacementTarget.Tag is valid but the Command never gets dynamically updated (from what I can see in Snoop)
  • UяošKoт
    UяošKoт about 9 years
    this is actually the only thing that works for me and i've tried like 10-15 suggestions from all over this site.
  • Nicholas Miller
    Nicholas Miller over 8 years
    Oddly enough, I had set the DataContext of the MenuItem and it doesn't work. As soon as I changed it to be set on the ContextMenu as you've described, it began to work. Thanks for posting this.
  • Yoda
    Yoda over 7 years
    This finally worked after I tried about 10 different approaches (from SO and elsewhere). Thanks a lot for this clean and pretty simple, yet so awesome answer! :)
  • n00b101
    n00b101 over 7 years
    This is the best solution
  • Michael
    Michael over 6 years
    Thats a very nice solution. I make my binding proxies strongly typed (Data property and dependency property are not typeof(object) but typeof(MyViewModel). This way there is better intellisense where I have to bind via the proxy.