Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding a TypeConverter directly to an Enumeration

I have the following scenario: I have an enum, and want to bind it show it in a DataGridView (databound) on a DataGridViewTextBoxColumn.

Here is my enum:

//[TypeConverter(typeof(EnumStringConverter))]
   public enum YesNoNA
   {
      [EnumDescription("Yes")]
      Yes,
      [EnumDescription("No")]
      No,
      [EnumDescription("N/A")]
      NA
   }

And here is a simple property that uses it:

  [TypeConverter(typeof(EnumStringConverter))]
  public YesNoNA HighLimitWithinBounds { get; protected set; }

In the situation I have above, the typeconverter works just fine. It does the conversion for me.

However, this is more complex than my ideal solution. If I put the typeconverter on the Enum itself (uncomment the code above), and comment out the typeconverter on the property, the typeconverter is no longer called!

I've done this convention on other classes, and it works just fine.

Why does putting a typeconverter directly on an enum not work?

For reference, here's my typeconverter:

  public class EnumStringConverter : TypeConverter
   {
      public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, Object value, Type destinationType)
      {
         if (value != null && destinationType == typeof(string))
         {
            return "Edited to protect the innocent!";
         }
         return TypeDescriptor.GetConverter(typeof(Enum)).ConvertTo(context, culture, value, destinationType);
      }
      public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
      {
         if (destinationType == typeof(string))
         {
            return true;
         }
         return base.CanConvertTo(context, destinationType);
      }
   };
like image 299
greggorob64 Avatar asked Feb 22 '13 16:02

greggorob64


1 Answers

I'm not sure what you mean by "this is more complex than my ideal solution". I have a way of doing this that is different than yours but it may not be less complex. I would say my way involves more overhead up front but pays off the more and more you use it in your application. It prepares your application to be localizable and means you don't have to add attributes to each enum value.

1) Make a Resource Manager Cache

This part is optional; however if you use multiple resource files many times like I do this could increase performance by reducing how much reflection is done.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Resources;

namespace AppResourceLib.Public.Reflection
{
  internal static class ResourceManagerCache
  {
    private static Dictionary<Type, ResourceManager> _resourceManagerMap =
      new Dictionary<Type, ResourceManager>();

    public static ResourceManager GetResourceManager(Type resourceType)
    {
      ResourceManager resourceManager = null;

      // Make sure the type is valid.
      if (null != resourceType)
      {
        // Try getting the cached resource manager.
        if (!ResourceManagerCache._resourceManagerMap.TryGetValue(resourceType, out resourceManager))
        {
          // If it is not in the cache create it.
          resourceManager = resourceType.InvokeMember(
            "ResourceManager",
            (BindingFlags.GetProperty | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic),
            null,                                                   
            null,                                                   
            null) as ResourceManager;

          // If it was created, add the resource manager to the cache.
          if (null != resourceManager)
          {
            ResourceManagerCache._resourceManagerMap.Add(resourceType, resourceManager);
          }
        }
      }

      return resourceManager;
    }
  }
}

2) Create a Localized Description Attribute

This is the attribute that you will apply to the Enum type. The cool thing about this is that you don't have to add an attribute to each enum, just once above the enum type declaration.

using System;
using System.ComponentModel;

namespace AppResourceLib.Public.Reflection
{
  /// <summary>
  /// A resource type attribute that can be applied to enumerations.
  /// </summary>
  [AttributeUsage(AttributeTargets.Enum)]
  public sealed class LocalizedDescriptionAttribute : Attribute
  {
    /// <summary>
    /// The type of resource associated with the enum type.
    /// </summary>
    private Type _resourceType;

    public LocalizedDescriptionAttribute(Type resourceType)
    {
      this._resourceType = resourceType;
    }

    /// <summary>
    /// The type of resource associated with the enum type.
    /// </summary>
    public Type ResourceType
    {
      get
      {
        return this._resourceType;
      }
    }
  }
}

3) Create Localized Description Converter

This converts the enum value into the string you will give it in the strings resource (.resx) file.

using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Windows.Data;

