Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?

38,106

Solution 1

The issue with using the DataTrigger/Binding solution mentioned above are two-fold. The first is you actually end up with a binding warning that you can't find the relative source for the selected item. The bigger issue however is that you've cluttered up your data templates and made them specific to a ComboBox.

The solution I present better follows WPF designs in that it uses a DataTemplateSelector on which you can specify separate templates using its SelectedItemTemplate and DropDownItemsTemplate properties as well as ‘selector’ variants for both.

Note: Updated for C#9 with nullability enabled and using pattern matching during the search

public class ComboBoxTemplateSelector : DataTemplateSelector {

    public DataTemplate?         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector? SelectedItemTemplateSelector  { get; set; }
    public DataTemplate?         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container) {

        var itemToCheck = container;

        // Search up the visual tree, stopping at either a ComboBox or
        // a ComboBoxItem (or null). This will determine which template to use
        while(itemToCheck is not null
        and not ComboBox
        and not ComboBoxItem)
            itemToCheck = VisualTreeHelper.GetParent(itemToCheck);

        // If you stopped at a ComboBoxItem, you're in the dropdown
        var inDropDown = itemToCheck is ComboBoxItem;

        return inDropDown
            ? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
            : SelectedItemTemplate  ?? SelectedItemTemplateSelector?.SelectTemplate(item, container); 
    }
}

To make it easier to use in XAML, I've also included a markup extension that simply creates and returns the above class in its ProvideValue function.

public class ComboBoxTemplateSelectorExtension : MarkupExtension {

    public DataTemplate?         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector? SelectedItemTemplateSelector  { get; set; }
    public DataTemplate?         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
        => new ComboBoxTemplateSelector(){
            SelectedItemTemplate          = SelectedItemTemplate,
            SelectedItemTemplateSelector  = SelectedItemTemplateSelector,
            DropdownItemsTemplate         = DropdownItemsTemplate,
            DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
        };
}

And here's how you use it. Nice, clean and clear and your templates stay 'pure'

Note: 'is:' here is my xmlns mapping for where I put the class in code. Make sure to import your own namespace and change 'is:' as appropriate.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />

You can also use DataTemplateSelectors if you prefer...

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

Or mix and match! Here I'm using a template for the selected item, but a template selector for the DropDown items.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

Additionally, if you don't specify a Template or a TemplateSelector for the selected or dropdown items, it simply falls back to the regular resolving of data templates based on data types, again, as you would expect. So, for instance, in the below case, the selected item has its template explicitly set, but the dropdown will inherit whichever data template applies for the DataType of the object in the data context.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MyTemplate} />

Enjoy!

Solution 2

Simple solution:

<DataTemplate>
    <StackPanel>
        <TextBlock Text="{Binding Name}"/>
        <TextBlock Text="{Binding Address}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </StackPanel>
</DataTemplate>

(Note that the element that is selected and displayed in the box and not the list is not inside a ComboBoxItem hence the trigger on Null)

If you want to switch out the whole template you can do that as well by using the trigger to e.g. apply a different ContentTemplate to a ContentControl. This also allows you to retain a default DataType-based template selection if you just change the template for this selective case, e.g.:

<ComboBox.ItemTemplate>
    <DataTemplate>
        <ContentControl Content="{Binding}">
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
                                        Value="{x:Null}">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <!-- ... -->
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>
</ComboBox.ItemTemplate>

Note that this method will cause binding errors as the relative source is not found for the selected item. For an alternate approach see MarqueIV's answer.

Solution 3

I was going to suggest using the combination of an ItemTemplate for the combo items, with the Text parameter as the title selection, but I see that ComboBox doesn't respect the Text parameter.

I dealt with something similar by overriding the ComboBox ControlTemplate. Here's the MSDN website with a sample for .NET 4.0.

In my solution, I change the ContentPresenter in the ComboBox template to have it bind to Text, with its ContentTemplate bound to a simple DataTemplate that contains a TextBlock like so:

<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
    <TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>

with this in the ControlTemplate:

<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>

With this binding link, I am able to control the Combo selection display directly via the Text parameter on the control (which I bind to an appropriate value on my ViewModel).

Solution 4

I used next approach

 <UserControl.Resources>
    <DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
        <TextBlock Text="{Binding Path=ShortName}" />
    </DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
    <ComboBox DisplayMemberPath="FullName"
              ItemsSource="{Binding Path=Offsets}"
              behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
              SelectedItem="{Binding Path=Selected}" />
    <TextBlock Text="User Time" />
    <TextBlock Text="" />
