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

<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中的View。这就是我想要进入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();
            }));
        }
    }
}

最佳答案

关于您的问题,或者更确切地说,关于WPF的一件令人沮丧的事情是,它与您所提出的方案有关,这是WPF似乎不适用于该特定方案。特别是:


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


但是,即使有这些挫败感,我仍然能够找到两种不同的方式来解决您的问题。一种方法直接针对您的确切要求,另一种则退后一步,(我希望)解决您要解决的更广泛的问题。

第二个路径的代码比第一个路径简单,因此IMHO更好,因为它不涉及摆弄可视化树以及树中各个元素之间相对位置的实现细节。因此,我将首先展示(即从复杂的角度讲,这实际上是“第一”路径,而不是“第二” :))。

首先,您将需要一些辅助课程:

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,并将显示成员绑定移动到模板中ContentContentPresenter中。由于上述麻烦,所以需要这样做;没有这个,就无法将用户定义的数据模板应用于用户定义的GridColumnDisplayData对象,以正确显示其DisplayValue属性。

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


在您的问题和评论中,您表示希望回到树上,以便更直接地查找属性名称。即到命令参数中的,传递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引用可靠地发送到您的命令处理程序。就是这样。 :)

关于c# - 为什么TextBlock不是路由事件上的OriginalSource?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/44549611/

10-13 06:25