WPF Context menu doesn't bind to right databound item

坚强是说给别人听的谎言 提交于 2019-11-28 05:58:36

The key thing to remember here is context menus are not part of the visual tree.

Therefore they don't inherit the same source as the control they belong to for binding. The way to deal with this is to bind to the placement target of the ContextMenu itself.

<MenuItem Header="Change" Command="{Binding 
    Path=PlacementTarget.ChangeCommand, 
    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"
/>

The cleanest way I have found to bind commands to context menu items involves using a class called CommandReference. You can find it in the MVVM toolkit on Codeplex at WPF Futures.

The XAML might look like this:

<UserControl x:Class="View.MyView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:vm="clr-namespace:ViewModel;assembly=MyViewModel"
                xmlns:mvvm="clr-namespace:ViewModelHelper;assembly=ViewModelHelper"
           <UserControl.Resources>
                <mvvm:CommandReference x:Key="MyCustomCommandReference" Command="{Binding MyCustomCommand}" />

                <ContextMenu x:Key="ItemContextMenu">
                    <MenuItem Header="Plate">
                        <MenuItem Header="Inspect Now" Command="{StaticResource MyCustomCommandReference}"
                                CommandParameter="{Binding}">
                        </MenuItem>
                    </MenuItem>
               </ContextMenu>
    </UserControl.Resources>

MyCustomCommand is a RelayCommand on the ViewModel. In this example, the ViewModel was attached to the view's datacontext in the code-behind.

Note: this XAML was copied from a working project and simplified for illustration. There may be typos or other minor errors.

I had the same issue recently with a ContextMenu located in a ListBox. I tried to bind a command the MVVM way without any code-behind. I finally gave up and I asked a friend for his help. He found a slightly twisted but concise solution. He is passing the ListBox in the DataContext of the ContextMenu and then find the command in the view model by accessing the DataContext of the ListBox. This is the simplest solution that I have seen so far. No custom code, no Tag, just pure XAML and MVVM.

I posted a fully working sample on Github. Here is an excerpt of the XAML.

<Window x:Class="WpfListContextMenu.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow" Height="350" Width="268">
  <Grid>
    <DockPanel>
      <ListBox x:Name="listBox" DockPanel.Dock="Top" ItemsSource="{Binding Items}" DisplayMemberPath="Name"
               SelectionMode="Extended">
        <ListBox.ContextMenu>
          <ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
            <MenuItem Header="Show Selected" Command="{Binding Path=DataContext.ShowSelectedCommand}"
                      CommandParameter="{Binding Path=SelectedItems}" />
          </ContextMenu>
        </ListBox.ContextMenu>
      </ListBox>
    </DockPanel>
  </Grid>
</Window>

I prefer another solution. Add context menu loader event.

<ContextMenu Loaded="ContextMenu_Loaded"> 
    <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
</ContextMenu> 

Assign data context within the event.

private void ContextMenu_Loaded(object sender, RoutedEventArgs e)
{
    (sender as ContextMenu).DataContext = this; //assignment can be replaced with desired data context
}

I found this method using the Tag property very useful when binding from a context menu deep inside a control template:

http://blog.jtango.net/binding-to-a-menuitem-in-a-wpf-context-menu

This makes it possible to bind to any datacontext available to the control that the context menu was opened from. The context menu can access the clicked control through "PlacementTarget". If the Tag property of the clicked control is bound to a desired datacontext, binding to "PlacementTarget.Tag" from inside the context menu will slingshot you directly to that datacontext.

I know this is already an old post, but I would like to add another solution for those one who are looking for different ways to do it.

I could not make the same solution to work in my case, since I was trying to do something else: open the context menu with a mouse click (just like a toolbar with a submenu attached to it) and also bind commands to my model. Since I was using an Event Trigger, the PlacementTarget object was null.

This is the solution I found to make it work only using XAML:

<!-- This is an example with a button, but could be other control -->
<Button>
  <...>

  <!-- This opens the context menu and binds the data context to it -->
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <EventTrigger.Actions>
        <BeginStoryboard>
          <Storyboard>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.DataContext">
              <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{Binding}"/>
            </ObjectAnimationUsingKeyFrames>
            <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
              <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
            </BooleanAnimationUsingKeyFrames>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger.Actions>
    </EventTrigger>
  </Button.Triggers>

  <!-- Here it goes the context menu -->
  <Button.ContextMenu>
    <ContextMenu>
      <MenuItem Header="Item 1" Command="{Binding MyCommand1}"/>
      <MenuItem Header="Item 2" Command="{Binding MyCommand2}"/>
    </ContextMenu>
  </Button.ContextMenu>

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