How to have userfriendly names for enumerations?

12,424

Solution 1

Thank you all for all answers. Finally I used a combination from Rex M and adrianbanks, and added my own improvements, to simplify the binding to ComboBox.

The changes were needed because, while working on the code, I realized sometimes I need to be able to exclude one enumeration item from the combo. E.g.

Enum Complexity
{
  // this will be used in filters, 
  // but not in module where I have to assign Complexity to a field
  AllComplexities,  
  NotSoComplex,
  LittleComplex,
  Complex,
  VeryComplex
}

So sometimes I want the picklist to show all but AllComplexities (in add - edit modules) and other time to show all (in filters)

Here's what I did:

  1. I created a extension method, that uses Description Attribute as localization lookup key. If Description attribute is missing, I create the lookup localization key as EnumName_ EnumValue. Finally, if translation is missing I just split enum name based on camelcase to separate words as shown by adrianbanks. BTW, TranslationHelper is a wrapper around resourceMgr.GetString(...)

The full code is shown below

public static string GetDescription(this System.Enum value)
{
    string enumID = string.Empty;
    string enumDesc = string.Empty;
    try 
    {         
        // try to lookup Description attribute
        FieldInfo field = value.GetType().GetField(value.ToString());
        object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true);
        if (attribs.Length > 0)
        {
            enumID = ((DescriptionAttribute)attribs[0]).Description;
            enumDesc = TranslationHelper.GetTranslation(enumID);
        }
        if (string.IsNullOrEmpty(enumID) || TranslationHelper.IsTranslationMissing(enumDesc))
        {
            // try to lookup translation from EnumName_EnumValue
            string[] enumName = value.GetType().ToString().Split('.');
            enumID = string.Format("{0}_{1}", enumName[enumName.Length - 1], value.ToString());
            enumDesc = TranslationHelper.GetTranslation(enumID);
            if (TranslationHelper.IsTranslationMissing(enumDesc))
                enumDesc = string.Empty;
        }

        // try to format CamelCase to proper names
        if (string.IsNullOrEmpty(enumDesc))
        {
            Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
            enumDesc = capitalLetterMatch.Replace(value.ToString(), " $&");
        }
    }
    catch (Exception)
    {
        // if any error, fallback to string value
        enumDesc = value.ToString();
    }

    return enumDesc;
}

I created a generic helper class based on Enum, which allow to bind the enum easily to DataSource

public class LocalizableEnum
{
    /// <summary>
    /// Column names exposed by LocalizableEnum
    /// </summary>
    public class ColumnNames
    {
        public const string ID = "EnumValue";
        public const string EntityValue = "EnumDescription";
    }
}

public class LocalizableEnum<T>
{

    private T m_ItemVal;
    private string m_ItemDesc;

    public LocalizableEnum(T id)
    {
        System.Enum idEnum = id as System.Enum;
        if (idEnum == null)
            throw new ArgumentException(string.Format("Type {0} is not enum", id.ToString()));
        else
        {
            m_ItemVal = id;
            m_ItemDesc = idEnum.GetDescription();
        }
    }

    public override string ToString()
    {
        return m_ItemDesc;
    }

    public T EnumValue
    {
        get { return m_ID; }
    }

    public string EnumDescription
    {
        get { return ToString(); }
    }

}

Then I created a generic static method that returns a List>, as below

public static List<LocalizableEnum<T>> GetEnumList<T>(object excludeMember)
{
    List<LocalizableEnum<T>> list =null;
    Array listVal = System.Enum.GetValues(typeof(T));
    if (listVal.Length>0)
    {
        string excludedValStr = string.Empty;
        if (excludeMember != null)
            excludedValStr = ((T)excludeMember).ToString();

        list = new List<LocalizableEnum<T>>();
        for (int i = 0; i < listVal.Length; i++)
        {
            T currentVal = (T)listVal.GetValue(i);
            if (excludedValStr != currentVal.ToString())
            {
                System.Enum enumVal = currentVal as System.Enum;
                LocalizableEnum<T> enumMember = new LocalizableEnum<T>(currentVal);
                list.Add(enumMember);
            }
        }
    }
    return list;
}

