2

I'm showing a context menu for elements in a ListView. The context menu is attached to the TextBlocks of the ListView as follows.

<ListView.Resources>
 <ContextMenu x:Key="ItemContextMenu">
  <MenuItem Command="local:MyCommands.Test" />
 </ContextMenu>
 <Style TargetType="{x:Type TextBlock}" >
  <Setter Property="ContextMenu" Value="{StaticResource ItemContextMenu}" />
 </Style>
</ListView.Resources>

The context menu properly shows up and the RoutedUIEvent is fired as well. The issue is that in the Executed callback the ExecutedRoutedEventArgs.OriginalSource is a ListViewItem and not the TextBlock.

I tried setting the IsHitTestVisible Property as well as the Background (see below), because MSDN says that the OriginalSource is determined by hit testing

Note that I'm using a GridView as the View in the ListView. This is the reason for me wanting to get to the TextBlock (to get the column index)

MainWindow

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <ListView>
        <ListView.Resources>
            <x:Array Type="{x:Type local:Data}" x:Key="Items">
                <local:Data Member1="First Item" />
                <local:Data Member1="Second Item" />
            </x:Array>
            <ContextMenu x:Key="ItemContextMenu">
                <MenuItem Header="Test" Command="local:MainWindow.Test" />
            </ContextMenu>
            <Style TargetType="{x:Type TextBlock}" >
                <Setter Property="ContextMenu" Value="{StaticResource ItemContextMenu}" />
                <Setter Property="IsHitTestVisible" Value="True" />
                <Setter Property="Background" Value="Wheat" />
            </Style>
        </ListView.Resources>
        <ListView.ItemsSource>
            <StaticResource ResourceKey="Items" />
        </ListView.ItemsSource>
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn Header="Member1" DisplayMemberBinding="{Binding Member1}"/>
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
</Window>

MainWindow.xaml.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Input;

namespace WpfApp1
{
    public class Data
    {
        public string Member1 { get; set; }
    }

    public partial class MainWindow : Window
    {
        public static RoutedCommand Test = new RoutedCommand();

        public MainWindow()
        {
            InitializeComponent();
            CommandBindings.Add(new CommandBinding(Test, (s, e) =>
            {
                Debugger.Break();
            }));
        }
    }
}
Dennis Kuypers
  • 546
  • 4
  • 16
  • Ya so in this case the ListView Item always has first hittestvisibility, I'm not sure but you could try putting `ClickMode="Press"` on the `TextBlock` but I'm not entirely sure that will work since it doesn't inherit from Button base, hence left as comment. – Chris W. Jun 14 '17 at 18:35
  • As noted, this is perfectly natural, since the event is in fact originally from the `ListViewItem`. You should rethink _why_ it is you think you need the `TextBlock` object. Lacking a full [mcve], no specific advice can be given. But you should be able to determine the column index via other means. You shouldn't need to dig into the view structure itself to determine what the user's clicking, as that information should have been bound in the XAML and sent with the click/command. – Peter Duniho Jun 14 '17 at 21:47
  • @PeterDuniho You're probably right, i should ask how to get the column instead. The ContextMenu is only shown when the TextBlock is clicked. If you click outside of it, but still on the ListViewItem no menu is shown. I assumed that the TextBlock was not considered as the OriginalSource because it lacks something that makes it a candidate for OriginalSource – Dennis Kuypers Jun 14 '17 at 23:29
  • @PeterDuniho I also added complete sample code – Dennis Kuypers Jun 14 '17 at 23:56
  • Thanks for the good sample. It would help if you could be more explicit about how you would get the column index, if you _could_ get the `TextBlock` object. It's still not clear to me in the sample, what your expectation there was. – Peter Duniho Jun 15 '17 at 01:03
  • Also, it'd help if you could explain why you want the index (a view concern), rather than the bound member value itself, or the whole data object. – Peter Duniho Jun 15 '17 at 01:18
  • @PeterDuniho In the context menu I would like to have an option "Filter the list to only show entries that have Member1 set to the same value as the selected item". In my example this is easily possible, because there is only one Member. But if i have two members/columns, there is no way for me to know which column the user selected an item in. The application Wireshark does a very similar thing where you can right click on any field to add that to the filter system. – Dennis Kuypers Jun 15 '17 at 12:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/146773/discussion-between-dennis-kuypers-and-peter-duniho). – Dennis Kuypers Jun 15 '17 at 13:55

