为什么TextBlock不是Routed Event上的OriginalSource?
我正在为ListView
元素显示上下文菜单。 上下文菜单附加到ListView
的TextBlock
,如下所示。
正确显示上下文菜单,同时也会触发RoutedUIEvent。 问题是在Executed回调中, ExecutedRoutedEventArgs.OriginalSource是ListViewItem而不是TextBlock。
我尝试设置IsHitTestVisible
属性以及Background
(见下文),因为MSDN说OriginalSource是由命中测试决定的
请注意,我在ListView中使用GridView作为View。 这就是我想要获取TextBlock(获取列索引)的原因
主窗口
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似乎很难为这个特定场景设计。 特别是:
-
DisplayMemberBinding
和CellTemplate
属性不能一起使用。 即你可以指定一个或另一个,但不能同时指定两者。 如果指定DisplayMemberBinding
,则它优先,并且不提供显示格式的自定义,除了在隐式使用的TextBlock
的样式中应用setter。 -
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看起来像这样:
这样做是将Data
对象映射到它们各自的属性值,以及这些属性值的名称。 这样,当应用数据模板时, MenuItem
可以将CommandParameter
绑定到该属性值名称,因此可以在处理程序中访问它。
请注意,这不是使用DisplayMemberBinding
,而是使用CellTemplate
,并将显示成员绑定移动到模板中ContentPresenter
的Content
。 由于上述烦恼,这是必需的; 如果没有这个,就无法将用户定义的数据模板应用于用户定义的GridColumnDisplayData
对象,以正确显示其DisplayValue
属性。
这里有一些冗余,因为您必须绑定到属性路径,并将属性名称指定为转换器参数。 不幸的是,后者易受印刷错误的影响,因为在编译或运行时没有任何东西可以解决不匹配问题。 我想在Debug构建中,你可以添加一些reflection来通过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:
在此版本中,您可以看到单元格模板用于设置DataContext
值,该值包含绑定属性值和对TextBlock
的引用。 然后,这些值将由模板中的各个元素解压缩,即TextBlock.Text
属性和MenuItem.CommandParameter
属性。
这里明显的缺点是,因为显示成员必须绑定在声明的单元格模板中,所以必须为每列重复代码。 我没有看到重用模板的方法,以某种方式将属性名称传递给它。 (另一个版本有类似的问题,但它的实现要简单得多,因此复制/粘贴看起来并不那么繁琐)。
但它确实可以将TextBlock
引用发送到您的命令处理程序,这就是您所要求的。 那就是那个。 🙂