namespace AppResourceLib.Public.Reflection
{
  [ValueConversion(typeof(Object), typeof(String))]
  public class LocalizedDescriptionConverter : IValueConverter
  {
    public Object Convert(Object value, Type targetType, Object param, CultureInfo cultureInfo)
    {
      String description = null;

      if (null != value)
      {
        // If everything fails then at least return the value.ToString().
        description = value.ToString();

        // Get the LocalizedDescriptionAttribute of the object.
        LocalizedDescriptionAttribute attribute =
          value.GetType().GetCustomAttribute(typeof(LocalizedDescriptionAttribute))
          as LocalizedDescriptionAttribute;

        // Make sure we found a LocalizedDescriptionAttribute.
        if (null != attribute)
        {          
          ResourceManager resourceManager = 
            ResourceManagerCache.GetResourceManager(attribute.ResourceType);

          if (null != resourceManager)
          {
            // Use the ResourceManager to get the description you gave the object value.
            // Here we just use the object value.ToString() (the name of the object) to get
            // the string in the .resx file. The only constraint here is that you have to
            // name your object description strings in the .resx file the same as your objects.
            // The benefit is that you only have to declare the LocalizedDescriptionAttribute
            // above the object type, not an attribute over every object.
            // And this way is localizable.
            description = resourceManager.GetString(value.ToString(), cultureInfo);

            String formatString = (param as String);

            // If a format string was passed in as a parameter,
            // make a string out of that.
            if (!String.IsNullOrEmpty(formatString))
            {
              formatString = formatString.Replace("\\t", "\t");
              formatString = formatString.Replace("\\n", "\n");
              formatString = formatString.Replace("\\r", "\r");

              description = String.Format(formatString, value.ToString(), description);              
            }          
          }
        }
      }

      return description;      
    }

    public Object ConvertBack(Object value, Type targetType, Object param, CultureInfo cultureInfo)
    {
      throw new NotImplementedException();

      return null;
        }
  }
}

4) Create a Resource (.resx) String File

Now you want to create a resource file that will contain the descriptions you want for your Enums key value style. What I mean by this is that in the "Name" column of the string resources you will put the exact name of the individual enums and in the "Value" column you put the string you want to get when converting that enum.
For example, say you had the following Enums.

public enum MyColors
{
  Black,
  Blue,
  White
}

Then your string resource file would look like this...

Name | Value

Black | A Dark Color
Blue | A cool color
White | A Bright color

5) Create Enums with Attribute

Now we finally make the Enum declaration with the LocalizedDescription. The parameter that you pass into the LocalizedDescription attribute is the type of your string resource file. Now when the converter is used it will see the attribute of the enum type, get the resource file, look for the key string that matches the string value of the particular enum value, and return the value from the resource file as the converted string.

using AppResourceLib.Public;
using AppResourceLib.Public.Reflection;

namespace MyEnums
{
  [LocalizedDescription(typeof(MyColorStrings))]
  public enum MyColors
  {
    Black,
    Blue,
    White
  }
}

The major drawback to this approach is that it will only work if the "Name" keys in your resource file match the names of your enum values. This is the only way to reference string values in a resource file without giving each enum a description attribute. So how do you use this for displaying values? Here is an example...

In your xaml code you will want to make a data provider to get the values of the enums to your UI element (I'm using a ComboBox here...). Then you will want to make the converter available and template your UI element to use the enum converter. So here it goes...

      <!-- Enum Colors -->
  <ObjectDataProvider x:Key="MyColorEnums"
                      MethodName="GetValues"
                      ObjectType="{x:Type sys:Enum}">
    <ObjectDataProvider.MethodParameters>
      <x:Type TypeName="MyColors"/>
    </ObjectDataProvider.MethodParameters>
  </ObjectDataProvider>


  <!-- Enum Type Converter -->
  <LocalizedDescriptionConverter x:Key="EnumConverter"/>


  <!-- Dropdown Expand ComboBox Template -->
  <DataTemplate x:Key="MyColorsComboBoxTemplate">
    <Label Content="{Binding Path=., Mode=OneWay,
      Converter={StaticResource EnumConverter}}"
           Height="Auto" Margin="0" VerticalAlignment="Center"/>
  </DataTemplate>

  <!-- And finally the ComboBox that will display all of your enum values
    but will use the strings from the resource file instead of enum.ToString() -->
  <ComboBox Width="80" HorizontalAlignment="Left"
  ItemTemplate="{StaticResource MyColorsComboBoxTemplate}"
  ItemsSource="{Binding Source={StaticResource MyColorEnums}}">

Wow, sorry this is so long. I'm not sure if this is too complex for you but it is another option. Hope it helps!

like image 105
akagixxer Avatar answered Oct 23 '22 07:10

akagixxer