and a wrapper to return list with all members

public static List<LocalizableEnum<T>> GetEnumList<T>()
{
        return GetEnumList<T>(null);
}

Now let's put all things together and bind to actual combo:

// in module where we want to show items with all complexities
// or just filter on one complexity

comboComplexity.DisplayMember = LocalizableEnum.ColumnNames.EnumValue;
comboComplexity.ValueMember = LocalizableEnum.ColumnNames.EnumDescription;
comboComplexity.DataSource = EnumHelper.GetEnumList<Complexity>();
comboComplexity.SelectedValue = Complexity.AllComplexities;

// ....
// and here in edit module where we don't want to see "All Complexities"
comboComplexity.DisplayMember = LocalizableEnum.ColumnNames.EnumValue;
comboComplexity.ValueMember = LocalizableEnum.ColumnNames.EnumDescription;
comboComplexity.DataSource = EnumHelper.GetEnumList<Complexity>(Complexity.AllComplexities);
comboComplexity.SelectedValue = Complexity.VeryComplex; // set default value

To read selected the value and use it, I use code as below

Complexity selComplexity = (Complexity)comboComplexity.SelectedValue;

Solution 2

Basic Friendly names

Use the Description attribute:*

enum MyEnum
{
    [Description("This is black")]
    Black,
    [Description("This is white")]
    White
}

And a handy extension method for enums:

public static string GetDescription(this Enum value)
{
    FieldInfo field = value.GetType().GetField(value.ToString());
    object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true);
    if(attribs.Length > 0)
    {
        return ((DescriptionAttribute)attribs[0]).Description;
    }
    return string.Empty;
}

Used like so:

MyEnum val = MyEnum.Black;
Console.WriteLine(val.GetDescription()); //writes "This is black"

(Note this doesn't exactly work for bit flags...)

For localization

There is a well-established pattern in .NET for handling multiple languages per string value - use a resource file, and expand the extension method to read from the resource file:

public static string GetDescription(this Enum value)
{
    FieldInfo field = value.GetType().GetField(value.ToString());
    object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true));
    if(attribs.Length > 0)
    {
        string message = ((DescriptionAttribute)attribs[0]).Description;
        return resourceMgr.GetString(message, CultureInfo.CurrentCulture);
    }
    return string.Empty;
}

Any time we can leverage existing BCL functionality to achieve what we want, that's definitely the first route to explore. This minimizes complexity and uses patterns already familiar to many other developers.

Putting it all together

To get this to bind to a DropDownList, we probably want to track the real enum values in our control and limit the translated, friendly name to visual sugar. We can do so by using an anonymous type and the DataField properties on the list:

<asp:DropDownList ID="myDDL"
                  DataTextField="Description"
                  DataValueField="Value" />

myDDL.DataSource = Enum.GetValues(typeof(MyEnum)).OfType<MyEnum>().Select(
    val => new { Description = val.GetDescription(), Value = val.ToString() });

myDDL.DataBind();

Let's break down that DataSource line:

  • First we call Enum.GetValues(typeof(MyEnum)), which gets us a loosely-typed Array of the values
  • Next we call OfType<MyEnum>() which converts the array to an IEnumerable<MyEnum>
  • Then we call Select() and provide a lambda that projects a new object with two fields, Description and Value.

The DataTextField and DataValueField properties are evaluated reflectively at databind-time, so they will search for fields on DataItem with matching names.

-Note in the main article, the author wrote their own DescriptionAttribute class which is unnecessary, as one already exists in .NET's standard libraries.-

Solution 3

The use of attributes as in the other answers is a good way to go, but if you just want to use the text from the values of the enum, the following code will split based on the camel-casing of the value:

public static string GetDescriptionOf(Enum enumType)
{
    Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
    return capitalLetterMatch.Replace(enumType.ToString(), " $&");
}

Calling GetDescriptionOf(Complexity.NotSoComplex) will return Not So Complex. This can be used with any enum value.

To make it more useful, you could make it an extension method:

