TypeConverter attribute on enum type breaks dependency properties of that type

0

I have defined an enum type detailing various color palettes for colorizing grayscale images, for which I am using Description attributes and a TypeConverter in order to use the description strings of the enum values for comboboxes, list boxes etc. that I am binding to this type. The enum looks like this:

    // available color palettes for colorizing 8 bit grayscale images
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    public enum ColorPalette
    {
        [Description("Alarm Blue")]
        AlarmBlue,
        [Description("Alarm Blue High")]
        AlarmBlueHi,
        [Description("Alarm Green")]
        AlarmGreen,
        [Description("Alarm Red")]
        AlarmRed,
        [Description("Fire")]
        Fire,
        [Description("Gray BW")]
        GrayBW,
        [Description("Ice 32")]
        Ice32,
        [Description("Iron")]
        Iron,
        [Description("Iron High")]
        IronHi,
        [Description("Medical 10")]
        Medical10,
        [Description("Rainbow")]
        Rainbow,
        [Description("Rainbow High")]
        RainbowHi,
        [Description("Temperature 256")]
        Temperature256,
        [Description("Nano Green")]
        NanoGreen
    };

The EnumDescriptionTypeConverter looks like this:

public class EnumDescriptionTypeConverter : EnumConverter
    {
        public EnumDescriptionTypeConverter(Type type) : base(type) { }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                if (value != null)
                {
                    FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
                    if (fieldInfo != null)
                    {
                        var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
                        return ((attributes.Length > 0) && (!string.IsNullOrEmpty(attributes[0].Description))) ? attributes[0].Description : value.ToString();
                    }
                }
                return string.Empty;
            }

            return base.ConvertTo(context, culture, value, destinationType);
        }
    }

Using this, I can bind the enum type to say, a combo box's ItemsSource property and have the description strings be used automatically as the combo box elements, using another custom markup extension class the code of which I don't believe is relevant here. The problem is, that if I try to create a public dependency property on a custom control based on this enum type, it won't work. Here's an example custom control:

    public class TestControl : Control
    {
        public ColorPalette Test1
        {
            get => (ColorPalette)GetValue(Test1Property);
            set => SetValue(Test1Property, value);
        }

        public static readonly DependencyProperty Test1Property = DependencyProperty.Register(nameof(Test1), typeof(ColorPalette),
            typeof(TestControl), new PropertyMetadata
            {
                DefaultValue = ColorPalette.Rainbow
            });
    }

This code compiles without error and I can put the TestControl into a window, until I try to set the value of the test property in the XAML - then I don't get the usual IntelliSense containing the enum values and when I try to manually set a value anyway, I get an Access Violation exception as soon as I run the application, right at the InitializeComponent() method of the MainWindow:

" Exception thrown at 0x00007FF84723A799 (KernelBase.dll) in .exe: 0xC0000005: Access violation reading location 0x0000000000000008. occurred "

This does not happen when I remove the TypeConverter attribute from the enum definition, but then of course the Description string binding doesn't work any more.

I don't know enough about WPF to realize what exactly the problem is. Is there a way to avoid this, and still use the TypeConverter for binding using the Description string attributes?

c#
wpf
enums
dependency-properties
typeconverter
asked on Stack Overflow Jul 8, 2020 by flibbo • edited Jul 8, 2020 by flibbo

2 Answers

0

So I found a workaround by using a different kind of MarkupExtension as binding source for enum types:

    public class EnumDescriptionBindingSourceExtension : MarkupExtension
    {
        public Type EnumType
        {
            get => enumType;
            set
            {
                if (enumType != value)
                {
                    if (value != null)
                    {
                        Type type = Nullable.GetUnderlyingType(value) ?? value;
                        if (!type.IsEnum)
                            throw new ArgumentException("Type must be an enum type");
                    }
                    enumType = value;
                }
            }
        }

        private Type enumType;

        public EnumDescriptionBindingSourceExtension() { }

        public EnumDescriptionBindingSourceExtension(Type enumType) => this.enumType = enumType;

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (enumType == null)
                throw new InvalidOperationException("The enum type must be specified");

            Type actualEnumType = Nullable.GetUnderlyingType(enumType) ?? enumType;
            Array enumValues = Enum.GetValues(actualEnumType);

            if (actualEnumType == enumType)
            {
                List<string> descriptions = new List<string>(enumValues.Length);
                foreach (object value in enumValues)
                {
                    FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
                    if (fieldInfo != null)
                    {
                        DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
                        descriptions.Add(((attributes.Length > 0) && !string.IsNullOrEmpty(attributes[0].Description)) ? attributes[0].Description : value.ToString());
                    }
                }
                return descriptions;
            }
            else
            {
                Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
                enumValues.CopyTo(tempArray, 1);
                return tempArray;
            }
        }
    }

This extension returns an array of the description strings (if any, otherwise just value.ToString()) of the enum values. When using this in XAML bindings, I can have my combo boxes be filled with the enum value descriptions directly, while previously I would use a markup extension that would just return an array of the enum values themselves and have the conversion to their description strings be done by the TypeConverter.

When using this new markup extension, I have to use a converter that can determine an original enum value from its description string:

public class EnumDescriptionConverter : IValueConverter
    {
        object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Enum enumObject)
            {
                FieldInfo fieldInfo = enumObject.GetType().GetField(enumObject.ToString());
                object[] attributes = fieldInfo.GetCustomAttributes(false);

                if (attributes.Length == 0)
                    return enumObject.ToString();
                else
                {
                    DescriptionAttribute attribute = attributes[0] as DescriptionAttribute;
                    return attribute.Description;
                }
            }
            else
                throw new ArgumentException($"Conversion is only defined for enum types");
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is string valString)
            {
                Array enumValues = targetType.GetEnumValues();
                FieldInfo fieldInfo;
                DescriptionAttribute[] attributes;
                string target;
                foreach (object enumValue in enumValues)
                {
                    fieldInfo = enumValue.GetType().GetField(enumValue.ToString());
                    if(fieldInfo != null)
                    {
                        attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
                        target = ((attributes.Length == 1) && !string.IsNullOrEmpty(attributes[0].Description)) ? attributes[0].Description : enumValue.ToString();
                        if (valString == target)
                            return enumValue;
                    }
                }
                throw new ArgumentException($"Back-conversion failed - no enum value corresponding to string");
            }
            else
                throw new ArgumentException($"Back-conversion is only defined for string type");
        }
    }

With both of these I can do for example the following in XAML:

<ns:EnumDescriptionConverter x:Key="enumDescriptionConverter"/>
(...)
<ComboBox ItemsSource="{Binding Source={ns:EnumDescriptionBindingSource {x:Type ns:MyEnumType}}, Mode=OneTime}" SelectedItem="{Binding MyEnumTypeProperty, Converter={StaticResource enumDescriptionConverter}}"/>

Which will automatically fill the combo box with the enum values, represented by their description strings, and bind the selected item to a property of that type. This then works without setting the TypeConverter attribute on the enum definition and thus my original problem doesn't occur.

I'm still none the wiser why it happened in the first place or if there's a better way to solve it but hey, it works.

answered on Stack Overflow Jul 9, 2020 by flibbo
-1

do you must use dependency property?

For this cases I used ViewModel with Enum object and IValueConverter in XAML code

example of ViewModel for Enum type

public abstract class VM_PropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChange(string propertyName)
    {
        var handler = PropertyChanged;
        if (PropertyChanged != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class VM_EnumItem<T> : VM_PropertyChanged
{

    public T Enum { get; }

    public bool IsEnabled
    {
        get { return isEnabled; }
        set { isEnabled = value; OnPropertyChange(nameof(IsEnabled)); }
    }
    private bool isEnabled;

    public VM_EnumItem(T Enum, bool IsEnabled)
    {
        this.Enum = Enum;
        this.IsEnabled = IsEnabled;
    }

    public override int GetHashCode()
    {
        return Enum.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj != null && obj is VM_EnumItem<T> item)
            return System.Enum.Equals(item.Enum, this.Enum);
        return false;
    }

    public override string ToString()
    {
        return string.Format("{0} | {1}", Enum, IsEnabled);
    }
 
}

example of ViewModel for WPF Control

class ViewModel : VM_PropertyChanged
{

    public enum ColorPalette
    {
        [Description("Alarm Blue")]
        AlarmBlue,
        [Description("Alarm Blue High")]
        AlarmBlueHi
    }
    // all options
    public ObservableCollection<VM_EnumItem<ColorPalette>> EnumItems { get; } = new ObservableCollection<VM_EnumItem<ColorPalette>>()
    {
           new VM_EnumItem<ColorPalette>(ColorPalette.AlarmBlue, true),
           new VM_EnumItem<ColorPalette>(ColorPalette.AlarmBlueHi, true)
     };

    public VM_EnumItem<ColorPalette> SelectedEnumItem
    {
        get { return EnumItems.Where(s => s.Enum == SelectedEnum).FirstOrDefault(); }
        set { SelectedEnum = value.Enum; OnPropertyChange(nameof(SelectedEnumItem)); }
    }

    private ColorPalette SelectedEnum; // your selected Enum
}

example of Converter

public class VM_Converter_EnumDescription : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Type type = value.GetType();
        if (!type.IsEnum)
            return value;

        string name = Enum.GetName(type, value);
        FieldInfo fi = type.GetField(name);
        DescriptionAttribute descriptionAttrib = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));

        return descriptionAttrib == null ? value.ToString() : descriptionAttrib.Description;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

example of WPF Control

<Window.Resources>
    <ResourceDictionary >
        <local:VM_Converter_EnumDescription x:Key="Converter_EnumDescription"/>
    </ResourceDictionary>
</Window.Resources>

////////////

    <ComboBox 
        ItemsSource="{Binding Path=EnumItems, Mode=OneWay}"
        SelectedItem="{Binding Path=SelectedEnumItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <ContentPresenter Content="{Binding Path=Enum, Converter={StaticResource Converter_EnumDescription}}"/>
            </DataTemplate>
        </ComboBox.ItemTemplate>
        <ComboBox.ItemContainerStyle>
            <Style TargetType="{x:Type ComboBoxItem}">
                <Setter Property="IsEnabled" Value="{Binding Path=IsEnabled}"/>
            </Style>
        </ComboBox.ItemContainerStyle>
    </ComboBox>
answered on Stack Overflow Jul 8, 2020 by krejcar

User contributions licensed under CC BY-SA 3.0