Click here to Skip to main content
15,892,927 members
Articles / Programming Languages / C#

Enum Pattern

Rate me:
Please Sign up or sign in to vote.
4.33/5 (17 votes)
22 Sep 2009CPOL5 min read 54.1K   33   12
Enum Pattern

Introduction

A programmer often has to deal with a fixed number of cases in his program. For example, if your program is about emergency management, you may consider the following cases:

  1. Fire
  2. Injured
  3. Burglary
  4. Blue Screen of Death
  5. Run out of Beer

and your program may look like:

C#
int emergency = 3;
switch (emergency)
{
    case 1:
    case 2:
    case 3:
        Console.WriteLine("Call 911");
        break;
    case 4:
        Console.WriteLine("Sue Microsoft");
        break;
    case 5:
        Console.WriteLine("Brew your own");
        break;
}

Here, each emergency case is assigned a number. The problem with it is that the program becomes hard to read if the reader doesn't know what each number stands for. This is where enum comes to the rescue:

C#
enum Emergency
{
    Fire = 1,
    Injured,
    Burglary,
    Blue_Screen_of_Death,
    Run_out_of_Beer
}

Emergency emergency = Emergency.Injured;
switch (emergency)
{
    case Emergency.Fire:
    case Emergency.Injured:
    case Emergency.Burglary:
        Console.WriteLine("Call 911");
        break;
    case Emergency.Blue_Screen_of_Death:
        Console.WriteLine("Sue Microsoft");
        break;
    case Emergency.Run_out_of_Beer:
        Console.WriteLine("Brew your own");
        break;
}

So enum is just a way of naming numbered cases. It makes your program more readable. In .NET, it's very easy to get the names of numbered cases as strings:

C#
Console.WriteLine(emergency.ToString());
//output: Injured
Console.WriteLine(string.Join(",", Enum.GetNames(typeof(Emergency))));
//output: Fire,Injured,Burglary,Blue_Screen_of_Death,Run_out_of_Beer

However, in principle, you should reject the temptation of using enum names in the presentation layer, as the names are meant for the convenience of programmers, not for the users of your program. For example, "Blue_Screen_of_Death" or even "BSoD" is perfectly fine in your program, but when presented to the users, it's just not that user friendly.

What if you do want to associate a user-friendly string to each enumerated case? Muaddubby in his article A Perfect C# String Enumerator was trying to do just that. He claims his string enumerator has all the right behaviors of a real enum. But he fails to address the fact that a real enum is a value type constant so you can use it in a switch statement and do flag bit operations with it when decorated with the [Flags] attribute. So his string enumerator is not that perfect after all. A better approach, as described in String Enumerations in C# by Matt Simner and Enum With String Values In C# by Stefan Sedich, is to leverage the existing enum. They associate a string to an enumerated case by way of a custom attribute, and use reflection to gain access to the string via an extension method.

Enum Pattern

From previous efforts of devising a string enumerator, it's evident that we need a general programming pattern of associating a string, or any object for that matter, to an enumerated case. In fact, Matt Simner and Stefan Sedich in their articles have demonstrated a special case of the pattern. We just need to make it a little bit more generic so it can handle any object, not just a string. Here is what is involved:

Step 1

We need to define a base class that represents the attribute of an enumerated case.

C#
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class EnumAttr : Attribute
{
    public EnumAttr()
    {
    }
}

Since the attribute can be any object, we don't want it to have any particular behaviors by implementing methods or properties, except that it must apply to an enum field no more than once.

Step 2

We define the following extension method to gain access to the attribute object associated with an enum field:

C#
public static class EnumExtension
{
    public static EnumAttr GetAttr(this Enum value)
    {
        Type type = value.GetType();
        FieldInfo fieldInfo = type.GetField(value.ToString());
        var atts = (EnumAttr[])fieldInfo.GetCustomAttributes(typeof(EnumAttr), false);
        return atts.Length > 0 ? atts[0] : null;
    }
}

Step 3

Define your own attribute class derived from EnumAttr.

Step 4

Define your enum and add an instance of your attribute class in Step 3 to each enum field. Your enum can be regular or flagged.

Step 5

Use your enum as usual. Whenever you need to access the attribute data of an enum field, just call the GetAttr() extension method.

Examples