</StackPanel>

And the behavior

public static class SelectedItemTemplateBehavior
{
    public static readonly DependencyProperty SelectedItemDataTemplateProperty =
        DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));

    public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
    {
        element.SetValue(SelectedItemDataTemplateProperty, value);
    }

    public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
    {
        return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
    }

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = d as ComboBox;
        if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
        {
            uiElement.Loaded -= UiElementLoaded;
            UpdateSelectionTemplate(uiElement);
            uiElement.Loaded += UiElementLoaded;

        }
    }

    static void UiElementLoaded(object sender, RoutedEventArgs e)
    {
        UpdateSelectionTemplate((ComboBox)sender);
    }

    private static void UpdateSelectionTemplate(ComboBox uiElement)
    {
        var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
        if (contentPresenter == null)
            return;
        var template = uiElement.GetSelectedItemDataTemplate();
        contentPresenter.ContentTemplate = template;
    }


    public static T GetChildOfType<T>(DependencyObject depObj)
        where T : DependencyObject
    {
        if (depObj == null) return null;

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = (child as T) ?? GetChildOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }
}

worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want

Solution 5

In addition to what said by H.B. answer, the Binding Error can be avoided with a Converter. The following example is based from the Solution edited by the OP himself.

The idea is very simple: bind to something that alway exists (Control) and do the relevant check inside the converter. The relevant part of the modified XAML is the following. Please note that Path=IsSelected was never really needed and ComboBoxItem is replaced with Control to avoid the binding errors.

<DataTrigger Binding="{Binding 
    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
    Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
    Value="{x:Null}">
  <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>

The C# Converter code is the following:

public class ComboBoxItemIsSelectedConverter : IValueConverter
{
    private static object _notNull = new object();
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // value is ComboBox when the item is the one in the closed combo
        if (value is ComboBox) return null; 

        // all the other items inside the dropdown will go here
        return _notNull;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Share:
38,106
kingfleur
Author by

kingfleur

I help companies and teams turn legacy code into maintainable software. You can see my work on Twitter, my blog, Careers, LinkedIn, BitBucket and GitHub.

Updated on October 07, 2021

Comments

  • kingfleur
    kingfleur over 2 years

    I have a WPF Combobox which is filled with, say, Customer objects. I have a DataTemplate:

    <DataTemplate DataType="{x:Type MyAssembly:Customer}">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
            <TextBlock Text="{Binding Address}" />
        </StackPanel>
    </DataTemplate>
    

    This way, when I open my ComboBox, I can see the different Customers with their Name and, below that, the Address.

    But when I select a Customer, I only want to display the Name in the ComboBox. Something like:

    <DataTemplate DataType="{x:Type MyAssembly:Customer}">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
        </StackPanel>
    </DataTemplate>
    

    Can I select another Template for the selected item in a ComboBox?

    Solution

    With help from the answers, I solved it like this:

    <UserControl.Resources>
        <ControlTemplate x:Key="SimpleTemplate">
            <StackPanel>
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </ControlTemplate>
        <ControlTemplate x:Key="ExtendedTemplate">
            <StackPanel>
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text="{Binding Address}" />
            </StackPanel>
        </ControlTemplate>
        <DataTemplate x:Key="CustomerTemplate">
            <Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
                    <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </UserControl.Resources>
    

    Then, my ComboBox:

    <ComboBox ItemsSource="{Binding Customers}" 
                    SelectedItem="{Binding SelectedCustomer}"
                    ItemTemplate="{StaticResource CustomerTemplate}" />
    

    The important part to get it to work was Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (the part where value should be x:Null, not True).

