【问题标题】:Why is the TextBlock not the OriginalSource on the Routed Event?为什么 TextBlock 不是路由事件上的 OriginalSource?
【发布时间】:2017-06-14 16:01:35
【问题描述】:

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

<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 也被触发。问题是在 Executed 回调中 ExecutedRoutedEventArgs.OriginalSource 是 ListViewItem 而不是 TextBlock。

我尝试设置IsHitTestVisible 属性以及Background(见下文),因为MSDN 说OriginalSource is determined by hit testing

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

主窗口

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

【问题讨论】:

  • 是的,所以在这种情况下,ListView 项始终具有第一个 hittestvisibility,我不确定,但您可以尝试将 ClickMode="Press" 放在 TextBlock 上,但我不完全确定这会起作用它不继承自 Button 基础,因此保留为注释。
  • 如前所述,这是非常自然的,因为该事件实际上最初来自ListViewItem。您应该重新考虑为什么您认为您需要TextBlock 对象。缺少完整的minimal reproducible example,无法给出具体建议。但是您应该能够通过其他方式确定列索引。您不需要深入查看视图结构本身来确定用户单击了什么,因为该信息应该已绑定在 XAML 中并与单击/命令一起发送。
  • @PeterDuniho 你可能是对的,我应该问如何获取该列。 ContextMenu 仅在单击 TextBlock 时显示。如果您在其外部单击,但仍在 ListViewItem 上,则不会显示任何菜单。我假设 TextBlock 不被视为 OriginalSource,因为它缺少使其成为 OriginalSource 候选者的东西
  • @PeterDuniho 我还添加了完整的示例代码
  • 感谢您提供的优质样品。如果您可以获得TextBlock 对象,那么您可以更明确地说明如何获得列索引,这将有所帮助。在样本中我仍然不清楚你的期望是什么。

标签: c# wpf xaml routed-events routedeventargs


【解决方案1】:

关于您的问题,或者更确切地说……关于 WPF,因为它与您问题中提出的场景有关,其中一个令人沮丧的事情是 WPF 似乎针对这种特定场景设计得不好。特别是:

  1. DisplayMemberBindingCellTemplate 属性一起工作。 IE。您可以指定其中之一,但不能同时指定两者。如果您指定 DisplayMemberBinding,则它优先且不提供显示格式的自定义,除了在隐式使用的 TextBlock 的样式中应用设置器。
  2. DisplayMemberBinding 不参与 WPF 其他地方常见的隐式数据模板行为。也就是说,当您使用此属性时,控件显式使用TextBlock 来显示数据,将值绑定到TextBlock.Text 属性。所以你最好绑定到string 值;如果您尝试使用其他类型,WPF 不会为您查找任何其他数据模板。

但是,即使有这些挫败感,我还是能够找到两种不同的方法来解决您的问题。一条路径直接关注您的确切要求,而另一条路径后退一步(我希望)解决您试图解决的更广泛的问题。

第二条路径比第一条路径更简单,恕我直言,因为这个原因更好,因为它不涉及摆弄可视树和该树的各个元素相对于彼此的位置的实现细节.所以,我将首先展示(即在复杂的意义上,这实际上是“第一”路径,而不是“第二”:))。

首先,您需要一个小助手类:

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

然后您将需要一个转换器来为您的网格单元生成该类的实例:

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 如下所示:

<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 绑定到该属性值名称,因此可以在处理程序中访问它。

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

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


在您的问题和 cmets 中,您表示希望回到树上更直接地找到属性名称。 IE。在命令参数中,传递TextBlock 对象引用,然后使用它导航回到绑定的属性名称。从某种意义上说,这更可靠,因为它直接与属性名称绑定。另一方面,在我看来,取决于可视化树的确切结构和其中发现的绑定更脆弱。从长远来看,它似乎可能会产生更高的维护成本。

也就是说,我确实想出了一种可以实现该目标的方法。首先,与另一个示例一样,您需要一个帮助类来存储数据:

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

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

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 属性。

这里明显的缺点是,因为显示成员必须绑定在声明的单元格模板,所以必须为每一列重复代码。我没有看到重用模板的方法,以某种方式将属性名称传递给它。 (另一个版本也有类似的问题,但实现起来要简单得多,所以复制/粘贴似乎没有那么繁重)。

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

【讨论】:

  • 这意味着我必须为每一列创建一个新的上下文菜单。在实际代码中,我使用 Behavior 来生成列并使它们与配置存储保持同步(这样在下一次应用程序启动时,我会得到相同大小的相同列)。我应该能够动态附加和生成菜单,所以你的回答给了我一个关于如何在我的情况下解决这个问题的提示。谢谢!
  • 对不起。第一个解决方案有效! UIElement 绑定正确地重新评估为 UIElement。我可以用这个。
  • 事实证明,由于新的 MultiBinding,我无法获取列名,但我能够在 TextBlock 的 .Tag 属性上设置列名
  • 嗯。好吧,MultiBinding 对我来说很好用。不确定您的情况有何不同,但很高兴听到您找到了可行的替代方案。
猜你喜欢
  • 2014-10-31
  • 1970-01-01
  • 1970-01-01
  • 2016-12-23
  • 2018-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多