18

Say I have an enum something like:

enum OrderStatus
{
    AwaitingAuthorization,
    InProduction,
    AwaitingDespatch
}

I've also created an extension method on my enum to tidy up the displayed values in the UI, so I have something like:

public static string ToDisplayString(this OrderStatus status)
{
    switch (status)
    {
        case Status.AwaitingAuthorization:
            return "Awaiting Authorization";

        case Status.InProduction:
            return "Item in Production";

        ... etc
    }
}

Inspired by the excellent post here, I want to bind my enums to a SelectList with an extension method:

public static SelectList ToSelectList<TEnum>(this TEnum enumObj)

however, to use the DisplayString values in the UI drop down I'd need to add a constraint along the lines of

: where TEnum has extension ToDisplayString

Obviously none of this is going to work at all with the current approach, unless there's some clever trick I don't know about.

Does anyone have any ideas about how I might be able to implement something like this?

Community
  • 1
  • 1
fearofawhackplanet
  • 52,166
  • 53
  • 160
  • 253

5 Answers5

33

Is there a compelling reason to use an enum here?

When you start jumping through crazy hoops to use enums, it might be time to use a class.

public class OrderStatus
{
    OrderStatus(string display) { this.display = display; }

    string display;

    public override string ToString(){ return display; }

    public static readonly OrderStatus AwaitingAuthorization
        = new OrderStatus("Awaiting Authorization");
    public static readonly OrderStatus InProduction
        = new OrderStatus("Item in Production");
    public static readonly OrderStatus AwaitingDispatch
        = new OrderStatus("Awaiting Dispatch");
}

You consume it the same as an enum:

public void AuthorizeAndSendToProduction(Order order, ProductionQueue queue)
{
    if(order.Status != OrderStatus.AwaitingAuthorization) 
    {
        Console.WriteLine("This order is not awaiting authorization!");
        return;
    }
    order.Status = OrderStatus.InProduction;
    queue.Enqueue(order);
}

The string representation is built-in, and all you need is ToString().

Jay
  • 56,361
  • 10
  • 99
  • 123
  • I use this implementation a lot. – Robin Robinson Jun 09 '10 at 16:49
  • Brilliant. Small caveat: you can't use default values, or you get "Default parameter value for '' must be a compile-time constant. – as9876 Jun 17 '15 at 14:56
  • @AYS That is correct. The default would have to be `null` and then the desired default assigned (if `null`) near the top of the function body. – Jay Jun 17 '15 at 16:50
  • haven't tried it, but can you get around that restriction by using a struct? – moarboilerplate Oct 05 '15 at 18:58
  • How would you go about serializing this to for example a JSON? Now we would have a nested object with no listed properties instead of the key (be it the internal int of the enum or its string value) as value. – Didii Sep 13 '18 at 08:18
  • @DavideCannizzo Can you explain why it is better to use struct rather than class and properties rather than readonly fields? Does it save memory? Does it help the compiler to optimize more? ... – Kjara Feb 28 '22 at 07:31
  • 1
    @Kjara Using properties does only make sense design–wise — in C# it is better to never expose fields directly. Using a `struct` rather than a `class` means `null` cannot be assigned to `OrderStatus`, which makes sense since the answer is trying to emulate an enum. As far as performance is concerned: 1) properties cause an additional call, but that's negligible and we can even be confident that the JITter will get rid of it; 2) using a `struct` won't impact anything, since the only instances we have are fields (properties backing fields) and so they have to live in the heap. – Davide Cannizzo Feb 28 '22 at 08:14
  • At that point, why bother wrapping the string representing order status into a class? Seems super redundant to me. – Jax Nov 26 '22 at 06:43
2

Of course, you can use the DisplayAttribute to annotate your Enums.

enum OrderStatus
{
    [Display(Description="Long Desc", Name="Awaiting Authorization", ShortName="Wait Auth")]
    AwaitingAuthorization,

    [Display(Description="...", Name="...", ShortName="...")]
    InProduction,

    [Display(Description="...", Name="...", ShortName="...")]       
    AwaitingDespatch
}

You can also opt to create an extension method taking any enumeration value and returning its display name based on the attribute set to it to tidy up the displayed values in the UI, as follows:

public static class EnumExtensions
{
    public static string ToName(this Enum enumValue)
    {
        var displayAttribute = enumValue.GetType()
            .GetMember(enumValue.ToString())[0]
            .GetCustomAttributes(false)
            .Select(a => a as DisplayAttribute)
            .FirstOrDefault();
        return displayAttribute?.Name ?? enumValue.ToString();
    }
}

With

public enum Test
{
    [Display(Name="AAA")]
    a,
    b
}

Code:

Console.WriteLine(Test.a.ToName());
Console.WriteLine(Test.b.ToName());

Results

AAA

b

I want to bind my enums to a SelectList with an extension method:

For type safety, I wouldn't use an extension methods, but instead a static class that deals with the Enum type:

Pre C# 7.3 version. Since Enum is not a valid type constraint prior to 7.3 (and it would cause a compile-time exception), you'll end up by considering that enums are value types and they implement some interfaces, in order to restrict the type parameter as close to Enum as possible.

public static class Enums<TEnum> where TEnum : struct, IComparable, IFormattable, IConvertible
{
    static Enums()
    {
        if (!typeof(TEnum).IsEnum)
        {
            throw new InvalidOperationException();
        }
    }
}

C# 7.3+ version, with compile time checking... yay!

public static class Enums<TEnum> where TEnum : Enum
{
}

GetValues Method for the class:

public static IEnumerable<TEnum> GetValues(bool includeFirst)
{
    var result = ((TEnum[])Enum.GetValues(typeof(TEnum))).ToList();
    if (!includeZero)
        result = result.Where(r => r != default).ToList();
    return result;
}

If you follow Enum Guidelines and include the Default (zero) value, we can ignore it (sometimes we want to display the value like "None Selected" and sometimes we don't "Invalid Selection").

Then we can add another method:

public static IEnumerable<string> GetNames(bool includeFirst)
{
    var result = GetValue(includeFirst)
       .Select(v => v.ToName())
       .ToList();
    return result;
}
Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
1

Instead of using "ToDisplayString", simply override ToString() of your enum. So if an enum overrides it it will take it, otherwise it will take the default ToString behavior (in ToSelectList).

yoozer8
  • 7,361
  • 7
  • 58
  • 93
0

If you just need to use relatively tiny enumerate classes that have no more than an explicit casting operator, ToString and do not take other usability for the special ones about enum on System and its derived namespaces, then the following example could be a solution:

namespace MyNamespace {
    public abstract class EnumerateClass<Type, InheritingClass> : IEquatable<InheritingClass>
        where Type : IEquatable<Type>
        where InheritingClass : EnumerateClass<Type, InheritingClass> {

        internal readonly Type Identifier;

        protected EnumerateClass (Type identifier) {
            this.Identifier = identifier;
        }
        public bool Equals(InheritingClass obj)
            => this.Identifier.Equals(obj.Identifier);
        public static explicit operator Type(EnumerateClass<Type, InheritingClass> obj)
            => obj.Identifier;
    }
    public sealed class MyNumber : EnumerateClass<int, MyNumber> {

        private MyNumber(int identifier) : base(identifier) { }

        public static readonly MyNumber None = new Number(0);
        public static readonly MyNumber One = new Number(1);
        public static readonly MyNumber Two = new Number(2);
        ...

        public override string ToString() {
            switch (this.Identifier) {
                case 0: return "None";
                case 1: return "One";
                case 2: return "Two";
                ...
            }
        }
    }
}
YehHyunLee
  • 93
  • 5
-4

You could do this:

public static string ToOrderStatusDisplayString(this Enum status)
{    
    switch ((OrderStatus)status)
    {
         ...
    }
}

Then restrict TEnum to Enum: where TEnum : System.Enum

Of course, that way you get a bunch of methods on the Enum itself and lose type safety.

František Žiačik
  • 7,511
  • 1
  • 34
  • 59
  • 1
    [You cannot write `where TEnum : Enum`](http://stackoverflow.com/questions/1331739/enum-type-constraints-in-c/1416660#1416660). – SLaks Jun 09 '10 at 17:11