The last two articles (here and here) described creating a custom type converter for converting units of measurement.

However, what happens when you want to display or convert to/from alternative representations? For example, consider the enum below.

csharp
internal enum Unit
{
  cm,
  mm,
  pt,
  px
}

Apart from the fact such an enum more than likely doesn't match any coding standards you use, what happens when you want to include percentages in the mix? Not many languages are going to let you use % as a symbol name!

So we rewrite the enum to make more sense, in which case you might have this:

csharp
internal enum Unit
{
  Centimetre,
  Millimetre,
  Point,
  Pixel,
  Percent
}
Using the actual enum member names doesn't seem like a good idea does it?
Using the actual enum member names doesn't seem like a good idea does it?

Great! Except... your users want to see cm, px, % etc. Now what?

The manual way

Well, you could create a function which takes a unit, and manually checks the values and returns an appropriate value, for example:

csharp
public string GetUnitSuffix(Unit unit)
{
  string result;

  switch (unit)
  {
    case Unit.Centimetre:
      result = "cm";
      break;
    ...
  }

  return result;
}

While this would certainly work, it means you have to duplicate this code for every enum you wish to have alternate descriptions for. Not to mention, should you add a new member to the enum, you have to remember to update this function. More than likely, you also want a sister version of this function which accepts the string version, and returns the enum value.

The automatic way

A better way would be to tag each enum member with an appropriate description, then you can use reflection to scan your enum members and perform automatic to and from conversions.

In this example, I'm going to use the DescriptionAttribute from the System.ComponentModel namespace, although depending on what you're trying to do, a custom attribute may be better - that's not exactly what this attribute was intended for!

First, decorate your enum with the attribute.

csharp
internal enum Unit
{
  [Description("cm")]
  Centimetre,

  [Description("mm")]
  Millimetre,

  [Description("pt")]
  Point,

  [Description("px")]
  Pixel,

  [Description("%")]
  Percent
}

Next add a couple of functions that will perform the conversion of your enum to and from a string. With this in place you can add new members, and, as long as you add your attribute to them, the functions will automatically handle the new values.

csharp
public static string GetDescription(this Unit value)
{
  FieldInfo field;
  DescriptionAttribute attribute;
  string result;

  field = value.GetType().GetField(value.ToString());
  attribute = (DescriptionAttribute)Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
  result = attribute != null ? attribute.Description : string.Empty;

  return result;
}

public static Unit GetValue(string value)
{
  Unit result;

  result = Unit.None;

  foreach (Unit id in Enum.GetValues(typeof(Unit)))
  {
    FieldInfo field;
    DescriptionAttribute attribute;

    field = id.GetType().GetField(id.ToString());
    attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;

    if (attribute != null && attribute.Description == value)
    {
      result = id;
      break;
    }
  }

  return result;
}

I choose to make the version that accepts the enum member as an input parameter an extension method, that way I can call it like this:

csharp
string unitSuffix;

unitSuffix = this.Unit.GetDescription();

However, as the sister method accepts a string parameter, it doesn't make sense to make this an extension, unless you want it to appear on every single string variable you declare! So I just revert back to the usual static calling convention.

csharp
Unit unit;

unit = EnumExtensions.GetValue(stringValue.Substring(nonDigitIndex));
Much better - the user sees the short form version, but the code uses the full name
Much better - the user sees the short form version, but the code uses the full name

Using Generics

While there's nothing wrong with the above methods, they could still be improved upon. As it stands now, the methods are fixed to a specific enum, so we can change them to use generics instead, then they'll work for all enums.

csharp
public static string GetDescription<T>(this T value)
  where T : struct
{
  FieldInfo field;
  DescriptionAttribute attribute;
  string result;

  field = value.GetType().GetField(value.ToString());
  attribute = (DescriptionAttribute)Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
  result = attribute != null ? attribute.Description : string.Empty;

  return result;
}

public static T GetValue<T>(string value, T defaultValue)
{
  T result;

  result = defaultValue;

  foreach (T id in Enum.GetValues(typeof(T)))
  {
    FieldInfo field;
    DescriptionAttribute attribute;

    field = id.GetType().GetField(id.ToString());
    attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;

    if (attribute != null && attribute.Description == value)
    {
      result = id;
      break;
    }
  }

  return result;
}

Final points

Using reflection does have an overhead. If you expect to be calling these methods a lot, you may wish to extend them yet further in order to support caching the results in a dictionary or other mechanism of your choice. That way, the first time a new member is requested you perform the reflection lookup, and thereafter just read the cache. I haven't done any benchmarking, but it's probably safe to say a dictionary lookup (remember to use TryGetValue!) is going to be a lot faster than a reflection scan.

An example showing how the custom type converter from the previous two articles updated to use the above technique is available from the link below.

Update History

  • 2013-07-28 - First published
  • 2020-11-21 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments

# Ofer

Very nice and useful, thanks!

Reply