WPF Context menu doesn't bind to right databound item
Asked Answered
S

6

17

I have a problem when binding a command in a context menu on a usercontrol that is on a tab page. The first time I use the menu (right-click on the tab) it works great, but if I switch tab the command will use the databound instance that was used the first time.

If I put a button that is bound to the command in the usercontrol it works as expected...

Can someone please tell me what I'm doing wrong??

This is a test project that exposes the problem:

App.xaml.cs:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        CompanyViewModel model = new CompanyViewModel();
        Window1 window = new Window1();
        window.DataContext = model;
        window.Show();
    }
}

Window1.xaml:

<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vw="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">

  <Window.Resources>
    <DataTemplate x:Key="HeaderTemplate">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Path=Name}" />
        </StackPanel>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vw:PersonViewModel}">
        <vw:UserControl1/>
    </DataTemplate>

</Window.Resources>
<Grid>
    <TabControl ItemsSource="{Binding Path=Persons}" 
                ItemTemplate="{StaticResource HeaderTemplate}"
                IsSynchronizedWithCurrentItem="True" />
</Grid>
</Window>

UserControl1.xaml:

<UserControl x:Class="WpfApplication1.UserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    MinWidth="200">
    <UserControl.ContextMenu>
        <ContextMenu >
            <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/>
        </ContextMenu>
    </UserControl.ContextMenu>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0">The name:</Label>
        <TextBox Grid.Column="1" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" />
    </Grid>
</UserControl>

CompanyViewModel.cs:

public class CompanyViewModel
{
    public ObservableCollection<PersonViewModel> Persons { get; set; }
    public CompanyViewModel()
    {
        Persons = new ObservableCollection<PersonViewModel>();
        Persons.Add(new PersonViewModel(new Person { Name = "Kalle" }));
        Persons.Add(new PersonViewModel(new Person { Name = "Nisse" }));
        Persons.Add(new PersonViewModel(new Person { Name = "Jocke" }));
    }
}

PersonViewModel.cs:

public class PersonViewModel : INotifyPropertyChanged
{
    Person _person;
    TestCommand _testCommand;

    public PersonViewModel(Person person)
    {
        _person = person;
        _testCommand = new TestCommand(this);
    }
    public ICommand ChangeCommand 
    {
        get
        {
            return _testCommand;
        }
    }
    public string Name 
    {
        get
        {
            return _person.Name;
        }
        set
        {
            if (value == _person.Name)
                return;
            _person.Name = value;
            OnPropertyChanged("Name");
        }
    }
    void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

TestCommand.cs:

public class TestCommand : ICommand
{
    PersonViewModel _person;
    public event EventHandler CanExecuteChanged;

    public TestCommand(PersonViewModel person)
    {
        _person = person;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }
    public void Execute(object parameter)
    {
        _person.Name = "Changed by command";
    }
}

Person.cs:

public class Person
{
    public string Name { get; set; }
}
Snowbound answered 19/3, 2009 at 13:20 Comment(0)
S
23

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}}}"
/>
Schwitzer answered 19/3, 2009 at 13:42 Comment(8)
Hi Cameron. Do you think your technique here is somehow related to the problem I've described here: #834107 ...I'm not binding to a command, but I have a suspicion it's a related problem.Hornbook
I am not convinced by this answer. The command bindings DO work for the menu item (it knows it has to bind the view model)... the problem is the menuitems do not rebind when the datacontext changes due to switching tab. If its due to them not being part of visual tree, how come it works first time?Peoria
@Schneider: I didn't say that bindings in a menu don't work, just that they don't inherit their datacontext from their parent like you'd expect. I'd say the WPF binding engine is setting the context when the menu is first opened and then not updating it when the tab changes.Schwitzer
Which is bloody annoying, not intuitive and not documented anywhere! :) As you say it must be a "special case" binding when the context menu is created and from then on nothing...Peoria
@Schnieder: Welcome to WPF! :DSchwitzer
I would still love to find a definitive explanation of this from the WPF team etc. Can I suggest you insert a paragraph in your answer explaining that BECAUSE its not in the visual tree, the data context and hence bindings do not update when the content in the content presenter changes due to selecting a tab (assuming that is what causes the problem)Peoria
WPF 4.0: <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"> (way shorter).Honea
@JoanComasFdz: That makes for a much neater solution. Means you don't have to adjust every MenuItem Command binding. Thanks!Whipping
T
8

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.

Tarttan answered 28/1, 2010 at 0:32 Comment(4)
Have you tried this with a RelayCommand with a CanExecute delegate, CyberMonk? I've found that CommandReference gets passed null to the parameter to CanExecute, though the Execute method gets passed the correct value. It's stopping me from using it right now.Heidelberg
OK, this may work but can anyone explain why its needed? Why do bindings on ContextMenus only run once?Peoria
Thanks. The CommandParameter binding might need to be something like {Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}; since MenuItem has a null DataContext.Cerebrate
Also, watch out for the issue with handlers being prematurely collected in the CommandReference implementation (here and here).Cerebrate
A
5

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>
Adjure answered 3/12, 2011 at 1:58 Comment(0)
S
2

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
}
Seabee answered 27/11, 2011 at 6:44 Comment(0)
S
0

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.

Sole answered 26/10, 2011 at 6:54 Comment(1)
Link is dead :(Magnetron
D
0

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>
Daube answered 14/11, 2017 at 9:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.