【问题标题】:WPF Context menu doesn't bind to right databound itemWPF 上下文菜单未绑定到正确的数据绑定项
【发布时间】:2010-10-14 07:30:44
【问题描述】:

在标签页上的用户控件的上下文菜单中绑定命令时遇到问题。 我第一次使用菜单(右键单击选项卡)时效果很好,但如果我切换选项卡,该命令将使用第一次使用的数据绑定实例。

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

谁能告诉我我做错了什么??

这是一个暴露问题的测试项目:

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

【问题讨论】:

    标签: wpf data-binding mvvm contextmenu


    【解决方案1】:

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

    因此,它们不会继承与它们所属的控件相同的源以进行绑定。处理的方法是绑定到 ContextMenu 本身的放置目标。

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

    【讨论】:

    • 我不相信这个答案。命令绑定确实适用于菜单项(它知道它必须绑定视图模型)......问题是当数据上下文由于切换选项卡而发生变化时,菜单项不会重新绑定。如果是因为它们不是视觉树的一部分,那么它是如何第一次工作的?
    • @Schnieder:欢迎使用 WPF! :D
    • 我仍然很想从 WPF 团队等处找到一个明确的解释。我可以建议你在答案中插入一段解释因为它不在可视化树、数据上下文中,因此当内容呈现器中的内容因选择选项卡而发生更改时,绑定不会更新(假设这是导致问题的原因)
    • WPF 4.0:(更短)。
    • @JoanComasFdz:这使得解决方案更加更整洁。意味着您不必调整每个 MenuItem Command 绑定。谢谢!
    【解决方案2】:

    我发现将命令绑定到上下文菜单项的最简洁的方法是使用一个名为 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 是从一个工作项目中复制而来的,为了便于说明而进行了简化。可能存在拼写错误或其他小错误。

    【讨论】:

    • Cyber​​Monk,您是否尝试过使用带有 CanExecute 代表的 RelayCommand?我发现 CommandReference 将 null 传递给 CanExecute 的参数,尽管 Execute 方法传递了正确的值。它阻止我现在使用它。
    • 好的,这可能有效,但谁能解释为什么需要它?为什么 ContextMenus 上的绑定只运行一次?
    • 谢谢。 CommandParameter 绑定可能需要类似于{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}};因为 MenuItem 有一个空的 DataContext。
    • 另外,请注意在 CommandReference 实现中过早收集处理程序的问题(herehere)。
    【解决方案3】:

    我最近遇到了与位于 ListBox 中的 ContextMenu 相同的问题。我试图在没有任何代码隐藏的情况下以 MVVM 方式绑定命令。我终于放弃了,我向一位朋友寻求帮助。他找到了一个稍微扭曲但简洁的解决方案。 他在 ContextMenu 的 DataContext 中传递 ListBox,然后通过访问 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>
    

    【讨论】:

      【解决方案4】:

      我更喜欢另一种解决方案。 添加上下文菜单加载器事件。

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

      【讨论】:

        【解决方案5】:

        我发现这种使用 Tag 属性的方法在从控件模板深处的上下文菜单绑定时非常有用:

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

        这使得绑定到任何可用于打开上下文菜单的控件的数据上下文成为可能。上下文菜单可以通过“PlacementTarget”访问被点击的控件。如果单击控件的 Tag 属性绑定到所需的数据上下文,则从上下文菜单中绑定到“PlacementTarget.Tag”会将您直接弹射到该数据上下文。

        【讨论】:

        • 链接已失效 :(
        【解决方案6】:

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

        我无法在我的情况下使用相同的解决方案,因为我试图做其他事情:通过鼠标单击打开上下文菜单(就像一个带有子菜单的工具栏)并将命令绑​​定到我的模型。由于我使用的是事件触发器,因此 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>
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-03-22
          • 1970-01-01
          • 2014-07-20
          • 1970-01-01
          • 1970-01-01
          • 2011-04-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多