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

276 views Asked by At

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?

2

There are 2 answers

3
Simon Mourier On BEST ANSWER

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

2
The Lemon On

You can use a bit of a hack by adding an event to any of the change events

private void myComboBox_SelectedValueChanged(object sender, EventArgs e)
{
    _comboBoxValue = myComboBox.Text;//store the selected value elsewhere
    BeginInvoke((MethodInvoker)delegate { myComboBox.Text = valueMapper[myComboBox.Text]; }); //idk how you're mapping the two strings
        //for all I know you might want ((Foo)myComboBox.SelectedItem).DisplayProperty 
}

This is a hack, there are other options though. Now that I think about it, a cleaner option would be to override the tostring of your objects dependent on whether or not the dropdown is open, back in a bit

OK plan B, not as clean code wise, but much cleaner UI wise. Recreate the underlying list after overriding the .ToString each time. This lets us use the dropdownStyle of dropDownList which is much cleaner

Note: the below code has the same class reflect itself in both string formats. You can do it with a dictionary lookup and populate with .keys and .values instead, either way works.

EventCode

private bool activateCombobox = false;
private void myComboBox_DropDown(object sender, EventArgs e)
{
    Foo.IsDroppedDown = true;
        myComboBox.Items.Clear();
        myComboBox.Items.AddRange(fooItems);
    Foo.IsDroppedDown = false;
    activateCombobox = true;
}

private void myComboBox_SelectedValueChanged(object sender, EventArgs e)
{
    if (activateCombobox)
    {
        activateCombobox = false;
        var selectedItem = myComboBox.SelectedItem;
        myComboBox.Items.Clear();
        myComboBox.Items.AddRange(fooItems);
        myComboBox.SelectedItem = selectedItem;
    }
        
}

and then our class code (change it to your classes ofc, this is just an example)

private Foo[] fooItems = new Foo[] { new Foo(1), new Foo(2), new Foo(3) };
private class Foo
{
    public int index = 0;
    public Foo() { }
    public Foo(int index) { this.index = index; }
    public string dropdownFoo { get { return $"Foo{index}"; } }
    public string displayFoo { get { return $"Bar{index}"; } }
    public override string ToString()
    {
        if (IsDroppedDown)
            return dropdownFoo;
        return displayFoo;
    }
    public static bool IsDroppedDown = false;
}