How can I overwrite my ListBox's ItemTemplate

2019-01-25 08:58发布

问题:

I have a generic style for a ListBox that overwrites the ItemTemplate to use RadioButtons. It works great, EXCEPT when I set a DisplayMemberPath. Then I just get the .ToString() of the item in the ListBox.

I feel like I'm missing something simple here... can someone help me spot it?

<Style x:Key="RadioButtonListBoxStyle" TargetType="{x:Type ListBox}">
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="KeyboardNavigation.DirectionalNavigation" Value="Cycle" />
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="{x:Type ListBoxItem}" >
                <Setter Property="Margin" Value="2, 2, 2, 0" />
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <Border Background="Transparent">
                                <RadioButton
                                    Content="{TemplateBinding ContentPresenter.Content}" VerticalAlignment="Center"
                                    IsChecked="{Binding Path=IsSelected,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}"/>

                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Setter.Value>
    </Setter>
</Style>

My ListBox is bound to a List<T> of KeyValuePairs. If I remove the Style, the DisplayMemberPath shows up correctly so it must be something with the style.

<ListBox Style="{StaticResource RadioButtonListBoxStyle}"
         ItemsSource="{Binding MyCollection}"
         DisplayMemberPath="Value" SelectedValuePath="Key" />

回答1:

Why exactly do you want to keep DisplayMemberPath? Its just a shortcut for an ItemTemplate containing a TextBlock showing the value in DisplayMemberPath. With your own ItemTemplate you have much more flexibility what and how you want to display. Just add a TextBlock into your ItemTemplate and set Text="{Binding Value}" and you have what you want.

As described here

This property is a simple way to define a default template that describes how to display the data objects.

DisplayMemberPath provides a simple way to a Template, but you want a different one. You can't and you don't need both.



回答2:

I still can't figure out how to get it to draw using the DisplayMemberPath, however I did find the piece I was missing to get it drawing using the ItemTemplate - I needed the ContentTemplate binding

<RadioButton 
    Content="{TemplateBinding ContentPresenter.Content}" 
    ContentTemplate="{TemplateBinding ContentPresenter.ContentTemplate}"
    IsChecked="{Binding Path=IsSelected,RelativeSource={
                        RelativeSource TemplatedParent},Mode=TwoWay}" />

Then in my XAML I can write:

<ListBox Style="{StaticResource RadioButtonListBoxStyle}"
         ItemsSource="{Binding MyCollection}"
         SelectedValuePath="Key">

    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Value}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Thanks to dowhilefor for pointing out that DisplayMemberPath is simply a shortcut way of writing the ItemTemplate, and that both cannot be set at once.



回答3:

I have been researching this same problem since morning and this thread helped me a lot. I investigated the solution and found out there is a way to support DisplayMemberPath even after defining a custom ListboxItem controltemplate in a style . The key is adding

    ContentTemplateSelector="{TemplateBinding ContentControl.ContentTemplateSelector}"

to the contentpresenter( the radiobutton in case of this thread).

The reason this will work is because internally the displaymemberpath property causes the Listbox to assign the ContentTemplateSelector to a "DisplayMemberTemplateSelector" template selector. Without this TemplateBinding in place, this selector does not take effect.

Cheers!



回答4:

tl;dr

Set an ItemContainerStyle with a ContentPresenter in it, and make sure not to overwrite the ItemTemplate.

The need

You want to define a generic style to be re-used.

You want to re-use it with different data types, so you want to be able to use DisplayMemberPath or ItemTemplate when re-using - without having to redefine the whole style.

The problem

You were not using a ContentPresenter on your item's container.
The item itself would have nowhere to be drawn.

The solution

For your concrete case

Put a ContentPresenter inside the RadioButton; that should work:

<RadioButton IsChecked="{Binding Path=IsSelected,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}"
             VerticalAlignment="Center">
    <ContentPresenter />
</RadioButton>

In general

Define the ItemContainerStyle. This lets you control how each item is wrapped.

That Style targets a ListBoxItem.

Define that style's Template, making sure to include a ContentPresenter (where the content of the item itself will be shown).

A sample

Defining the style
<Style TargetType="{x:Type ListBoxItem}" x:Key="BigListBoxItemStyle" BasedOn="{StaticResource DefaultListBoxItemStyle}">
    <Setter Property="Foreground" Value="DeepPink" />
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="Height" Value="71" />
    <Setter Property="FontSize" Value="18" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="BorderBrush" Value="Transparent" />
    <Setter Property="Padding" Value="10 5 10 5" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border Background="{TemplateBinding Background}"
                        Margin="{TemplateBinding Margin}"
                        BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="1" />
                        </Grid.RowDefinitions>
                        <ContentPresenter Grid.Row="0" Grid.Column="0"
                                          Margin="{TemplateBinding Padding}"
                                          VerticalAlignment="Center" />
                        <Rectangle x:Name="GraySeparator"
                                   Grid.Row="1"
                                   Height="1" Stroke="Gray" Opacity="0.2"
                                   HorizontalAlignment="Stretch" VerticalAlignment="Bottom" />
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True" >
                        <Setter Property="Background" Value="Yellow" />
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True" >
                        <Setter Property="BorderBrush" Value="DarkGreen" />
                        <Setter Property="Visibility" Value="Hidden" TargetName="GraySeparator" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Opacity" Value=".5" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="BigListBoxStyle" TargetType="{x:Type ListBox}">
    <Setter Property="ItemContainerStyle" Value="{StaticResource BigListBoxItemStyle}" />
</Style>
Using it
<ListBox Style="{StaticResource BigListBoxStyle}"
         ItemsSource="{Binding MyTuples}"
         DisplayMemberPath="Item2"
         SelectedItem="{Binding SelectedTuple}">