Почему TextBlock не является источником OriginalSource на маршрутизируемом событии?
Я показываю контекстное меню для элементов в ListView
, Контекстное меню прикреплено к TextBlock
с ListView
следующее.
<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 определяется тестированием попаданий
Обратите внимание, что я использую GridView в качестве представления в ListView. Это причина, по которой я хочу попасть в TextBlock (чтобы получить индекс столбца)
MainWindow
<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();
}));
}
}
}
1 ответ
Одна из неприятных вещей в вашем вопросе, а точнее... в отношении WPF в том смысле, в каком он связан со сценарием, заданным в вашем вопросе, заключается в том, что WPF кажется плохо разработанным для этого конкретного сценария. Особенно:
-
DisplayMemberBinding
а такжеCellTemplate
свойства не работают вместе. Т.е. вы можете указать одно или другое, но не оба. Если вы укажете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
и перемещает привязку элемента дисплея в Content
для ContentPresenter
в шаблоне. Это необходимо из-за вышеупомянутого раздражения; без этого невозможно применить пользовательский шаблон данных к пользовательскому GridColumnDisplayData
объект, чтобы правильно отобразить его DisplayValue
имущество.
Здесь есть некоторая избыточность, потому что вы должны привязать путь к свойству, а также указать имя свойства в качестве параметра конвертера. И, к сожалению, последний подвержен опечаткам, так как во время компиляции или выполнения нет ничего, что могло бы выявить несоответствие. Я предполагаю, что в сборке Debug вы могли бы добавить некоторое отражение, чтобы получить значение свойства по имени свойства, указанному в параметре конвертера, и убедиться, что оно совпадает с указанным в пути привязки.
В своих вопросах и комментариях вы выразили желание пройтись вверх по дереву, чтобы более точно найти имя свойства. Т.е. в параметре команды передайте 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
ссылка на ваш обработчик команд, который вы и просили. Итак, это так.:)