I have a Combobox in a view, bound to a viewmodel with an ObservableCollection of a custom class of mine. This custom class is simply a wrapper for an Enum which holds the value, and a string which is the enum description attribute. The combobox sets the DisplayMemberPath property to this name property, to display a more human readable description attribute value, rather than the enum itself
I am finding that when I set the ItemsSource of the combobox to a collection of these Enum wrapper classes, and then set the SelectedItem property to one of these items, the combobox is not updating in the UI when I start my application. If I change this to a List of strings, it seems to work.
Here is my combobox:
<ComboBox
DisplayMemberPath="Name"
IsEditable="False"
IsReadOnly="True"
ItemsSource="{Binding SelectableTags}"
SelectedItem="{Binding SelectedTag, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
My custom class the combobox is bound to a collection of:
public class EnumNamePair : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
private Enum enumValue;
public Enum EnumValue
{
get => enumValue;
set
{
SetProperty(ref enumValue, value);
Name = enumValue.GetEnumDescription();
}
}
public EnumNamePair(Enum enumValue)
{
EnumValue = enumValue;
}
}
Part of my viewmodel:
private ObservableCollection<EnumNamePair> selectableTags = new();
public ObservableCollection<EnumNamePair> SelectableTags
{
get => selectableTags;
set => SetProperty(ref selectableTags, value);
}
private EnumNamePair selectedTag;
public EnumNamePair SelectedTag
{
get => selectedTag;
set => SetProperty(ref selectedTag, value);
}
public TaggingRuleViewModel(string tag)
{
SelectableTags = new List<EnumNamePair>(
Enum.GetValues(typeof(AllowedTag)).Cast<AllowedTag>().Select(x => new EnumNamePair(x)));
SelectedTag = SelectableTags.First(x => x.EnumValue.ToString() == tag),
}
This is simplified but recreates the problem. I have tried various additional raisings of OnPropertyChanged for my bound properties, altering the Readonly/Editable property setters on the combobox, dumbing down my viewmodels/custom class (it used to have a single getter for the name property rather than set on setting the EnumValue etc). All that seems to work is changing the list of my custom class to a list of strings, and handling the conversion in the viewmodel. I know there are other ways of handling showing an enum description attribute in a combobox but at this point I simply want to know why this doesn't work.
CodePudding user response:
Converter implementation example:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core2022.SO.JosephWard
{
public static class EnumExtensions
{
private static readonly Dictionary<Type, Dictionary<string, string>> enums
= new Dictionary<Type, Dictionary<string, string>>();
public static string GetEnumDescription<T>(this T value)
where T : Enum
{
Type enumType = value.GetType();
if (!enums.TryGetValue(enumType, out Dictionary<string, string>? descriptions))
{
descriptions = enumType.GetFields()
.ToDictionary(
info => info.Name,
info => ((DescriptionAttribute?)info.GetCustomAttributes(typeof(DescriptionAttribute), false)?.FirstOrDefault())?.Description ?? info.Name);
enums.Add(enumType, descriptions);
}
string name = value.ToString();
if (descriptions.TryGetValue(name, out string? description))
{
return description;
}
else
{
throw new ArgumentException("UnexpectedValue", nameof(value));
}
}
}
}
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Core2022.SO.JosephWard
{
[ValueConversion(typeof(Enum), typeof(string))]
public class EnumToDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Enum @enum)
return @enum.GetEnumDescription();
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public static EnumToDescriptionConverter Instance { get; } = new EnumToDescriptionConverter();
}
}
using System;
using System.Windows.Markup;
namespace Core2022.SO.JosephWard
{
[MarkupExtensionReturnType(typeof(EnumToDescriptionConverter))]
public class EnumToDescriptionExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return EnumToDescriptionConverter.Instance;
}
}
}
using System.ComponentModel;
namespace Core2022.SO.JosephWard
{
public enum AllowedTag
{
[Description("Value not set")]
None,
[Description("First value")]
First,
[Description("Second value")]
Second
}
}
using System.Collections.ObjectModel;
namespace Core2022.SO.JosephWard
{
public class EnumsViewModel
{
public ObservableCollection<AllowedTag> Tags { get; } = new ObservableCollection<AllowedTag>()
{ AllowedTag.None, AllowedTag.Second, AllowedTag.First };
}
}
<Window x:Class="Core2022.SO.JosephWard.EnumsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Core2022.SO.JosephWard"
mc:Ignorable="d"
Title="EnumsWindow" Height="150" Width="300">
<Window.DataContext>
<local:EnumsViewModel/>
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding Tags}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:AllowedTag}">
<TextBlock Text="{Binding Converter={local:EnumToDescription}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
</Grid>
</Window>