public static string ToFriendlyString(this Enum enumType)
{
    Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
    return capitalLetterMatch.Replace(enumType.ToString(), " $&");
}

You cal now call it using Complexity.NotSoComplex.ToFriendlyString() to return Not So Complex.


EDIT: just noticed that in your question you mention that you need to localise the text. In that case, I'd use an attribute to contain a key to look up the localised value, but default to the friendly string method as a last resort if the localised text cannot be found. You would define you enums like this:

enum Complexity
{
    [LocalisedEnum("Complexity.NotSoComplex")]
    NotSoComplex,
    [LocalisedEnum("Complexity.LittleComplex")]
    LittleComplex,
    [LocalisedEnum("Complexity.Complex")]
    Complex,
    [LocalisedEnum("Complexity.VeryComplex")]
    VeryComplex
}

You would also need this code:

[AttributeUsage(AttributeTargets.Field, AllowMultiple=false, Inherited=true)]
public class LocalisedEnum : Attribute
{
    public string LocalisationKey{get;set;}

    public LocalisedEnum(string localisationKey)
    {
        LocalisationKey = localisationKey;
    }
}

public static class LocalisedEnumExtensions
{
    public static string ToLocalisedString(this Enum enumType)
    {
        // default value is the ToString();
        string description = enumType.ToString();

        try
        {
            bool done = false;

            MemberInfo[] memberInfo = enumType.GetType().GetMember(enumType.ToString());

            if (memberInfo != null && memberInfo.Length > 0)
            {
                object[] attributes = memberInfo[0].GetCustomAttributes(typeof(LocalisedEnum), false);

                if (attributes != null && attributes.Length > 0)
                {
                    LocalisedEnum descriptionAttribute = attributes[0] as LocalisedEnum;

                    if (description != null && descriptionAttribute != null)
                    {
                        string desc = TranslationHelper.GetTranslation(descriptionAttribute.LocalisationKey);

                        if (desc != null)
                        {
                            description = desc;
                            done = true;
                        }
                    }
                }
            }

            if (!done)
            {
                Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
                description = capitalLetterMatch.Replace(enumType.ToString(), " $&");
            }
        }
        catch
        {
            description = enumType.ToString();
        }

        return description;
    }
}

To get the localised descriptions, you would then call:

Complexity.NotSoComplex.ToLocalisedString()

This has several fallback cases:

  • if the enum has a LocalisedEnum attribute defined, it will use the key to look up the translated text
  • if the enum has a LocalisedEnum attribute defined but no localised text is found, it defaults to using the camel-case split method
  • if the enum does not have a LocalisedEnum attribute defined, it will use the camel-case split method
  • upon any error, it defaults to the ToString of the enum value

Solution 4