The other day, I was looking for something on Craig's List and wondered around to the personal classified ad section. To my surprise (well, not that surprised), I found that there were a lot more categories than just man-seeking-woman and woman-seeking-man. Here is a partial list:

C#
public class SexInterestAttr : EnumAttr
{
    public string Desc { get; set; }
}

public enum SexInterest
{
    [SexInterestAttr(Desc = "woman seeking man")]
    w4m,
    [SexInterestAttr(Desc = "man seeking man")]
    m4m,
    [SexInterestAttr(Desc = "man seeking woman")]
    m4w,
    [SexInterestAttr(Desc = "woman seeking woman")]
    w4w,
    [SexInterestAttr(Desc = "couple seeking couple")]
    mw4mw,
    [SexInterestAttr(Desc = "couple seeking woman")]
    mw4w,
    [SexInterestAttr(Desc = "couple seeking man")]
    mw4m,
    [SexInterestAttr(Desc = "woman seeking couple")]
    w4mw,
    [SexInterestAttr(Desc = "man seeking couple")]
    m4mw,
    [SexInterestAttr(Desc = "woman seeking lesbian couple")]
    w4ww,
    [SexInterestAttr(Desc = "man seeking gay couple")]
    m4mm,
    [SexInterestAttr(Desc = "gay couple seeking man")]
    mm4m,
    [SexInterestAttr(Desc = "lesbian couple seeking woman")]
    ww4w,
    [SexInterestAttr(Desc = "lesbian couple seeking man")]
    ww4m,
    [SexInterestAttr(Desc = "gay couple seeking woman")]
    mm4w,
    [SexInterestAttr(Desc = "man seeking lesbian couple")]
    m4ww,
    [SexInterestAttr(Desc = "woman seeking gay couple")]
    w4mm,
}

Here, our attribute class, SexInterestAttr, contains only one property Desc, a user-friendly string to describe the sex interest. Notice that in our SexInterest enum, we can use the object initializer syntax to create an instance of SexInterestAttr. The following code snippet shows how to use the enum and its attribute:

C#
    var interests = (SexInterest[])Enum.GetValues(typeof(SexInterest));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        interests.Select(i => string.Format("{0} - {1}", i.ToString(), 
            ((SexInterestAttr)i.GetAttr()).Desc)).ToArray())); 
Output: 
    w4m - woman seeking man
    m4m - man seeking man
    m4w - man seeking woman
    w4w - woman seeking woman
    mw4mw - couple seeking couple
    mw4w - couple seeking woman
    mw4m - couple seeking man
    w4mw - woman seeking couple
    m4mw - man seeking couple
    w4ww - woman seeking lesbian couple
    m4mm - man seeking gay couple
    mm4m - gay couple seeking man
    ww4w - lesbian couple seeking woman
    ww4m - lesbian couple seeking man
    mm4w - gay couple seeking woman
    m4ww - man seeking lesbian couple
    w4mm - woman seeking gay couple

Adding attribute to enum doesn't alter its value type. We can still use it as usual such as in a switch statement:

C#
var interest = SexInterest.w4m;
switch (interest)
{
    case SexInterest.m4m:
        Console.WriteLine("you are a gay");
        break;
    case SexInterest.w4w:
        Console.WriteLine("you are a lesbian");
        break;
    case SexInterest.m4w:
    case SexInterest.w4m:
        Console.WriteLine("you are a hetero");
        break;
    default:
        Console.WriteLine("whatever. it's a free country");
        break;
}
//ourput: you are a hetero

As a side point, one of the advantages of using extension method is that it seemingly becomes part of our enum type and actually shows up in the IntelliSense:

The above example is quite simple. We only associate a string to each enumerated case. Now let's take a look at a more complex example. Assume we have an enumeration of US presidents and each president can be described with the following PresidentAttr class:

C#
public class PresidentAttr : EnumAttr
{
    public PresidentAttr(string name, string party, int yearTookOffice,
        int yearBorn, int yearDied)
    {
        Name = name;
        Party = party;
        YearTookOffice = yearTookOffice;
        YearBorn = yearBorn;
        YearDied = yearDied;
    }
    public string Name { get; set; }
    public string Party { get; set; }
    public int YearTookOffice { get; set; }
    public int YearBorn { get; set; }
    public int YearDied { get; set; }
    public bool IsAlive { get { return (YearDied <= 0); } }
    public int AgeTookOffice { get { return YearTookOffice - YearBorn; } }
}

