1

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?

aturnbul
  • 347
  • 2
  • 12

2 Answers2

1

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

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Thank you @Simon, this approach makes sense to me. When I tried something like this, the `Visibility="{x:Bind TheComboBox.IsDropDownOpen}"` bindings in the `TextBlock`s failed because _The property 'TheComboBox' was not found in type 'FooBar'_. How do you reference the parent control in a `DataTemplate`? – aturnbul Jan 04 '23 at 17:45
  • No, it builds, runs and works fine. It's just the visual editor that complains. – Simon Mourier Jan 04 '23 at 17:56
  • Very odd. But it works! I has a resource lookup in the test code that I forgot to remove. It's strange, though, that the function binding doesn't work. That exact code works in other places in my XAML. Thank you again - this is very helpful. – aturnbul Jan 04 '23 at 18:21
0

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;
}
The Lemon
  • 1,211
  • 15
  • 26
  • Thank you @TheLemon. I've tried your suggestions in quite a few different ways but I always seem to generate multiple `SelectedIndexChanged` events, or it just doesn't work. I actually started out by rewriting the `Items` list each time the dropdown opened or closed which is how I noticed the multiple change notifications. Ultimately, @Simon's answer worked for me. – aturnbul Jan 04 '23 at 18:27
  • @aturnbul that's what the activateCombobox flag was for haha, ensures only one trigger per time dropped down – The Lemon Jan 05 '23 at 21:41