Friendly-Format Enum for use in ComboBoxes, Checke

2019-06-09 12:28发布

问题:

Requirement

I want to select values from an enum in C# using a ComboBox or select bitmasks (for enums with the Flags attribute) with a CheckedListBox. I want a way to add the values to the controls as selectable items, and cleanly tell which the user has selected.

Objective 1: User-friendly

I also want the selection to be clear and pretty to the user. Currently I can already add Enum values to a ComboBox or a CheckedListBox, but Enum.ToString() will return the identifier name. Pascal Case is good enough for me, but not for my users.

Objective 2: Easy to code

I want this to be easy to code. I mean easy. I want this to be a mere afterthought to defining and/or using any enum value.

My Solution

Now I looked around and saw several nice solutions. Some of them are better in their own ways, like if you really need to put in your own custom descriptions, or to take it one step further, support multiple languages. You can do fake enums too. But I kept looking, and none of them had quite the combination of elegance and simplicity--for my purposes--as what I wrote. See below.

回答1:

The basis of my solution is a struct that can wrap Enum values and override ToString().

Enter the EnumWrapper:

public struct EnumWrapper
{
    private readonly Enum e;
    public EnumWrapper(Enum e) {
        this.e = e;
    }
    public static implicit operator Enum(EnumWrapper wrapper) {
        return wrapper.e;
    }
    public static explicit operator EnumWrapper(Enum e) {
        return new EnumWrapper(e);
    }
    public override string ToString() {
        return e.ToStringFriendly();
    }
}

The method ToStringFriendly() is defined as an extension method on Enum:

using System.Text.RegularExpressions;
public static class _Extensions
{
    public static string ToStringFriendly(this Enum e)
    {
        string s = e.ToString();

        // enforce a charset: letters, numbers, and underscores
        s = Regex.Replace(s, "[^A-Za-z0-9_]", ""); 

        // separate numbers from letters
        s = Regex.Replace(s, "([a-zA-Z])([0-9])", "$1 $2"); 

        // separate letters from numbers
        s = Regex.Replace(s, "([0-9])([a-zA-Z])", "$1 $2"); 

        // space lowercases before uppercase (word boundary)
        s = Regex.Replace(s, "([a-z])([A-Z])", "$1 $2"); 

        // see that the nice pretty capitalized words are spaced left
        s = Regex.Replace(s, "(?!^)([^ _])([A-Z][a-z]+)", "$1 $2"); 

        // replace double underscores with colon-space delimiter
        s = Regex.Replace(s, "__", ": "); 

        // finally replace single underscores with hyphens
        s = Regex.Replace(s, "_", "-"); 

        return s;
    }
}

Now, to add any Enum value to a ComboBox, for example,

comboBox.Items.Add((EnumWrapper)MyEnum.SomeValue);

And to get it back out (after null-testing, of course):

MyEnum myValue = (MyEnum)(Enum)(EnumWrapper)comboBox.SelectedItem;

And that's it. Now what are the pros and cons of this approach?

The Pros:

  • You can pass the enum values (almost) directly in and out of your controls. Just cast back and forth to/from EnumWrapper.
  • This works nicely for all Pascal-cased Enum values with a few special cases. One example might be a value called MyValue__DescriptionOf123_ENUMValue which would come out of ToStringFriendly() as "My Value: Description of 123-ENUM Value".
  • The mechanics (a struct and an extension method) can be written once and tucked out of the way. There is no additional coding in each Enum. This means it works nicely as above for enums you didn't write, assuming Pascal case, which in .NET is a good assumption. Note: This is why I'd say this answer, however elegant, is not a better solution than mine, as it requires that you can add a TypeConverter attribute to each Enum.

The Cons:

  • This doesn't support custom descriptions or multiple languages. For example a value like Encoding.EBCDIC will always come through as "EBCDIC" and does not allow you to manually type "Extended Binary Coded Decimal Interchange Code", much less other languages.

Future Work

One could add custom descriptions and multi-language support by changing ToStringFriendly() to do a language-lookup for that value before de-Pascal-casing.

For more Regex fun, see this thread.