【问题标题】:TypeConverter attribute on enum type breaks dependency properties of that type枚举类型的 TypeConverter 属性破坏了该类型的依赖属性
【发布时间】:2020-10-28 17:31:51
【问题描述】:

我已经定义了一个枚举类型,详细说明了用于为灰度图像着色的各种调色板,为此我使用了描述属性和一个 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 属性,并使用另一个自定义标记扩展类自动将描述字符串用作组合框元素,我不相信它的代码是在这里相关。 问题是,如果我尝试在基于此枚举类型的自定义控件上创建公共依赖属性,它将不起作用。这是一个示例自定义控件:

    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 放入一个窗口,直到我尝试在 XAML 中设置测试属性的值 - 然后我没有得到包含枚举值的通常 IntelliSense,当我尝试无论如何手动设置一个值,当我运行应用程序时,我得到一个访问冲突异常,就在 MainWindow 的 InitializeComponent() 方法:

" 在 .exe 中的 0x00007FF84723A799 (KernelBase.dll) 处引发异常:0xC0000005:访问冲突读取位置 0x0000000000000008。发生“

当我从枚举定义中删除 TypeConverter 属性时不会发生这种情况,但是当然描述字符串绑定不再起作用。

我对 WPF 的了解还不够,无法意识到问题到底出在哪里。有没有办法避免这种情况,并且仍然使用 TypeConverter 来使用 Description 字符串属性进行绑定?

【问题讨论】:

    标签: c# wpf enums dependency-properties typeconverter


    【解决方案1】:

    所以我找到了一种解决方法,即使用不同类型的 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;
                }
            }
        }
    

    此扩展返回枚举值的描述​​字符串数组(如果有,否则只是 value.ToString())。在 XAML 绑定中使用它时,我可以让我的组合框直接填充枚举值描述,而以前我会使用标记扩展,它只会返回枚举值本身的数组并转换为它们的描述字符串由 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 属性即可工作,因此不会发生我原来的问题。

    我仍然不知道为什么它首先会发生,或者是否有更好的方法来解决它,但是嘿,它有效。

    【讨论】:

      【解决方案2】:

      你必须使用依赖属性吗?

      对于这种情况,我在 XAML 代码中使用了带有 Enum 对象和 IValueConverter 的 ViewModel

      枚举类型的 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>
      

      【讨论】:

      • 对不起,我要么不明白,要么这不能解决我的问题。我不想在这里设置任何 MVVM 模式,我只想创建一个具有特定类型依赖属性的自定义控件。此控件在 XAML 中可供任何想要使用它的 Windows 使用,因此需要使用依赖项属性。理想情况下,我希望此属性与枚举类型一起使用,该类型也使用在其 TypeConverter 属性中设置的 EnumDescriptionTypeConverter。
      猜你喜欢
      • 2019-03-08
      • 2020-04-07
      • 1970-01-01
      • 1970-01-01
      • 2017-10-03
      • 2017-03-23
      • 1970-01-01
      • 1970-01-01
      • 2021-04-02
      相关资源
      最近更新 更多