1 Answers1

1

One of the frustrating things about your question, or rather…about WPF as it relates to the scenario posited in your question is that WPF seems poorly designed for this particular scenario. In particular:

  1. The DisplayMemberBinding and CellTemplate properties do not work together. I.e. you can specify one or the other, but not both. If you specify DisplayMemberBinding, it takes precedence and offers no customization of the display formatting, other than to apply setters in a style for the TextBlock that is implicitly used.
  2. The DisplayMemberBinding does not participate in the usual implicit data templating behavior found elsewhere in WPF. That is, when you use this property, the control explicitly uses TextBlock to display the data, binding the value to the TextBlock.Text property. So you'd darn well better be binding to a string value; WPF isn't going to look up any other data template for you, if you try to use a different type.

However, even with these frustrations, I was able to find two different paths to addressing your question. One path focuses directly on your exact request, while the other takes a step back and (I hope) addresses the broader issue you're trying to solve.

The second path results in simpler code than the first, and IMHO is better for that reason as well as because it does not involve fiddling around with the visual tree and implementation details of where various elements of that tree are relative to each other. So, I will show that first (i.e. in a convoluted sense, this is actually the "first" path, not the "second" :) ).

First, you will need a little helper class:

class GridColumnDisplayData
{
    public object DisplayValue { get; set; }
    public string ColumnProperty { get; set; }
}

Then you will need a converter to produce instances of that class for your grid cells:

class GridColumnDisplayDataConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return new GridColumnDisplayData { DisplayValue = value, ColumnProperty = (string)parameter };
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The XAML looks like this:

<Window x:Class="TestSO44549611TextBlockMenu.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO44549611TextBlockMenu"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <ListView>
    <ListView.Resources>
      <x:Array Type="{x:Type l:Data}" x:Key="Items">
        <l:Data Member1="First Item"/>
        <l:Data Member1="Second Item"/>
      </x:Array>
      <ContextMenu x:Key="ItemContextMenu">
        <MenuItem Header="Test" Command="l:MainWindow.Test"
                  CommandParameter="{Binding ColumnProperty}"/>
      </ContextMenu>
      <DataTemplate DataType="{x:Type l:GridColumnDisplayData}">
        <TextBlock Background="Wheat" Text="{Binding DisplayValue}"
                   ContextMenu="{StaticResource ItemContextMenu}"/>
      </DataTemplate>
      <l:GridColumnDisplayDataConverter x:Key="columnDisplayConverter"/>
    </ListView.Resources>
    <ListView.ItemsSource>
      <StaticResource ResourceKey="Items" />
    </ListView.ItemsSource>
    <ListView.View>
      <GridView>
        <GridView.Columns>
          <GridViewColumn Header="Member1">
            <GridViewColumn.CellTemplate>
              <DataTemplate>
                <ContentPresenter Content="{Binding Member1,
                            Converter={StaticResource columnDisplayConverter}, ConverterParameter=Member1}"/>
              </DataTemplate>
            </GridViewColumn.CellTemplate>
          </GridViewColumn>
        </GridView.Columns>
      </GridView>
    </ListView.View>
  </ListView>
</Window>

What this does is map the Data objects to their individual property values, as well as the name of those property values. That way, when the data template is applied, the MenuItem can bind the CommandParameter to that property value name, so it's accessible in the handler.

Note that rather than using DisplayMemberBinding, this uses CellTemplate, and moves the display member binding into the Content for the ContentPresenter in the template. This is required because of the afore-mentioned annoyance; without this, there's no way to apply a user-defined data template to the user-defined GridColumnDisplayData object, to properly display its DisplayValue property.

There's a bit of redundancy here, because you have to bind to the property path, as well as specify the property name as the converter parameter. And unfortunately, the latter is susceptible to typographical errors, since there's nothing at compile- or run-time that would catch a mismatch. I suppose in a Debug build, you could add some reflection to retrieve the property value by the property name given in the converter parameter and make sure it's the same as that given in the binding path.


