枚举类型的 TypeConverter 属性打破了该类型的依赖属性
TypeConverter attribute on enum type breaks dependency properties of that type
我定义了一个枚举类型,详细说明了各种调色板,用于为灰度图像着色,为此我使用了 Description 属性和一个 TypeConverter,以便将枚举值的描述字符串用于组合框、列表框等。我绑定到这种类型。枚举如下所示:
// 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
};
EnumDescriptionTypeConverter 如下所示:
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);
}
}
使用这个,我可以绑定枚举类型说,组合框的 ItemsSource 属性 并让描述字符串自动用作组合框元素,使用另一个自定义标记扩展 class我认为这里的代码不相关。
问题是,如果我尝试在基于此枚举类型的自定义控件上创建 public 依赖项 属性,它将无法工作。下面是一个自定义控件示例:
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
});
}
这段代码编译没有错误,我可以将 TestControl 放入 window,直到我尝试在 XAML 中设置测试值 属性 - 然后我不'得到包含枚举值的通常的 IntelliSense,当我尝试手动设置一个值时,我在 运行 应用程序时立即收到访问冲突异常,就在 MainWindow 的 InitializeComponent() 方法中:
" 在 .exe 中的 0x00007FF84723A799 (KernelBase.dll) 抛出异常:0xC0000005:读取位置 0x0000000000000008 发生访问冲突。"
当我从枚举定义中删除 TypeConverter 属性时,不会发生这种情况,但是 Description 字符串绑定当然不再起作用。
我对 WPF 的了解还不够,无法意识到问题到底出在哪里。有没有办法避免这种情况,并且仍然使用 TypeConverter 来使用 Description 字符串属性进行绑定?
是否必须使用依赖项属性?
对于这种情况,我在 XAML 代码
中使用了带有 Enum 对象的 ViewModel 和 IValueConverter
枚举类型的 ViewModel 示例
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);
}
}
WPF 控件的 ViewModel 示例
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
}
转换器示例
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();
}
}
WPF 控件示例
<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>
所以我找到了一个解决方法,使用不同类型的 MarkupExtension 作为枚举类型的绑定源:
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;
}
}
}
此扩展 return 是枚举值的描述字符串数组(如果有,否则只是 value.ToString())。在 XAML 绑定中使用它时,我可以让我的组合框直接用枚举值描述填充,而以前我会使用一个标记扩展,它只是 return 枚举值本身的数组和由 TypeConverter 完成对其描述字符串的转换。
使用这个新的标记扩展时,我必须使用一个转换器,它可以从其描述字符串中确定原始枚举值:
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");
}
}
有了这两个,我可以在 XAML 中执行以下操作:
<ns:EnumDescriptionConverter x:Key="enumDescriptionConverter"/>
(...)
<ComboBox ItemsSource="{Binding Source={ns:EnumDescriptionBindingSource {x:Type ns:MyEnumType}}, Mode=OneTime}" SelectedItem="{Binding MyEnumTypeProperty, Converter={StaticResource enumDescriptionConverter}}"/>
这将自动用枚举值填充组合框,由它们的描述字符串表示,并将所选项目绑定到该类型的 属性。然后,无需在枚举定义上设置 TypeConverter 属性即可工作,因此不会出现我原来的问题。
我仍然none更清楚为什么它首先会发生,或者是否有更好的方法来解决它但是嘿,它有效。
我定义了一个枚举类型,详细说明了各种调色板,用于为灰度图像着色,为此我使用了 Description 属性和一个 TypeConverter,以便将枚举值的描述字符串用于组合框、列表框等。我绑定到这种类型。枚举如下所示:
// 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
};
EnumDescriptionTypeConverter 如下所示:
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);
}
}
使用这个,我可以绑定枚举类型说,组合框的 ItemsSource 属性 并让描述字符串自动用作组合框元素,使用另一个自定义标记扩展 class我认为这里的代码不相关。 问题是,如果我尝试在基于此枚举类型的自定义控件上创建 public 依赖项 属性,它将无法工作。下面是一个自定义控件示例:
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
});
}
这段代码编译没有错误,我可以将 TestControl 放入 window,直到我尝试在 XAML 中设置测试值 属性 - 然后我不'得到包含枚举值的通常的 IntelliSense,当我尝试手动设置一个值时,我在 运行 应用程序时立即收到访问冲突异常,就在 MainWindow 的 InitializeComponent() 方法中:
" 在 .exe 中的 0x00007FF84723A799 (KernelBase.dll) 抛出异常:0xC0000005:读取位置 0x0000000000000008 发生访问冲突。"
当我从枚举定义中删除 TypeConverter 属性时,不会发生这种情况,但是 Description 字符串绑定当然不再起作用。
我对 WPF 的了解还不够,无法意识到问题到底出在哪里。有没有办法避免这种情况,并且仍然使用 TypeConverter 来使用 Description 字符串属性进行绑定?
是否必须使用依赖项属性?
对于这种情况,我在 XAML 代码
中使用了带有 Enum 对象的 ViewModel 和 IValueConverter枚举类型的 ViewModel 示例
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);
}
}
WPF 控件的 ViewModel 示例
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
}
转换器示例
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();
}
}
WPF 控件示例
<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>
所以我找到了一个解决方法,使用不同类型的 MarkupExtension 作为枚举类型的绑定源:
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;
}
}
}
此扩展 return 是枚举值的描述字符串数组(如果有,否则只是 value.ToString())。在 XAML 绑定中使用它时,我可以让我的组合框直接用枚举值描述填充,而以前我会使用一个标记扩展,它只是 return 枚举值本身的数组和由 TypeConverter 完成对其描述字符串的转换。
使用这个新的标记扩展时,我必须使用一个转换器,它可以从其描述字符串中确定原始枚举值:
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");
}
}
有了这两个,我可以在 XAML 中执行以下操作:
<ns:EnumDescriptionConverter x:Key="enumDescriptionConverter"/>
(...)
<ComboBox ItemsSource="{Binding Source={ns:EnumDescriptionBindingSource {x:Type ns:MyEnumType}}, Mode=OneTime}" SelectedItem="{Binding MyEnumTypeProperty, Converter={StaticResource enumDescriptionConverter}}"/>
这将自动用枚举值填充组合框,由它们的描述字符串表示,并将所选项目绑定到该类型的 属性。然后,无需在枚举定义上设置 TypeConverter 属性即可工作,因此不会出现我原来的问题。
我仍然none更清楚为什么它首先会发生,或者是否有更好的方法来解决它但是嘿,它有效。