Search code examples
c#comboboxcontroltemplatewinui-3visualstates

ComboBox: How to show one property of Items when DropDown is open and a different one when DropDown is closed


In a WinUI 3 desktop app, I have a list of objects, each with a LongName and an Abbreviation property (both strings). I'd like to use a ComboBox to select a specific item. When the ComboBox dropdown is closed, I'd like the Abbreviation of the SelectedItem to display in the ComboBox but when the dropdown opens, I'd like the list to use the LongNames.

For example, consider the FooBar class:

public partial class FooBar : ObservableObject
{
    public static readonly FooBar[] FooBars =
    {
        new("Foo1","Bar1"), new("Foo2","Bar2"), new("Foo3","Bar3")
    };

    public FooBar(string foo, string bar)
    {
        Foo = foo;
        Bar = bar;
    }

    [ObservableProperty] private string _foo;
    [ObservableProperty] private string _bar;
}

and the ComboBox:

    <Grid x:Name="ContentArea">
        <ComboBox x:Name="TheComboBox"
                  SelectedIndex="{x:Bind ViewModel.SelectedFooBar, Mode=TwoWay}"
                  ItemsSource="{x:Bind classes:FooBar.FooBars}"/>
    </Grid>

I'd like TheComboBox to show the Foo property for each FooBar when TheComboBox.IsDropDownOpen is true, and the Bar property when it's false.

dropdown open or dropdown closed.

I've tried setting ItemTemplate, DisplayMemberPath, ItemContainerStyle, ItemTemplateSelector, various tricks in code-behind, and editing the DefaultComboBoxItemStyle but none of these seem to work to change the property displayed dynamically. Making the change in code-behind seems to trigger SelectedItemChanged, probably because the Items list changes (but I'm not sure). I tried editing the DefaultComboBoxStyle (paricularly the VisualStates) but it's not obvious to me where individual items are displayed there.

Does anyone have any ideas for this or tips on how I might go about it, please?


Solution

  • Here is one solution mostly based on XAML and databinding with the x:Bind extension (I've used a UserControl to be able to put the converter resource somewhere because with WinUI3 you can't put it under the Window element):

    <UserControl
        x:Class="MyApp.UserControl1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:classes="using:MyApp.Models">
        <UserControl.Resources>
            <classes:VisibilityNegateConverter x:Key="vn" />
        </UserControl.Resources>
    
        <ComboBox x:Name="TheComboBox" ItemsSource="{x:Bind classes:FooBar.FooBars}">
            <ComboBox.ItemTemplate>
                <DataTemplate x:DataType="classes:FooBar">
                    <StackPanel>
                        <TextBlock Text="{x:Bind Foo}" Visibility="{x:Bind TheComboBox.IsDropDownOpen}" />
                        <TextBlock Text="{x:Bind Bar}" Visibility="{x:Bind TheComboBox.IsDropDownOpen, Converter={StaticResource vn}}" />
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </UserControl>
    

    And the converter for the "reverse" visibility conversion between Boolean and Visibility ("forward" conversion is now implicit in WinUI3 and UPW for some times)

    public class VisibilityNegateConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language) => (bool)value ? Visibility.Collapsed : Visibility.Visible;
        public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotSupportedException();
    }
    

    Note: I've tried to use function binding to avoid the need for a converter, something like this:

    <TextBlock Text="{x:Bind Bar}" Visibility="{x:Bind TheComboBox.IsDropDownOpen.Equals(x:False)}" />
    

    But compilation fails miserably (maybe a bug in XAML compiler?)