In your question and comments, you had expressed a desire to walk back up the tree to find the property name more directly. I.e. to in the command parameter, pass the TextBlock object reference, and then use that to navigate your way back to the bound property name. In one sense, this is more reliable, as it goes directly to the property name bound. On the other hand, it seems to me that depending on the exact structure of the visual tree and the bindings found within is more fragile. In the long run, it seems likely to incur a higher maintenance cost.

That said, I did come up with a way that would accomplish that goal. First, as in the other example, you'll need a helper class to store the data:

public class GridCellHelper
{
    public object DisplayValue { get; set; }
    public UIElement UIElement { get; set; }
}

And similarly, a converter (this time, IMultiValueConverter) to create instances of that class for each cell:

class GridCellHelperConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return new GridCellHelper { DisplayValue = values[0], UIElement = (UIElement)values[1] };
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And finally, the XAML:

<Window x:Class="TestSO44549611TextBlockMenu.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO44549611TextBlockMenu"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <ListView>
    <ListView.Resources>
      <x:Array Type="{x:Type l:Data}" x:Key="Items">
        <l:Data Member1="First Item"/>
        <l:Data Member1="Second Item"/>
      </x:Array>
      <l:GridCellHelperConverter x:Key="cellHelperConverter"/>
    </ListView.Resources>
    <ListView.ItemsSource>
      <StaticResource ResourceKey="Items" />
    </ListView.ItemsSource>
    <ListView.View>
      <GridView>
        <GridView.Columns>
          <GridViewColumn Header="Member1">
            <GridViewColumn.CellTemplate>
              <DataTemplate>
                <TextBlock Background="Wheat" Text="{Binding DisplayValue}">
                  <TextBlock.DataContext>
                    <MultiBinding Converter="{StaticResource cellHelperConverter}">
                      <Binding Path="Member1"/>
                      <Binding RelativeSource="{x:Static RelativeSource.Self}"/>
                    </MultiBinding>
                  </TextBlock.DataContext>
                  <TextBlock.ContextMenu>
                    <ContextMenu>
                      <MenuItem Header="Test" Command="l:MainWindow.Test"
                        CommandParameter="{Binding UIElement}"/>
                    </ContextMenu>
                  </TextBlock.ContextMenu>
                </TextBlock>
              </DataTemplate>
            </GridViewColumn.CellTemplate>
          </GridViewColumn>
        </GridView.Columns>
      </GridView>
    </ListView.View>
  </ListView>
</Window>

In this version, you can see that the cell template is used to set up a DataContext value containing both the bound property value, and the reference to the TextBlock. These values are then unpacked by the individual elements in the template, i.e. the TextBlock.Text property and the MenuItem.CommandParameter property.

The obvious downside here is that, because the display member has to be bound inside the cell template being declared, the code has to be repeated for each column. I didn't see a way to reuse the template, somehow passing the property name to it. (The other version has a similar problem, but it's a much simpler implementation, so the copy/paste doesn't seem so onerous).

But it does reliably send the TextBlock reference to your command handler, which is what you asked for. So, there's that. :)

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • This means that i have to create a new context menu for each column. In the actual code I'm using a Behavior to generate the columns and keep them in sync with the configuration storage (so that on the next application start i get the same columns wit the same size). I should be able to dynamically attach and generate the menues, so your answer gave me a tip on how to solve this in my situation. Thank you! – Dennis Kuypers Jun 16 '17 at 13:54
  • I'm sorry. The first solution works! The UIElement Binding is correctly re-evaluated to the UIElement. I can use this. – Dennis Kuypers Jun 16 '17 at 14:01
  • It turns out that i wasn't able to get the column name due to the new MultiBinding, but I was able to set the column name on the TextBlock's .Tag Property – Dennis Kuypers Jun 16 '17 at 21:06
  • Hmm. Well, `MultiBinding` works fine for me. Not sure what was different in your case, but glad to hear you found a working alternative. – Peter Duniho Jun 16 '17 at 21:14