为什么TextBlock不是Routed Event上的OriginalSource?

时间:2017-06-14 16:01:35

标签: c# wpf xaml routed-events routedeventargs

我正在显示ListView中元素的上下文菜单。上下文菜单附加到TextBlock的{​​{1}},如下所示。

ListView

正确显示上下文菜单,同时也会触发RoutedUIEvent。问题是在Executed回调中ExecutedRoutedEventArgs.OriginalSource是ListViewItem而不是TextBlock。

我尝试设置<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> 属性以及IsHitTestVisible(见下文),因为MSDN说OriginalSource is determined by hit testing

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

主窗口

Background

MainWindow.xaml.cs

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

1 个答案:

答案 0 :(得分:1)

关于你的问题的一个令人沮丧的事情,或者更确切地说......关于WPF,因为它与你提出的问题中提到的场景有关,WPF似乎很难为这个特定场景设计。特别是:

  1. DisplayMemberBindingCellTemplate属性一起工作。即你可以指定一个或另一个,但不能同时指定两者。如果指定DisplayMemberBinding,则它优先,并且不提供显示格式的自定义,除了在隐式使用的TextBlock的样式中应用setter。
  2. DisplayMemberBinding 不参与WPF中其他位置的常见隐式数据模板行为。也就是说,当您使用此属性时,控件显式使用TextBlock来显示数据,将值绑定到TextBlock.Text属性。因此,您最好能够绑定到string值;如果您尝试使用其他类型,WPF不会为您查找任何其他数据模板。
  3. 然而,即使有这些挫折,我也能找到两种不同的途径来解决你的问题。一条路径直接关注您的确切要求,而另一条路径则退后一步(我希望)解决您尝试解决的更广泛问题。

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

    首先,你需要一个小帮手类:

    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,并将显示成员绑定移动到模板中Content的{​​{1}}。由于上述烦恼,这是必需的;如果没有这个,就无法将用户定义的数据模板应用于用户定义的ContentPresenter对象,以正确显示其GridColumnDisplayData属性。

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


    在您的问题和评论中,您曾表示希望走回树上以更直接地找到属性名称。即在命令参数中,传递DisplayValue对象引用,然后使用它来导航回到绑定的属性名称。从某种意义上说,这更可靠,因为它直接转到属性名称绑定。另一方面,在我看来,根据视觉树的确切结构和内部的绑定更脆弱。从长远来看,它似乎可能会带来更高的维护成本。

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

    TextBlock

    同样,转换器(这次是public class GridCellHelper { public object DisplayValue { get; set; } public UIElement UIElement { get; set; } } )为每个单元格创建该类的实例:

    IMultiValueConverter

    最后,XAML:

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

    在此版本中,您可以看到单元格模板用于设置包含绑定属性值和<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属性。

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

    但它 可靠地将MenuItem.CommandParameter引用发送到您的命令处理程序,这就是您要求的。那就是那个。 :)