问题
I'm showing a context menu for elements in a ListView
. The context menu is attached to the TextBlock
s of the ListView
as follows.
<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>
The context menu properly shows up and the RoutedUIEvent is fired as well. The issue is that in the Executed callback the ExecutedRoutedEventArgs.OriginalSource is a ListViewItem and not the TextBlock.
I tried setting the IsHitTestVisible
Property as well as the Background
(see below), because MSDN says that the OriginalSource is determined by hit testing
Note that I'm using a GridView as the View in the ListView. This is the reason for me wanting to get to the TextBlock (to get the column index)
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:
One of the frustrating things about your question, or rather…about WPF as it relates to the scenario posited in your question is that WPF seems poorly designed for this particular scenario. In particular:
- The
DisplayMemberBinding
andCellTemplate
properties do not work together. I.e. you can specify one or the other, but not both. If you specifyDisplayMemberBinding
, it takes precedence and offers no customization of the display formatting, other than to apply setters in a style for theTextBlock
that is implicitly used. - The
DisplayMemberBinding
does not participate in the usual implicit data templating behavior found elsewhere in WPF. That is, when you use this property, the control explicitly usesTextBlock
to display the data, binding the value to theTextBlock.Text
property. So you'd darn well better be binding to astring
value; WPF isn't going to look up any other data template for you, if you try to use a different type.
However, even with these frustrations, I was able to find two different paths to addressing your question. One path focuses directly on your exact request, while the other takes a step back and (I hope) addresses the broader issue you're trying to solve.
The second path results in simpler code than the first, and IMHO is better for that reason as well as because it does not involve fiddling around with the visual tree and implementation details of where various elements of that tree are relative to each other. So, I will show that first (i.e. in a convoluted sense, this is actually the "first" path, not the "second" :) ).
First, you will need a little helper class:
class GridColumnDisplayData
{
public object DisplayValue { get; set; }
public string ColumnProperty { get; set; }
}
Then you will need a converter to produce instances of that class for your grid cells:
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();
}
}
The XAML looks like this:
<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>
What this does is map the Data
objects to their individual property values, as well as the name of those property values. That way, when the data template is applied, the MenuItem
can bind the CommandParameter
to that property value name, so it's accessible in the handler.
Note that rather than using DisplayMemberBinding
, this uses CellTemplate
, and moves the display member binding into the Content
for the ContentPresenter
in the template. This is required because of the afore-mentioned annoyance; without this, there's no way to apply a user-defined data template to the user-defined GridColumnDisplayData
object, to properly display its DisplayValue
property.
There's a bit of redundancy here, because you have to bind to the property path, as well as specify the property name as the converter parameter. And unfortunately, the latter is susceptible to typographical errors, since there's nothing at compile- or run-time that would catch a mismatch. I suppose in a Debug build, you could add some reflection to retrieve the property value by the property name given in the converter parameter and make sure it's the same as that given in the binding path.
In your question and comments, you had expressed a desire to walk back up the tree to find the property name more directly. I.e. to in the command parameter, pass the TextBlock
object reference, and then use that to navigate your way back to the bound property name. In one sense, this is more reliable, as it goes directly to the property name bound. On the other hand, it seems to me that depending on the exact structure of the visual tree and the bindings found within is more fragile. In the long run, it seems likely to incur a higher maintenance cost.
That said, I did come up with a way that would accomplish that goal. First, as in the other example, you'll need a helper class to store the data:
public class GridCellHelper
{
public object DisplayValue { get; set; }
public UIElement UIElement { get; set; }
}
And similarly, a converter (this time, IMultiValueConverter
) to create instances of that class for each cell:
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();
}
}
And finally, the 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>
In this version, you can see that the cell template is used to set up a DataContext
value containing both the bound property value, and the reference to the TextBlock
. These values are then unpacked by the individual elements in the template, i.e. the TextBlock.Text
property and the MenuItem.CommandParameter
property.
The obvious downside here is that, because the display member has to be bound inside the cell template being declared, the code has to be repeated for each column. I didn't see a way to reuse the template, somehow passing the property name to it. (The other version has a similar problem, but it's a much simpler implementation, so the copy/paste doesn't seem so onerous).
But it does reliably send the TextBlock
reference to your command handler, which is what you asked for. So, there's that. :)
来源:https://stackoverflow.com/questions/44549611/why-is-the-textblock-not-the-originalsource-on-the-routed-event