WPF:为GridView的项显示上下文菜单

21

我有以下的 GridView:

<ListView Name="TrackListView" ItemContainerStyle="{StaticResource itemstyle}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Title" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn Header="Artist" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Album.Artist.Name}" />
            <GridViewColumn Header="Album" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Album.Name}"/>
            <GridViewColumn Header="Length" Width="100" HeaderTemplate="{StaticResource BlueHeader}"/>
        </GridView>
     </ListView.View>
</ListView>

现在,我想在绑定的项目上右键单击时显示一个上下文菜单,允许我在代码后端处理事件时检索所选的项目。

有哪些可能的方法可以实现这个功能?


[更新]

根据Dennis Roche的代码,我现在有了以下代码:

    <ListView Name="TrackListView" ItemContainerStyle="{StaticResource itemstyle}">
        <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}">
                <EventSetter Event="PreviewMouseLeftButtonDown" Handler="OnListViewItem_PreviewMouseLeftButtonDown" />
                <Setter Property="ContextMenu">
                    <Setter.Value>
                        <ContextMenu>
                            <MenuItem Header="Add to Playlist"></MenuItem>
                        </ContextMenu>
                     </Setter.Value>
                </Setter>
            </Style>
        </ListView.ItemContainerStyle>

        <ListView.View>
            <GridView>
                <GridViewColumn Header="Title" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Artist" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Album.Artist.Name}" />
                <GridViewColumn Header="Album" Width="100" HeaderTemplate="{StaticResource BlueHeader}" DisplayMemberBinding="{Binding Album.Name}"/>
                <GridViewColumn Header="Length" Width="100" HeaderTemplate="{StaticResource BlueHeader}"/>
            </GridView>
         </ListView.View>
    </ListView>

但是在运行时,我收到了这个异常:

无法将类型为 'System.Windows.Controls.ContextMenu' 的内容添加到类型为 'System.Object' 的对象中。 错误发生在标记文件 'MusicRepo_Importer;component/controls/trackgridcontrol.xaml' 中的对象 'System.Windows.Controls.ContextMenu'。

问题出在哪里?


1
我能看到的第一个错误是您两次设置了ItemContainerStyle:首先是作为资源,然后再本地设置。此外,上下文菜单需要成为资源。这似乎是WPF的一个bug。我将在原帖中更新解决方案。 - Dennis
4个回答

21
是的,添加一个带有上下文菜单的ListView.ItemContainerStyle。
<ListView>
  <ListView.Resources>
    <ContextMenu x:Key="ItemContextMenu">
      ...
    </ContextMenu>
  </ListView.Resources>
  <ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListViewItem}">
      <EventSetter Event="PreviewMouseLeftButtonDown" Handler="OnListViewItem_PreviewMouseLeftButtonDown" />
      <Setter Property="ContextMenu" Value="{StaticResource ItemContextMenu}"/>
    </Style>
  </ListView.ItemContainerStyle>
</ListView>

注意:您需要将ContextMenu作为资源引用,不能在本地定义它。
这将为整行启用上下文菜单。 :)
此外,请注意我处理PreviewMouseLeftButtonDown事件,以便确保该项已聚焦(并且在查询ListView时是当前选定的项)。我发现当在应用程序之间更改焦点时,必须这样做,但在您的情况下可能不是这样。
更新:
在代码后台文件中,您需要遍历可视树以查找列表容器项,因为事件的原始源可以是项模板的元素(例如stackpanel)。
void OnListViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  if (e.Handled)
    return;

  ListViewItem item = MyVisualTreeHelper.FindParent<ListViewItem>((DependencyObject)e.OriginalSource);
  if (item == null)
    return;

  if (item.Focusable && !item.IsFocused)
    item.Focus();
}

MyVisualTreeHelper是我编写的一个包装器,用于快速遍历可视化树。下面是其中的一个子集。

public static class MyVisualTreeHelper
{
  static bool AlwaysTrue<T>(T obj) { return true; }

  /// <summary>
  /// Finds a parent of a given item on the visual tree. If the element is a ContentElement or FrameworkElement 
  /// it will use the logical tree to jump the gap.
  /// If not matching item can be found, a null reference is returned.
  /// </summary>
  /// <typeparam name="T">The type of the element to be found</typeparam>
  /// <param name="child">A direct or indirect child of the wanted item.</param>
  /// <returns>The first parent item that matches the submitted type parameter. If not matching item can be found, a null reference is returned.</returns>
  public static T FindParent<T>(DependencyObject child) where T : DependencyObject
  {
    return FindParent<T>(child, AlwaysTrue<T>);
  }

  public static T FindParent<T>(DependencyObject child, Predicate<T> predicate) where T : DependencyObject
  {
    DependencyObject parent = GetParent(child);
    if (parent == null)
      return null;

    // check if the parent matches the type and predicate we're looking for
    if ((parent is T) && (predicate((T)parent)))
      return parent as T;
    else
      return FindParent<T>(parent);
  }

  static DependencyObject GetParent(DependencyObject child)
  {
    DependencyObject parent = null;
    if (child is Visual || child is Visual3D)
      parent = VisualTreeHelper.GetParent(child);

    // if fails to find a parent via the visual tree, try to logical tree.
    return parent ?? LogicalTreeHelper.GetParent(child);
  }
}

我希望这些额外的信息可以帮到您。
丹尼斯