I use the following class

    public class EnumUtils
    {
    /// <summary>
    ///     Reads and returns the value of the Description Attribute of an enumeration value.
    /// </summary>
    /// <param name="value">The enumeration value whose Description attribute you wish to have returned.</param>
    /// <returns>The string value portion of the Description attribute.</returns>
    public static string StringValueOf(Enum value)
    {
        FieldInfo fi = value.GetType().GetField(value.ToString());
        DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (attributes.Length > 0)
        {
            return attributes[0].Description;
        }
        else
        {
            return value.ToString();
        }
    }

    /// <summary>
    ///     Returns the Enumeration value that has a given Description attribute.
    /// </summary>
    /// <param name="value">The Description attribute value.</param>
    /// <param name="enumType">The type of enumeration in which to search.</param>
    /// <returns>The enumeration value that matches the Description value provided.</returns>
    /// <exception cref="ArgumentException">Thrown when the specified Description value is not found with in the provided Enumeration Type.</exception>
    public static object EnumValueOf(string value, Type enumType)
    {
        string[] names = Enum.GetNames(enumType);
        foreach (string name in names)
        {
            if (StringValueOf((Enum)Enum.Parse(enumType, name)).Equals(value))
            {
                return Enum.Parse(enumType, name);
            }
        }

        throw new ArgumentException("The string is not a description or value of the specified enum.");
    }

Which reads an attribute called description

public enum PuppyType
{
    [Description("Cute Puppy")]
    CutePuppy = 0,
    [Description("Silly Puppy")]
    SillyPuppy
}
Share:
12,424

Related videos on Youtube

bzamfir
Author by

bzamfir

Updated on February 15, 2020

Comments

  • bzamfir
    bzamfir over 4 years

    I have an enumeration like

    Enum Complexity
    {
      NotSoComplex,
      LittleComplex,
      Complex,
      VeryComplex
    }
    

    And I want to use it in a dropdown list, but don't want to see such Camel names in list (looks really odd for users). Instead I would like to have in normal wording, like Not so complex Little complex (etc)

    Also, my application is multi-lang and I would like to be able to display those strings localized, and I use a helper, TranslationHelper(string strID) which gives me the localized version for a string id.

    I have a working solution, but not very elegant: I create a helper class for the enum, with one member Complexity and ToString() overwritten, like below (code simplified)

    public class ComplexityHelper
    {
        public ComplexityHelper(Complexity c, string desc)
        { m_complex = c; m_desc=desc; }
    
        public Complexity Complexity { get { ... } set {...} }
        public override ToString() { return m_desc; }
    
        //Then a static field like this 
    
        private static List<Complexity> m_cxList = null;
    
        // and method that returns the status lists to bind to DataSource of lists
        public static List<ComplexityHelper> GetComplexities() 
        {
            if (m_cxList == null)
            {
               string[] list = TranslationHelper.GetTranslation("item_Complexities").Split(',');
               Array listVal = Enum.GetValues(typeof(Complexities));
               if (list.Length != listVal.Length)
                   throw new Exception("Invalid Complexities translations (item_Complexities)");
               m_cxList = new List<Complexity>();
               for (int i = 0; i < list.Length; i++)
               {
                 Complexity cx = (ComplexitylistVal.GetValue(i);
                 ComplexityHelper ch = new ComplexityHelper(cx, list[i]);
                 m_cxList.Add(ch);
               }
            }
            return m_cxList;
        }
    }
    

    While workable, I'm not happy with it, since I have to code it similarily for various enums I need to use in picklists.

    Does anyone have a suggestion for a simpler or more generic solution?

    Thanks Bogdan

  • stevemegson
    stevemegson over 14 years
    For localization, you might skip the description attributes and just use "MyEnum.Black" and "MyEnum.White" as the resource names.
  • bzamfir
    bzamfir over 14 years
    Hi Thanks for answer. I tried your suggestion, and tried to bind the combo to enum with code like this comboComplexity.DataSource = Enum.GetValues(typeof(Complexity)); But this made the list to display only default enum names I renamed also ToLocalizedString() to ToString() (knowing that this is actually called by Combo) but still didn't worked. Any suggestion? I would like a simple binding like this, since with an enum with 20+ values, adding all values one by one will be a pain Thanks
  • bzamfir
    bzamfir over 14 years
    Hi, What I would like to accomplish is to use a binding like comboComplexity.DataSource = Enum.GetValues(typeof(Complexity)); But using this I still get in the combo the names of the enumeration values, and not their descriptions Also, I renamed GetDescription to ToString(), hoping that combo will use that for enum name, but no luck Any suggestion? Thanks
  • JonDrnek
    JonDrnek over 14 years
    @bzamfir see my additional notes at the bottom of the answer.
  • adrianbanks
    adrianbanks over 14 years
    By renaming ToLocalizedString() to ToString(), you have not overridden the ToString() method of the enum. Instead, you have made an extension method with the same name as the already existing ToString() method of the enum. Calling ToString() will call the existing method and not the extension method. You will have to get the localised strings for each enum value and databind to that set of strings.
  • Fischer
    Fischer over 11 years
    Added an extensionmethod to easy add to dropdown: myDropDown.Items.AddEnumDescriptions<myEnumType>(); public static void AddEnumDescriptions<T>(this ListItemCollection value) { foreach (var enumvalue in System.Enum.GetValues(typeof(T)).OfType<Enum>()) { value.Add(new ListItem(enumvalue.GetDescription(), enumvalue.ToString())); } }