views:

2365

answers:

4

I'm trying to use the context menu in a listview to run some code that requires data from which item it originated from.

I initially just did this:

XAML:

    <ListView x:Name="lvResources" ScrollViewer.VerticalScrollBarVisibility="Visible">
      <ListView.Resources>
        <ContextMenu x:Key="resourceContextMenu">
            <MenuItem Header="Get Metadata" Name="cmMetadata" Click="cmMetadata_Click" />
        </ContextMenu>
      </ListView.Resources>
      <ListView.ItemContainerStyle>
          <Style TargetType="{x:Type ListViewItem}">
              <Setter Property="ContextMenu" Value="{StaticResource resourceContextMenu}" />
          </Style>
      </ListView.ItemContainerStyle>
 ...

C#:

    private void cmMetadata_Click(object sender, RoutedEventArgs e)
    {
      // code that needs item data here
    }

But I found that the originating listview item was not accessible that way.

I've read some tactics about how to get around this, like intercepting the MouseDown event and setting a private field to the listviewitem that was clicked, but that doesn't sit well with me as it seems a bit hacky to pass data around that way. And WPF is supposed to be easy, right? :) I've read this SO question and this MSDN forum question, but I'm still not sure how to really do this, as neither of those articles seem to work in my case. Is there a better way to pass the item that was clicked on through to the context menu?

Thanks!

+3  A: 

Well in the cmMetadata_Click handler, you can just query the lvResources.SelectedItem property, since lvResources will be accessible from the code-behind file that the click handler is located in. It's not elegant, but it will work.

If you want to be a little more elegant, you could change where you set up your ContextMenu. For example, you could try something like this:

<ListView x:Name="lvResources" ScrollViewer.VerticalScrollBarVisibility="Visible">
 <ListView.Style>
  <Style TargetType="ListView">
   <Setter Property="ItemContainerStyle">
    <Setter.Value>
     <Style TargetType="{x:Type ListViewItem}">
      <Setter Property="Template">
       <Setter.Value>
        <ControlTemplate TargetType="{x:Type ListViewItem}">
         <TextBlock Text="{TemplateBinding Content}">
          <TextBlock.ContextMenu>
           <ContextMenu>
            <MenuItem Header="Get Metadata" Name="cmMetadata" Click="cmMetadata_Click" 
             DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"/>
           </ContextMenu>
          </TextBlock.ContextMenu>
         </TextBlock>
        </ControlTemplate>
       </Setter.Value>
      </Setter>
     </Style>
    </Setter.Value>
   </Setter>
  </Style>
 </ListView.Style>
 <ListViewItem>One Item</ListViewItem>
 <ListViewItem>Another item</ListViewItem>
</ListView>

What this does is plug in a template for your ListViewItem, and then you can use the handy TemplatedParent shortcut to assign the ListViewItem to the DataContext of your menu item.

Now your code-behind looks like this:

private void cmMetadata_Click(object sender, RoutedEventArgs e)
{
    MenuItem menu = sender as MenuItem;
    ListViewItem item = menu.DataContext as ListViewItem;
}

Obviously the downside is you will now need to complete the template for a ListViewItem, but I'm sure you can find one that will suit your needs pretty quickly.

Charlie
+1 for a nice and simple solution!
patjbs
I wanted to note that you mention simply using the listview selection to determine what was clicked. That doesn't work well in the case where users have selected multiple items, but still just want to execute the code on the item they clicked, which you wouldn't be able to detect in that case.
patjbs
+1 It helped me a lot.
MartyIX
+1  A: 

So I decided to try and implement a command solution. I'm pretty pleased with how it's working now.

First, created my command:

public static class CustomCommands
{
    public static RoutedCommand DisplayMetadata = new RoutedCommand();
}

Next in my custom listview control, I added a new command binding to the constructor:

public SortableListView()
{
    CommandBindings.Add(new CommandBinding(CustomCommands.DisplayMetadata, DisplayMetadataExecuted, DisplayMetadataCanExecute));
}

And also there, added the event handlers:

public void DisplayMetadataExecuted(object sender, ExecutedRoutedEventArgs e)
{
    var nbSelectedItem = (MyItem)e.Parameter;

    // do stuff with selected item
}

public void DisplayMetadataCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
    e.Handled = true;
}

I was already using a style selector to dynamically assign styles to the listview items, so instead of doing this in the xaml, I have to set the binding in the codebehind. You could do it in the xaml as well though:

public override Style SelectStyle(object item, DependencyObject container)
{
    ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(container);
    MyItem selectedItem = (MyItem)item;
    Style s = new Style();

    var listMenuItems = new List<MenuItem>();
    var mi = new MenuItem();
    mi.Header= "Get Metadata";
    mi.Name= "cmMetadata";
    mi.Command = CustomCommands.DisplayMetadata;
    mi.CommandParameter = selectedItem;
    listMenuItems.Add(mi);

    ContextMenu cm = new ContextMenu();
    cm.ItemsSource = listMenuItems;

    // Global styles
    s.Setters.Add(new Setter(Control.ContextMenuProperty, cm));

    // other style selection code

    return s;
}

I like the feel of this solution much better than attempting to set a field on mouse click and try to access what was clicked that way.

patjbs
+2  A: 

Similar to Charlie's answer, but shouldn't require XAML changes.

private void cmMetadata_Click(object sender, RoutedEventArgs e)
{
    MenuItem menu = sender as MenuItem;
    ListViewItem lvi = lvResources.ItemContainerGenerator.ContainerFromItem(menu.DataContext) as ListViewItem;
}
Bryce Kahle
A: 

Hallo, I'am reading Your discussion and is very helpful but when I use GridViewColumnHeader data is unvisible. How must I modify ListViewStyle so I can use code like this...

  <ListView.View>
            <GridView>
                <GridView.Columns>

                    <GridViewColumn Header="ID" DisplayMemberBinding="{Binding Path=ID}"></GridViewColumn>
                    <GridViewColumn Header="FirstName" DisplayMemberBinding="{Binding Path=FirstName}"></GridViewColumn>

And is there any way to get concreate column in listviewitem.. e.g. I invoke context menu on some row and I need get information about row, and property thet is bind to this column

thank you very much and sorry for my english.

Mich.

Michal