WPF上下文菜单未绑定到正确的数据项

17

我在一个选项卡页面上的用户控件中绑定上下文菜单的命令时遇到了问题。

第一次使用菜单(右键单击标签页)时,它能够正常工作,但如果我切换选项卡,则该命令将使用第一次使用的数据绑定实例。

如果我在用户控件中放置一个绑定到该命令的按钮,则它会按预期工作...

请问有人能告诉我我做错了什么吗??

这是一个显示问题的测试项目:

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; }
}
6个回答

23

需要记住的关键点是上下文菜单不是可视树的一部分。

因此它们不会继承与其所属控件相同的源进行绑定。处理这种情况的方法是绑定到上下文菜单本身的放置目标。

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

2
我对这个答案并不满意。命令绑定确实适用于菜单项(它知道必须绑定视图模型)...问题在于当数据上下文由于切换选项卡而更改时,菜单项不会重新绑定。如果这是因为它们不是可视树的一部分,那么第一次为什么能够工作呢? - Jack Ukleja
6
@Schnieder:欢迎来到 WPF! :D - Cameron MacFarland
2
我仍然希望能够从WPF团队中找到一个明确的解释等。我可以建议您在回答中插入一段解释,即因为它不在可视树中,所以当内容呈现器中的内容由于选择选项卡而更改时,数据上下文和绑定不会更新(假设这是问题的原因)。 - Jack Ukleja
5
WPF 4.0: <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"> (更简洁的方式)。 - JoanComasFdz
1
@JoanComasFdz:这样做会使解决方案变得更加整洁。这意味着您不必调整每个MenuItem命令绑定。谢谢! - quarkonium
显示剩余3条评论

8
我发现将命令绑定到上下文菜单项的最简洁方法是使用一个称为CommandReference的类。您可以在Codeplex的MVVM工具包中找到它,网址是WPF Futures
XAML可能如下所示:
<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是ViewModel上的RelayCommand。在此示例中,ViewModel已附加到代码后台中视图的数据上下文。

注意:此XAML从一个工作项目中复制并简化以进行说明。可能存在拼写错误或其他次要错误。


1
你尝试过使用带有CanExecute委托的RelayCommand吗,CyberMonk?我发现CommandReference将null传递给CanExecute参数,但是Execute方法会传递正确的值。这正阻止我现在使用它。 - Matt Hamilton
好的,这可能有效,但有人能解释一下为什么需要它吗?为什么ContextMenus上的绑定只运行一次? - Jack Ukleja
谢谢。CommandParameter绑定可能需要像{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}这样的东西;因为MenuItem具有空DataContext。 - alexei
此外,要注意CommandReference实现中处理程序被过早回收的问题(这里这里)。 - alexei

5
我最近遇到了一个在ListBox中的ContextMenu的问题。我尝试使用MVVM方式绑定命令,但没有任何代码后台。最后我放弃了并向朋友寻求帮助。他找到了一个略微扭曲但简洁的解决方案。他将ListBox传递给ContextMenu的DataContext,然后通过访问ListBox的DataContext在视图模型中找到命令。这是我迄今为止看到的最简单的解决方案。没有自定义代码,没有标签,只有纯XAML和MVVM。

我在Github上发布了一个完全可行的示例。以下是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>

2
我更倾向于另一个解决方案。 添加上下文菜单加载器事件。
<ContextMenu Loaded="ContextMenu_Loaded"> 
    <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
</ContextMenu> 

在事件中分配数据上下文。

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

0

我知道这已经是一个旧帖子了,但是我想为那些正在寻找不同方法来解决它的人添加另一种解决方案。

在我的情况下,我无法使相同的解决方案工作,因为我试图做其他事情:使用鼠标单击打开上下文菜单(就像带有附加子菜单的工具栏)并将命令绑定到我的模型。由于我使用了Event Trigger,PlacementTarget对象为空。

这是我发现的只使用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>

0
我发现在控件模板深处的上下文菜单中使用标签属性绑定非常有用:

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

这使得绑定到控件可用的任何数据上下文成为可能,该上下文菜单是从其中打开的。上下文菜单可以通过“PlacementTarget”访问单击的控件。如果单击的控件的标记属性绑定到所需的数据上下文,则从上下文菜单内部绑定到“PlacementTarget.Tag”将直接跳转到该数据上下文。

链接已失效 :( - FrankM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接