你能详细说明一下PreviewMouseLeftButtonDown需要做什么吗? - bendewey
我已经更新了答案,详细说明了在代码后台发生的情况。 - Dennis
任何试图实现这个的人,你的答案是 using System.Windows.Media; using System.Windows.Media.Media3D; - SteveCav

11

Dennis,

很喜欢这个例子,不过我没有发现需要使用你的Visual Tree Helper...

   <ListView.Resources>
    <ContextMenu x:Key="ItemContextMenu">
        <MenuItem x:Name="menuItem_CopyUsername"
                  Click="menuItem_CopyUsername_Click"
                  Header="Copy Username">
            <MenuItem.Icon>
                <Image Source="/mypgm;component/Images/Copy.png" />
            </MenuItem.Icon>
        </MenuItem>
        <MenuItem x:Name="menuItem_CopyPassword"
                  Click="menuItem_CopyPassword_Click"
                  Header="Copy Password">
            <MenuItem.Icon>
                <Image Source="/mypgm;component/Images/addclip.png" />
            </MenuItem.Icon>
        </MenuItem>
        <Separator />
        <MenuItem x:Name="menuItem_DeleteCreds"
                  Click="menuItem_DeleteCreds_Click"
                  Header="Delete">
            <MenuItem.Icon>
                <Image Source="/mypgm;component/Images/Delete.png" />
            </MenuItem.Icon>
        </MenuItem>
    </ContextMenu>
</ListView.Resources>
<ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListViewItem}">
        <Setter Property="ContextMenu" Value="{StaticResource ItemContextMenu}" />
    </Style>
</ListView.ItemContainerStyle>

然后在 MenuItem_Click 事件中,我添加了类似于以下代码的内容:

private void menuItem_CopyUsername_Click(object sender, RoutedEventArgs e)
{
    Clipboard.SetText(mySelectedItem.Username);
}

mySelectedItem被用于ListView.SelectedItem:

 <ListView x:Name="ListViewCreds" SelectedItem="{Binding mySelectedItem, UpdateSourceTrigger=PropertyChanged}" ....
请在帮助您的情况下选中我...

3
您可能对这个SO问题的答案感兴趣-我曾经有同样的问题,但是使用mousedown事件来捕获点击的项并不满意。几个人用简单易懂的解决方案做出了回应,您可能会感兴趣。
总结:您可以使用数据上下文将项传递到处理程序中,或者使用命令+命令参数设置。

1

这里有另一种方法,它使用一个共享的上下文菜单来处理所有列表视图项。这次不需要遍历可视树,也不依赖于ListView的选定项。同时,它使用命令来代替处理点击事件,我认为这比处理点击事件更好。上下文菜单直接为点击的列表元素打开,并且元素的DataContext可以作为命令参数在命令处理程序中访问。以下是XAML代码:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
      <Window.CommandBindings>
        <CommandBinding Command="{x:Static ApplicationCommands.Open}" CanExecute="Open_CanExecute" Executed="Open_Executed" />
        <CommandBinding Command="{x:Static ApplicationCommands.Print}" CanExecute="Print_CanExecute" Executed="Print_Executed" />
      </Window.CommandBindings>
      <Grid>
        <ListView ItemsSource="{Binding People}">
          <ListView.Resources>
            <ContextMenu x:Key="cmItemContextMenu">
              <MenuItem Header="Open" Command="{x:Static ApplicationCommands.Open}" CommandParameter="{Binding}" />
              <MenuItem Header="Print" Command="{x:Static ApplicationCommands.Print}" CommandParameter="{Binding}" />
            </ContextMenu>
          </ListView.Resources>
          <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}">
              <Setter Property="ContextMenu" Value="{StaticResource cmItemContextMenu}" />
              <EventSetter Event="ContextMenuOpening" Handler="ListViewItem_ContextMenuOpening" />
            </Style>
          </ListView.ItemContainerStyle>
          <ListView.View>
            <GridView>
              <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
              <GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
              <GridViewColumn Header="Mail" Width="180" DisplayMemberBinding="{Binding Mail}" />
            </GridView>
          </ListView.View>
        </ListView>
      </Grid>
    </Window>

还有C#:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        People = new ObservableCollection<Person>();
        People.Add(new Person() { Name = "Alice", Age = "32", Mail = "alice32@example.com" });
        People.Add(new Person() { Name = "Bob", Age = "28", Mail = "bob28@example.com" });
        People.Add(new Person() { Name = "George", Age = "33", Mail = "george33@example.com" });

        InitializeComponent();

        DataContext = this;
    }

    public ObservableCollection<Person> People { get; }

    private void ListViewItem_ContextMenuOpening(object sender, ContextMenuEventArgs e)
    {
        var lvi = (ListViewItem)sender;
        var cm = lvi.ContextMenu;
        cm.DataContext = lvi.DataContext;
    }

    private void Open_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = e.Parameter is Person;
    }

    private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        var p = (Person)e.Parameter;
        MessageBox.Show($"Opening {p.Name}`s data.", "Open");
    }

    private void Print_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = e.Parameter is Person;
    }

    private void Print_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        var p = (Person)e.Parameter;
        MessageBox.Show($"Printing {p.Name}`s data.", "Print");
    }
}

诀窍是在ContextMenu即将打开时将ListViewItem的DataContext复制到它的DataContext中,在ListViewItem_ContextMenuOpening事件处理程序中完成此操作。CommandParameter中的无参数{Binding}完成其余操作。
据我所知,可以使用一些聪明的技巧在XAML中设置ContextMenu的DataContext,但根据我的经验,我认为这是一种更可靠的方法。

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