question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

WinUI 3 cannot handle structs as ItemSource/SelectedItem for ComboBox

See original GitHub issue

Using a collection of structs as the ItemSource for a ComboBox and then setting the SelectedItem to a value equivalent to one of the items in the ItemSource throws an exception:

Exception thrown at 0x00007FFE12E0A799 (KernelBase.dll) in PrayerGuardian.exe: WinRT originate error - 0x80070057 : ‘Value does not fall within the expected range.’.

This is with the struct correctly implementing IEquatable<T> and the .Equals(other) method being called and returning true for the matching pair. The same code used to work under UWP.

Steps to reproduce the bug

Start with a struct that will be used as the ItemSource/SelectedItem underlying type:

    public struct NameAndValue<T> : IEquatable<NameAndValue<T>> where T : Enum
    {
        public string Name { get; private set; }
        public T Value { get; private set; }

        public NameAndValue(string name, T value)
        {
            Name = name;
            Value = value;
        }

        public override string ToString()
        {
            return Name;
        }

        public T ToValue()
        {
            return Value;
        }

        public override bool Equals(object obj) => obj is NameAndValue<T> other && Equals(other);
        public override int GetHashCode() => Name.GetHashCode(StringComparison.OrdinalIgnoreCase) ^ Value.GetHashCode();
        public static bool operator ==(NameAndValue<T> left, NameAndValue<T> right) => left.Equals(right);
        public static bool operator !=(NameAndValue<T> left, NameAndValue<T> right) => !(left == right);
        public bool Equals(NameAndValue<T> other) => Name == other.Name && Value.Equals(other.Value);


        // It's there but the Roslyn analyzer is dumb and can't realize this is a generic
#pragma warning disable CA2225 // Operator overloads have named alternates
        public static implicit operator T(NameAndValue<T> nameAndValue)
#pragma warning restore CA2225 // Operator overloads have named alternates
        {
            return nameAndValue.Value;
        }
    }

I use a converter to go from an enum to a NameAndValue, but you could presumably also create instances by hand:

    public class NameAndValueConverter<T> : IValueConverter
        where T: Enum
    {
        private string[] _names;

        public IList<NameAndValue<T>> Values { get; }

        public NameAndValueConverter()
        {
            var values = (T[])Enum.GetValues(typeof(T));
            var names = Enum.GetNames(typeof(T));
            Values = new NameAndValue<T>[values.Length];
            _names = new string[values.Length];

            for (int i = 0; i < values.Length; ++i)
            {
                var name = names[i];
                var value = values[i];

                var attributes = typeof(T).GetField(name).GetCustomAttributes(typeof(DisplayNameAttribute), false);
                if (attributes.Length > 0 && attributes[0] is DisplayNameAttribute dna && !string.IsNullOrWhiteSpace(dna.DisplayName))
                {
                    name = dna.DisplayName;
                }
                else
                {
                    attributes = typeof(T).GetField(name).GetCustomAttributes(typeof(DisplayAttribute), false);
                    if (attributes.Length > 0 && attributes[0] is DisplayAttribute da && !string.IsNullOrWhiteSpace(da.Name))
                    {
                        name = da.Name;
                    }
                }
                _names[i] = name;
                Values[i] = new NameAndValue<T>(name, value);
            }
        }

        public T NamedToEnum(NameAndValue<T> nameAndValue)
        {
            return nameAndValue.Value;
        }

        public NameAndValue<T> EnumToNamed(T value)
        {
            int val = (int)(object)value;
            if (val >= _names.Length)
            {
                return new NameAndValue<T>($"Unknown value {val}", value);
            }
            return new NameAndValue<T>(_names[val], value);
        }

        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is NameAndValue<T> nameAndValue)
            {
                return NamedToEnum(nameAndValue);
            }
            if (value is T @enum)
            {
                return EnumToNamed(@enum);
            }
            else
            {
                throw new Exception("Unsupported conversion!");
            }
        }

        // string to enum
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            return Convert(value, targetType, parameter, language);
        }
    }

(The purpose is to use an enum as the ItemSource and have it reflect the Display or DisplayName attribute in the stringified context)

Then create a two-way binding for the SelectedItem property:

<ComboBox Width="250" Margin="0,4,0,4" ItemsSource="{x:Bind CalculationMethods, Mode=OneTime}" SelectedItem="{x:Bind SelectedMethod, Mode=TwoWay, Converter={StaticResource CalculationMethodConverter}}" SelectionChanged="CalculationMethod_SelectionChanged" />

with CalculationMethods and SelectedMethod being as follows:

public IList<NameAndValue<CalculationMethod>> CalculationMethods { get; } = new NameAndValueConverter<CalculationMethod>().Values;

private CalculationMethod _selectedMethod = CalculationMethod.ISNA;
public CalculationMethod SelectedMethod
{
    get => _selectedMethod;
    set
    {
         _selectedMethod = value;
         RaisePropertyChanged(nameof(SelectedMethod));
    }
}

Then assign the SelectedMethod property to a new instance of SelectedMethod, optionally placing a breakpoint in the NameAndValue<T>.IEquatable impl to verify that it is both called and that it returns true for the matching pairs.

Expected behavior It should be possible to bind the ComboBoxItem nee NameAndValue<T> to the assigned value

Screenshots

Version Info WinUI 3 Preview 1

NuGet package version: [Microsoft.WinUI 3.0.0-preview1.200515.3]

Windows 10 version Saw the problem?
Insider Build (xxxxx)
November 2019 Update (18363) Yes
May 2019 Update (18362)
October 2018 Update (17763)
April 2018 Update (17134)
Fall Creators Update (16299)
Creators Update (15063)
Device form factor Saw the problem?
Desktop Yes
Mobile
Xbox
Surface Hub
IoT

Additional context

Simple changing struct to class makes everything suddenly work again.

Ashamed at how much of my time this wasted, as the error message had me tracking down all my grid row and column indices.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
mqudsicommented, Oct 2, 2021

@akilarajesh1313 thanks for waiting.

I was able to test it (code: WinUI_2539.zip) and couldn’t get this to repro with 0.8.2. I also added some more tests to specifically verify that it supports assigning the SelectedItem to a new (but equal) instance of a struct from ItemsSource and ran into no issues.

I guess this can be closed.

1reaction
mqudsicommented, Sep 10, 2021

I am out of the office and won’t be able to look into this until closer to the end of the month.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Default selected index for combo-box crashes my UWP App
With the SelectedIndex set to 0, the ComboBox is trying to access its first child to select it. When the XAML is loading,...
Read more >
WinUI 3 UserControl with ComboBox ...
Suppose we have public enum MyEnum {None, First, Second} and in MainWindow.xaml.cs, we have private IList<MyEnum> _myEnums = Enum.
Read more >
Question about DisplayMemberPath with a ComboBox in ...
Question about DisplayMemberPath with a ComboBox in WinUI 3. I am trying to build a list of ComboBoxItem dynamically using a list of...
Read more >
WinUI 3 | XAML Brewer, by Diederik Krols
It provides touch and mouse event handling, making it suitable for both desktop and mobile applications. Users can also select map features, overlay...
Read more >
How to use Windows Community Toolkit
Task 1 - Add Windows Community Toolkit to Uno Projects. Install Nuget package for targeted control datagrid-nuget. Note. For UWP and WinUI 3...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found