Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#5.0

A Dictionary for Enum Descriptions

4.96/5 (8 votes)
21 Apr 2014CPOL3 min read 31.6K   223  
A dictionary class for storing string descriptions provided by the Description attribute on enum fields.

Introduction

Here I introduce a simple, dictionary based class, the EnumDescripionDictionary<TEnum>, for storing descriptive text for members of Enum types. This article allows the use of the Description attribute to provide this text, but a future article will grow the class a little to allow for any attribute type that can provide a description for an enum field.

Background

A core purpose of an Enum based type is to use the descriptiveness of natural language versus the opacity of plain numbers to make the developer's life easier. However, very often the single word names used for enum fields are far from descriptive enough for end users, and we wish to associate longer, more understandable, descriptions for enum fields, often for use in the UI. For example, a ComboBox item can have a Value property from an enum field, and a Text property from that fields description.

The versatile Description attribute can be used to decorate many code artefacts, including enum fields, with a more informative description. However, accessing the descriptions provided by the attribute requires using reflection, something we really need to avoid doing whenever we need the description. The class I describe here allows the creation of a dictionary of descriptions for an Enum type once, for later use t any time we need the descriptions.

Using the code

Deriving from Dictionary<TKey, TValue>, this class needs nothing much more (to be practical) than the constructor code below. To be impractically perfect, this dictionary should be readonly in all aspects, and the description of each enum member in the dictionary should be readonly as well. To do this, I would have had to implement IDictionary<TKey, TValue>, which is quite a big interface, instead of just deriving from Dictionary<TKey, TValue>.

C#
/// <summary>
/// Provides a dictionary of descriptions of arbitrary length for fields of any enum type.
/// </summary>
/// <typeparam name="TEnum">The <see cref="Enum"/> type to build a descriptions dictionary from.</typeparam>
/// <remarks>
/// The class looks for a <see cref="DescriptionAttribute"/> on each field of the enum, and if found, it 
/// uses the value provided by that. If not found, it simply uses the field name itself, as it does if 
/// the value of the <see cref="DescriptionAttribute.Description"/> property is empty. The dictionary 
/// key is the enum field value itself.
/// </remarks>
public class EnumDescripionDictionary<TEnum> : Dictionary<TEnum, string> where TEnum : struct, IComparable, IConvertible, IFormattable
{
  public EnumDescripionDictionary()
  {
    if (!typeof(TEnum).IsEnum)
    {
      // There is no BCL exception suitable for invalid generic type parameters.
      throw new NotSupportedException("Generic parameter T must be of type Enum.");
    }

    // These binding flags exclude the hidden, generated instance field named 'value__'. 
    var fields = typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static);
    foreach (var field in fields)
    {
      var descAtt = field.GetCustomAttribute<DescriptionAttribute>();
      if (descAtt != null)
      {
        var desc = descAtt.Description;
        if (!string.IsNullOrEmpty(desc))
        {
          Add((TEnum)Enum.Parse(typeof(TEnum), field.Name), desc);
          continue;
        }
      }
      Add((TEnum)Enum.Parse(typeof(TEnum), field.Name), field.Name);
    }
  }

  /// <summary>
  /// This method hides the <see cref="Remove"/> method of the base <see cref="Dictionary{TKey, TValue}"/> class 
  /// to prevent the removal of items. As this dictionary describes an <see cref="Enum"/> type, its contents 
  /// may not be changed at runtime.
  /// </summary>
  /// <param name="key">Key of the dictionary item to be removed.</param>
  /// <remarks>
  /// It is not necessary to hide the <see cref="Dictionary{TKey, TValue}.Add"/> method, as using any of the enum
  /// values as a key for add will cause an <see cref="ArgumentException"/> because of the duplicate key.
  /// </remarks>
  public new void Remove(TEnum key)
  {
    throw new InvalidOperationException(string.Format("Items may not be removed from this dictionary as type '{0}' has not changed.", typeof(TEnum).Name));
  }
}

The code is fairly self explanatory, and using the following test enum, I will give a usage example:

C#
internal enum TestEnum
{
  [Description("Not Operational")]
  Nop,
  Installation,
  [Description("System Set-up")]
  SystemSetup,
  [Description("Import / Export")]
  ImportExport,
  Configuration,
  [Description("Help and Documentation")]
  Help,
  Uninstall
}  

A little example program to build and demonstrate the usage of an EnumDescripionDictionary<TestEnum> :

C#
class Program
{
  static void Main(string[] args)
  {
    var dict = new EnumDescripionDictionary<TestEnum>();
    Console.WriteLine();
    Console.WriteLine("Value: {0}\tDesc: '{1}'", TestEnum.Nop, dict[TestEnum.Nop]);
    Console.WriteLine("Value: {0}\tDesc: '{1}'", TestEnum.Configuration, dict[TestEnum.Configuration]);
    Console.WriteLine();
    Console.WriteLine("Press Enter to exit.");
    Console.ReadLine();
  }
} 

This produces the following output:

Image 1

Points of Interest

The weird and wonderful generic type parameter constraints are used because .NET does not provide any native constraint to say something like where TKey: Enum.

Then I discovered Jon Skeet's unconstrained-melody, "A utility library for C# using "invalid" constraints". It allows me to replace:

public class EnumDescripionDictionary<TEnum> : Dictionary<TEnum, string> where TEnum : struct, IComparable, IConvertible, IFormattable  

with:

public class EnumDescripionDictionary<TEnum> : Dictionary<TEnum, string> where TEnum : IEnumConstraint 

It also provides a host of lovely Enum utilities that far surpass and mostly make redundant what I have done above.

The sudden arrival of a new, public field, called 'value__' in the enum fields after compilation was indeed surprising, but as I commented, the correct binding flags easily hide this field. This StackOverflow answer offers more explanation:

The JIT compiler needs a definition of a value type that describes its layout when it gets boxed. Most of them are baked into mscorlib, like System.Int32. The enum keyword lets you create a new value type. The compiler must thus provide a definition for it in the metadata. Which is what you are looking at. You'll see static fields for each enumeration member, used by ToString(). And one instance field name value__ that stores the enumeration value. Key point is that this only exists in the boxed version of an enum value.

Next

An expansion of the EnumDescripionDictionary<TEnum> class that will extract a description from any property of any custom attribute you may wish to use instead of Description.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)