75

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).

AwkwardCoder
  • 24,893
  • 27
  • 82
  • 152
Peter
  • 13,733
  • 11
  • 75
  • 122
  • 2
    Your solution works, but I get errors in the Output window. `System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ComboBoxItem', AncestorLevel='1''. BindingExpression:Path=IsSelected; DataItem=null; target element is 'ContentPresenter' (Name=''); target property is 'NoTarget' (type 'Object')` – user11909 Apr 06 '17 at 08:35
  • 1
    I remember seeing these errors too. But I'm no longer on the project (or even in the company), so I can't check this, sorry. – Peter Apr 06 '17 at 08:41
  • The mentioning of the Binding Path in the DataTrigger is unnecessary. As the ComboBoxItem becomes selected, a different template will be applied to the control and the DataTrigger binding will no longer be able to find an ancestor of type ComboBoxItem in its element tree. Thus, the comparison with null will always be successful. This approach works because the Visual tree of the ComboBoxItem is different depending on whether it is selected or displayed in the popup. – Dennis Kassel Oct 24 '18 at 14:00

7 Answers7

80

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!

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • 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. – Peter Oct 30 '15 at 08:16
  • 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. – Mark A. Donohoe Oct 30 '15 at 15:26
  • 5
    I take my hat off to you for a really reusable solution! – henon Jul 18 '16 at 14:34
  • Thanks! I appreciate that. If you can, please vote it up! – Mark A. Donohoe Jul 18 '16 at 14:37
  • For some reason when I implement this solution the ComboBoxTemplateSelector code is never executed, there are no binding errors either. – rollsch Apr 01 '19 at 05:45
  • 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. – Mark A. Donohoe Apr 02 '19 at 15:17
  • Maybe it is worth mentioning that setting a `DisplayMemberPath` makes the ComboBox ignore the `ItemTemplateSelector`. The `SelectTemplate` function will not be called – M C Sep 14 '21 at 14:53
  • 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. – Mark A. Donohoe Sep 14 '21 at 14:57
  • @MarkA.Donohoe please take a look at my answer : https://stackoverflow.com/a/69470062/361177 – Orace Oct 06 '21 at 17:23
  • Instead of using VisualTreeHelper, you could use FrameworkElement::TemplatedParent. Should be faster than walking up the tree. – LaniusExcubitor May 15 '23 at 08:12
35

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.

Community
  • 1
  • 1
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • I wanted to use two Templates, to keep it seperate. I used code from a sample project from this site: http://www.developingfor.net/net/dynamically-switch-wpf-datatemplate.html. 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! – Peter Jan 14 '11 at 17:25
  • 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. – H.B. Jan 14 '11 at 20:49
  • 4
    `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. – springy76 Sep 04 '14 at 09:20
  • 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')" – Simon F Apr 27 '15 at 16:16
  • @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`.) – H.B. Apr 27 '15 at 19:16
  • @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. – Simon F Apr 28 '15 at 20:11
  • @Peter, see my addition. (Yes, I know this is old, but you may still like my answer for the future.) – Mark A. Donohoe Oct 29 '15 at 18:14
  • 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! – James McDuffie Aug 15 '17 at 00:56
2

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();
    }
}
raf
  • 131
  • 1
  • 4
1

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

Artiom
  • 7,694
  • 3
  • 38
  • 45
1

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).

cunningdave
  • 1,470
  • 10
  • 13
  • 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). – Peter Jan 14 '11 at 16:19
  • 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.) – cunningdave Jan 18 '11 at 16:29
0

Yes. You use a Template Selector to determine which template to bind at run-time. Thus if IsSelected = False then Use this template, if IsSelected = True, use this other template.

Of Note: Once you implement your template selector, you will need to give the templates keynames.

CodeWarrior
  • 7,388
  • 7
  • 51
  • 78
  • I tried that, using examples I also found here (http://www.developingfor.net/net/dynamically-switch-wpf-datatemplate.html), but found it not so reusable, and I wanted to solve this in XAML only. – Peter Jan 14 '11 at 17:31
0

I propose this solution without DataTemplateSelector, Trigger, binding nor behavior.

The first step is to put the ItemTemplate (of the selected element) in the ComboBox resources and the ItemTemplate (of the item in the drop down menu) in the ComboBox.ItemsPanel resources and give both resources the same key.

The second step is to postpone the ItemTemplate resolution at run time by using both a ContentPresenter and a DynamicResource in the actual ComboBox.ItemTemplate implementation.

<ComboBox ItemsSource="{Binding Items, Mode=OneWay}">

    <ComboBox.Resources>
        <!-- Define ItemTemplate resource -->
        <DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
            <TextBlock Text="{Binding FieldOne, Mode=OneWay}" />
        </DataTemplate>
    </ComboBox.Resources>

    <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Grid.IsSharedSizeScope="True"
                        IsItemsHost="True">
                <StackPanel.Resources>
                    <!-- Redefine ItemTemplate resource -->
                    <DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" SharedSizeGroup="GroupOne" />
                                <ColumnDefinition Width="10" SharedSizeGroup="GroupSpace" />
                                <ColumnDefinition Width="Auto" SharedSizeGroup="GroupTwo" />
                            </Grid.ColumnDefinitions>
                
                            <TextBlock Grid.Column="0" Text="{Binding FieldOne, Mode=OneWay}" />
                            <TextBlock Grid.Column="2" Text="{Binding FieldTwo, Mode=OneWay}" />
                        </Grid>
                    </DataTemplate>
                </StackPanel.Resources>
            </StackPanel>
        </ItemsPanelTemplate>
    </ComboBox.ItemsPanel>

    <ComboBox.ItemTemplate>
        <DataTemplate>
            <ContentPresenter ContentTemplate="{DynamicResource ItemTemplate}" />
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>
Orace
  • 7,822
  • 30
  • 45
  • 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. – Mark A. Donohoe Oct 07 '21 at 16:40