WinUI 3 cannot handle structs as ItemSource/SelectedItem for ComboBox
See original GitHub issueUsing 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:
- Created 3 years ago
- Comments:8 (4 by maintainers)
Top GitHub Comments
@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 fromItemsSource
and ran into no issues.I guess this can be closed.
I am out of the office and won’t be able to look into this until closer to the end of the month.