Search code examples
c#wpfcombobox

WPF Combobox in view not updating when bound property in viewmodel updated


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.


Solution

  • 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>
    

    enter image description here