为什么TextBlock不是Routed事件上的OriginalSource? [英] Why is the TextBlock not the OriginalSource on the Routed Event?

查看:79
本文介绍了为什么TextBlock不是Routed事件上的OriginalSource?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在显示ListView中元素的上下文菜单.上下文菜单附加到ListViewTextBlock上,如下所示.

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>

上下文菜单会正确显示,同时也会触发RoutedUIEvent.问题在于,在执行的回调中,

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.

我尝试设置IsHitTestVisible属性以及Background(请参见下文),因为MSDN表示

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

请注意,我正在使用GridView作为ListView中的视图.这就是我想要进入TextBlock(获取列索引)的原因

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)

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

推荐答案

关于您的问题,或者更确切地说,关于WPF的令人沮丧的事情之一,因为它与您所提出的问题有关,是WPF似乎不适合该特定项目设想.特别是:

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. DisplayMemberBindingCellTemplate属性不能一起工作. IE.您可以指定一个,但不能两个都指定.如果指定DisplayMemberBinding,则优先级,并且不对显示格式进行自定义,除了为隐式使用的TextBlock样式应用设置程序外.
  2. DisplayMemberBinding 不参与WPF其他地方常见的隐式数据模板行为.也就是说,当您使用此属性时,控件将显式使用TextBlock显示数据,并将值绑定到TextBlock.Text属性.因此,最好绑定到string值.如果您尝试使用其他类型,WPF不会为您查找任何其他数据模板.
  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.

第二个路径的代码比第一个路径简单,因此IMHO更好,因为它不涉及摆弄可视化树以及树中各个元素彼此相对的实现细节. .因此,我将首先说明这一点(即从复杂的角度讲,这实际上是第一"路径,而不是第二"路径:)).

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

XAML看起来像这样:

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>

这是将Data对象映射到它们各自的属性值以及这些属性值的名称.这样,在应用数据模板时,MenuItem可以将CommandParameter绑定到该属性值名称,以便可以在处理程序中访问它.

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.

请注意,它使用的是CellTemplate,而不是使用DisplayMemberBinding,并将显示成员绑定移动到模板中ContentPresenterContent中.由于上述麻烦,所以需要这样做;没有这个,就无法将用户定义的数据模板应用于用户定义的GridColumnDisplayData对象,以正确显示其DisplayValue属性.

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.

这里有些多余,因为您必须绑定到属性路径,并且将属性名称指定为转换器参数.不幸的是,后者很容易出现印刷错误,因为在编译时或运行时,没有任何东西会引起不匹配.我想在Debug构建中,您可以添加一些反射以通过converter参数中指定的属性名称检索属性值,并确保它与绑定路径中指定的属性名称相同.

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.


在您的问题和评论中,您表示希望回到树上,以便更直接地查找属性名称. IE.到命令参数中的,传递TextBlock对象引用,然后使用它来导航回到绑定的属性名称.从某种意义上说,这更可靠,因为它直接与属性名称绑定.另一方面,在我看来,根据视觉树的确切结构以及在其中找到的绑定,它更易碎.从长远来看,这似乎可能会导致更高的维护成本.


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

类似地,一个转换器(这次是IMultiValueConverter)为每个单元创建该类的实例:

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

最后是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>

在此版本中,您可以看到单元格模板用于设置DataContext值,该值既包含绑定属性值,又包含对TextBlock的引用.然后,这些值由模板中的各个元素(即TextBlock.Text属性和MenuItem.CommandParameter属性)解包.

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

但是确实确实将TextBlock引用发送给您的命令处理程序,这正是您所要求的.就是这样. :)

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

这篇关于为什么TextBlock不是Routed事件上的OriginalSource?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