  • kingfleur
    kingfleur over 13 years
    Not quite sure this is what I'm looking for. I want the look of a ComboBox that isn't 'active' (i.e. user hasn't clicked on it, it isn't 'open'), to show just one piece of text. But then, when the user clicks on it, it should open/dropdown, and every item should show two pieces of text (thus, a different template).
  • kingfleur
    kingfleur over 13 years
    I wanted to use two Templates, to keep it seperate. I used code from a sample project from this site: developingfor.net/net/dynamically-switch-wpf-datatemplate.ht‌​ml. But while it worked for a ListBox, it didn't work for a ComboBox. Your last sentence solved it or me though. The selected item in a ComboBox doesn't have IsSelected = True, but it's Null. See my edit above for full code how I solved it. Thanks a lot!
  • kingfleur
    kingfleur over 13 years
    I tried that, using examples I also found here (developingfor.net/net/dynamically-switch-wpf-datatemplate.h‌​tml), but found it not so reusable, and I wanted to solve this in XAML only.
  • H.B.
    H.B. over 13 years
    Glad that it was useful even though it wasn't exactly what you asked for. I didn't know about the null-thing before trying to answer your question either, i experimented and found out about it that way.
  • cunningdave
    cunningdave over 13 years
    If you experiment with the code above, I think you'll get to where you want to go. By setting this control template, you can control the collapsed text of the combo via its Text property (or whatever property you like) thus allowing you to display your simple unselected text. You can modify the individual item texts by specifying the ItemTemplate when you create your combobox. (The ItemTemplate would presumably have a stackpanel and two TextBlocks, or whatever formating you like.)
  • springy76
    springy76 over 9 years
    IsSelected is not nullable and so never can be really NULL. You don't need Path=IsSelected, because the NULL check for a surrounding ComboBoxItem is totally sufficient.
  • Simon F
    Simon F about 9 years
    Sometimes the short text doesn't display for me, even though the ShortName property is set and OnPropertyChanged etc. Are you supposed to get a binding error? This pops up whenever the short name field goes from empty (not displaying properly) to filled, as well as on startup "System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ComboBoxItem', AncestorLevel='1''. BindingExpression:(no path); DataItem=null; target element is 'ContentControl' (Name=''); target property is 'NoTarget' (type 'Object')"
  • H.B.
    H.B. about 9 years
    @SimonF: I have no idea what your concrete circumstances are so i cannot give you any advice. I haven't had any problems with this, the bindings are absolutely standard. Aren't you using Artiom's approach? (As you mention ShortName.)
  • Simon F
    Simon F about 9 years
    @H.B. I am doing it your way + using springy76's suggestion. If you are not having this same binding error then that might be a good place for me to start looking.
  • Mark A. Donohoe
    Mark A. Donohoe over 8 years
    @Peter, see my addition. (Yes, I know this is old, but you may still like my answer for the future.)
  • kingfleur
    kingfleur over 8 years
    Very cool. And I indeed have those binding warnings (never found out where they came from, but didn't really take a look either). I can really check it out right now, but I might in the future.
  • Mark A. Donohoe
    Mark A. Donohoe over 8 years
    Glad to be of assistance. Just know if you're using this in your code, the return statement (return inDropDown above) uses the new C#6 ?. syntax so if you're not using VS 2015, just remove the '?' and explicitly check for nulls before calling SelectTemplate. I'll add that to the code.
  • henon
    henon almost 8 years
    I take my hat off to you for a really reusable solution!
  • Mark A. Donohoe
    Mark A. Donohoe almost 8 years
    Thanks! I appreciate that. If you can, please vote it up!
  • James McDuffie
    James McDuffie over 6 years
    I really prefer doing this way because it is overall much less code. And simple to see in the xaml template. Thanks for sharing this HB!
  • rollsch
    rollsch about 5 years
    For some reason when I implement this solution the ComboBoxTemplateSelector code is never executed, there are no binding errors either.
  • Mark A. Donohoe
    Mark A. Donohoe about 5 years
    Sounds like your XAML isn't set up properly. Try using your own TemplateSelector. If that doesn't work, make sure the XAML you think is being used is actually being used by changing other properties, like color, font, etc.
  • M C
    M C over 2 years
    Maybe it is worth mentioning that setting a DisplayMemberPath makes the ComboBox ignore the ItemTemplateSelector. The SelectTemplate function will not be called
  • Mark A. Donohoe
    Mark A. Donohoe over 2 years
    Good point! That said, usually you would be using one or the other. You generally use DisplayMemberPath when you want to use the default template but using a specific property, so you don’t have to specify your own template to do the same. You can think of it as a ToString specifically for use in that ItemsControl’s default item template.
  • Orace
    Orace over 2 years
    @MarkA.Donohoe please take a look at my answer : stackoverflow.com/a/69470062/361177
  • Mark A. Donohoe
    Mark A. Donohoe over 2 years
    This is a pretty novel approach using the same DataTemplateKey in two different scopes like that. However this method doesn't allow you to only set a template for the selected item while also defaulting to the normal resolution in the dropdown as it would 'pick up' the one in the outer control. The other issue is it now requires you to explicitly define the panel, which you may not always be able to do, or it may require a lot of extra set-up. Still, for quick one-offs, this seems pretty neat. My advice though is what you're solving is exactly why ItemTemplateSelector was created.