Our President enum is defined as:

C#
public enum President
{
    [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
    GeorgeWashington,
    [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
    JohnAdams,
    [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
    ThomasJefferson,
    ...

    [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
    BillClinton,
    [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
    GeorgeWBush,
    [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
    BarackObama
}

Now we can have some funs with our President enum:

C#
    var presidents = (President[])Enum.GetValues(typeof(President));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        presidents.Select(p => (PresidentAttr)p.GetAttr()).
        Select(a => string.Format("{0} ({1}-{2}), took office in {3}", 
            a.Name, a.YearBorn, (a.YearDied<=0)? "" : a.YearDied.ToString(), 
            a.YearTookOffice)).ToArray()));

Output:
    George Washington (1732-1799), took office in 1789
    John Adams (1735-1826), took office in 1797
    Thomas Jefferson (1743-1826), took office in 1801
    ...
    Bill Clinton (1946-), took office in 1993
    George W. Bush (1946-), took office in 2001
    Barack Obama (1961-), took office in 2009 

    Console.WriteLine("There are {0} Democratic presidents", 
        presidents.Count(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));
    Console.WriteLine("They are:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Array.FindAll(presidents, p => ((PresidentAttr)p.GetAttr()).Party == "Democratic").
        Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    There are 16 Democratic presidents
    They are:
    Andrew Jackson
    Martin Van Buren
    James K. Polk
    Franklin Pierce
    James Buchanan
    Andrew Johnson
    Grover Cleveland
    Grover Cleveland (2nd term)
    Woodrow Wilson
    Franklin D. Roosevelt
    Harry S. Truman
    John F. Kennedy
    Lyndon B. Johnson
    Jimmy Carter
    Bill Clinton
    Barack Obama

    Console.WriteLine("Presidents still alive: {0}", 
        string.Join(",", Array.FindAll(presidents, p => 
            ((PresidentAttr)p.GetAttr()).IsAlive).Select(p => 
                ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Presidents still alive: 
        Jimmy Carter,George H. W. Bush,Bill Clinton,George W. Bush,Barack Obama 

    int maxAge = presidents.ToList().Max(p =>((PresidentAttr)p.GetAttr()).AgeTookOffice);
    var maxPAttr = (PresidentAttr)Array.Find(presidents, p => 
        ((PresidentAttr)p.GetAttr()).AgeTookOffice == maxAge).GetAttr();
    Console.WriteLine("The oldest president was {0}, a {1}, who took office at age {2}.", 
        maxPAttr.Name, maxPAttr.Party, maxPAttr.AgeTookOffice);

Output:
    The oldest president was Ronald Reagan, a Republican, who took office at age 70.

Our last example is going to demonstrate President enum with [Flags] attribute, or PresidentFlagged. Here is its definition:

C#
[Flags]
public enum PresidentFlagged : long
{
    [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
    GeorgeWashington = 0x1,
    [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
    JohnAdams = 0x2,
    [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
    ThomasJefferson = 0x4,
    ...
    [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
    BillClinton = 0x20000000000,
    [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
    GeorgeWBush = 0x40000000000,
    [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
    BarackObama = 0x80000000000,
    _MostInfluential = AbrahamLincoln | FranklinDRoosevelt | GeorgeWashington
        | ThomasJefferson | AndrewJackson | TheodoreRoosevelt,
    _DiedInOffice = WilliamHenryHarrison | ZacharyTaylor | AbrahamLincoln | JamesAGarfield
        | WilliamMcKinley | WarrenGHarding | FranklinDRoosevelt | JohnFKennedy,
    _Assassinated = AbrahamLincoln | JamesAGarfield | WilliamMcKinley | JohnFKennedy,
}

Notice that we need to assign an integer number to each enum field in the order of 2^n in order for the flag bit operation to work. Also, we predefined three combined values, _MostInfluential, _DiedInOffice and _Assassinated. I prefer to prefix the combined values with an underscore "_" so they can be differentiated from single values and are put on top of the list of all possible values when displayed in the IntelliSense (see image below).

In the example, we also need the following helper method to parse a combined enum value into individual single enum values:

C#
private static PresidentFlagged[] ParseValues(PresidentFlagged combinedValue)
{
    List<PresidentFlagged> values = new List<PresidentFlagged>();
    PresidentFlagged[] allvalues = (PresidentFlagged[])Enum.
        GetValues(typeof(PresidentFlagged));
    return Array.FindAll(allvalues, v =>
        ((combinedValue & v) == v) && !v.ToString().StartsWith("_"));
}

Notice when finding individual single values of a combined enum value, we exclude the predefined combined values that are identified by the "_" prefix. Now the fun part:

C#
    PresidentFlagged jfk = PresidentFlagged.JohnFKennedy;
    var jfkAttr = (PresidentAttr)jfk.GetAttr();
    Console.WriteLine("Was {0} died in office? {1}", jfkAttr.Name, 
        ((PresidentFlagged._DiedInOffice & jfk) == jfk));
    Console.WriteLine("Was he Assassinated? {0}", 
        ((PresidentFlagged._Assassinated & jfk) == jfk));

Output:
    Was John F. Kennedy died in office? True
    Was he Assassinated? True 

    var Influentials = ParseValues(PresidentFlagged._MostInfluential);
    Console.WriteLine("Most influential presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Influentials.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Most influential presidents:
    George Washington
    Thomas Jefferson
    Andrew Jackson
    Abraham Lincoln
    Theodore Roosevelt
    Franklin D. Roosevelt

    var myFavorites = ParseValues(PresidentFlagged.AbrahamLincoln 
        | PresidentFlagged.FranklinDRoosevelt 
        | PresidentFlagged.BillClinton | PresidentFlagged.BarackObama);
    Console.WriteLine("My favorite presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        myFavorites.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
    Console.WriteLine("Are they all Democratic? {0}", 
        myFavorites.All(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));

Output:
    My favorite presidents:
    Abraham Lincoln
    Franklin D. Roosevelt
    Bill Clinton
    Barack Obama
    Are they all Democratic? False

Conclusions

An enum pattern, in which each enumerated case is associated to an instance of a generic attribute class, is identified and implemented. It is particularly useful in situations where there is a finite number of cases and each case is characterized by a set of constants.

You can download the C# project from the link at the top of this post.

License

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


Written By
Software Developer (Senior) Pharos
United States United States
I work as a Principal Software Engineer for Pharos. I develop location-based services and applications using Microsoft .Net Technologies. I am a Microsoft Certified Application Developer (MCAD) and Microsoft Certified Solution Developer (MCSD). I have more than 20 years of experience in computer programming and software development. I'm currently interested in topics related to C#, APS.Net, SQL Server, Windows Programming, Web Programming, AJAX, Bing Maps, Google Maps, and e-Commerce.
My blog site: http://www.tofuculture.com

Comments and Discussions

 
GeneralGood article But Pin
Hiren solanki10-Sep-10 21:51
Hiren solanki10-Sep-10 21:51 
GeneralNice article Pin
wmjordan22-Nov-09 2:24
professionalwmjordan22-Nov-09 2:24 
GeneralImproved extension method(s) Pin
bcd7-Nov-09 4:04
bcd7-Nov-09 4:04 
GeneralObjects Pin
TheSebaster30-Sep-09 5:39
TheSebaster30-Sep-09 5:39 
GeneralRe: Objects Pin
Gong Liu30-Sep-09 20:16
Gong Liu30-Sep-09 20:16 
GeneralRe: Objects Pin
TheSebaster1-Oct-09 2:08
TheSebaster1-Oct-09 2:08 
GeneralRe: Objects Pin
Gong Liu1-Oct-09 6:34
Gong Liu1-Oct-09 6:34 
QuestionWhy not using object? Pin
TheSebaster29-Sep-09 3:35
TheSebaster29-Sep-09 3:35 
AnswerRe: Why not using object? Pin
Gong Liu29-Sep-09 20:51
Gong Liu29-Sep-09 20:51 
GeneralVery creative ! Pin
BillWoodruff16-Sep-09 17:38
professionalBillWoodruff16-Sep-09 17:38 
GeneralI'm loving this! Pin
Anthony Daly30-Aug-09 1:35
Anthony Daly30-Aug-09 1:35 
GeneralMy vote is 5 Pin
VMykyt3-Aug-09 0:28
VMykyt3-Aug-09 0:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.