Why is the TextBlock not the OriginalSource on the Routed Event?

帅比萌擦擦* 提交于 2019-12-31 03:05:12

问题


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

回答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. :)



来源:https://stackoverflow.com/questions/44549611/why-is-the-textblock-not-the-originalsource-on-the-routed-event

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!