WPF: Displaying a Context Menu for a GridView's Items
Asked Answered
P

4

21

I have the following 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>

Now I would like to display a context menu on a right click on a bounded item that will allow me to retrieve the item selected when I handle the event in the code behind.

In what possible way can I accomplish this?


[Update]

Following Dennis Roche's code, I now have this:

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

But upon running, I am receiving this exception:

Cannot add content of type 'System.Windows.Controls.ContextMenu' to an object of type 'System.Object'. Error at object 'System.Windows.Controls.ContextMenu' in markup file 'MusicRepo_Importer;component/controls/trackgridcontrol.xaml'.

What is the problem?

Polyglot answered 14/4, 2009 at 14:31 Comment(1)
First error that I can see is that you are setting the ItemContainerStyle twice: first to a resource and then again locally. Also, the context menu needs to be a resource. It appears to be a bug with WPF. I will update my original post with a solution.Evaluate
E
21

Yes, add a ListView.ItemContainerStyle with the Context Menu.

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

NOTE: You need to reference the ContextMenu as a resource and cannot define it locally.

This will enable the context menu for the entire row. :)

Also see that I handle the PreviewMouseLeftButtonDown event so I can ensure the item is focused (and is the currently selected item when you query the ListView). I found that I had to this when changing focus between applications, this may not be true in your case.

Updated

In the code behind file you need to walk-up the visual tree to find the list container item as the original source of the event can be an element of the item template (e.g. a 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();
}

The MyVisualTreeHelper that is use a wrapper that I've written to quickly walk the visual tree. A subset is posted below.

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

I hope this additional information helps.

Dennis

Evaluate answered 14/4, 2009 at 23:3 Comment(3)
Can you elaborate on what needed to be done on PreviewMouseLeftButtonDown?Correctitude
I've updated the answer with details of what happens in the code-behind.Evaluate
Anyone trying to implement this, the answer to your question is using System.Windows.Media; using System.Windows.Media.Media3D;Jepum
B
12

Dennis,

Love the example, however I did not find any need for your 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>

Then inside the MenuItem_Click events I added code that looks like this:

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

mySelectedItem is used on the ListView.SelectedItem:

 <ListView x:Name="ListViewCreds" SelectedItem="{Binding mySelectedItem, UpdateSourceTrigger=PropertyChanged}" ....

Please tick me if it helps...

Babur answered 16/8, 2012 at 13:47 Comment(0)
D
3

You might be interested in the answers for this SO question - I had the same question but wasn't satisfied with using the mousedown event to capture the item that was clicked upon. Several people has responded with simple and easy to comprehend solutions that you might be interested in.

Summary : You can use the data context to pass the item through to the handler, or a command + command parameter setup.

Dieppe answered 15/7, 2009 at 18:8 Comment(0)
B
1

Here is another approach that uses one shared context menu for all list view items. This time without traversing through visual tree and without depending on ListView's selected item. Also it uses commands which I think are much better than handling click events. The context menu opens directly for clicked list element and element's DataContext is accessible for a command handler as command parameter. Here's the 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>

And C#:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        People = new ObservableCollection<Person>();
        People.Add(new Person() { Name = "Alice", Age = "32", Mail = "[email protected]" });
        People.Add(new Person() { Name = "Bob", Age = "28", Mail = "[email protected]" });
        People.Add(new Person() { Name = "George", Age = "33", Mail = "[email protected]" });

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

The trick is to copy ListViewItem's DataContext to ContextMenu's DataContext when the menu is about to be open, which is done in ListViewItem_ContextMenuOpening event handler. Parameterless {Binding} in CommandParameter does the rest.

AFIK it's possible to set ContextMenu's DataContext in XAML using some clever tricks, but in my experience, I think this is a more reliable approach.

Bedfellow answered 27/9, 2022 at 19:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.