Code Focused
Extending Enum
The Enum or enumeration is one of the fundamental constructs in the .NET Framework, serving as an easy-to-remember set of names for a series of fixed values that are logically related. It turns out that there is a surprising amount to know about the Enum construct.
The Enum or enumeration is one of the fundamental constructs in the .NET Framework, serving as an easy-to-remember set of names for a series of fixed values that are logically related, such as the days of the week, the mode options for opening a file, etc. That is all we need to know about the Enum construct, right? In truth, there is much more to know.
Few developers explore the Enum statement beyond its most basic level. Once you do, you will be surprised to see the rich functionality available with just a few lines of code. Because the Enum is such a fundamental construct, making better use of Enum will carry benefits to all areas of your application.
Have you ever wished that your enumeration entry could be more than a fixed numeric value? Are there multiple data elements you would like to associate with a specific enumeration value? Although Enum cannot be inherited, it is very useful to know that you can extend an Enum to carry additional related information for each entry. This article shows how to use custom attributes to decorate an Enum of On VB columns, each with its own Display Name and Publication Date and how to retrieve that information from the enumeration entry as needed. Using this technique, each enumeration entry can carry an unlimited number of attributes of any value type.
An Enum cannot serve as a DataSource since it is a Type and not a full class with public properties. This article demonstrates how to use the custom attribute capability to create a List() suitable for binding using a custom DisplayName attribute.
I am very pleased to bring these techniques together into a single article, particularly to benefit the Visual Basic community.
Example Enum
A list of my OnVB articles to date will serve as our example Enum as follow:
Public Enum OnVBArticles
Unknown = 0
Win32APICalls = 20
WordAsReportWriterForSilverlight4
WhatsNewInVisualBasic
ALM_BackupOnBuildPart1
ALM_BackupOnBuildPart2
CSharpFromVBDeveloperPerspective
WhenHexadecimalIsJustNotEnough
HowToGetOrientedWithANewDatabase
HowToValidateFormsWithASPNETMVC2DataAnnotations
End Enum
My recommended guidelines for creating an Enum include: always create an entry Unknown for the zero value, do not use Enum as a suffix for the name, do not have multiple entries with the same value, and of course, use clear names that represent the element's meaning.
Brevity is not a goal in this case. I explicitly set the Unknown entry to zero to emphasize its purpose, and I set the first valid entry to a value of 20 to avoid confusion between the entry's value and its position in the list when testing. Subsequent entries will take the next highest number, so the value for HowToValidateFormsWithASPNETMVC2DataAnnotations will be 28.
Enum definitions must be numeric and default to type Integer. Integer is the most efficient and is recommended in almost all cases.
Adding methods
In the same class as the Enum, I defined a series of shared methods to provide the extended functionality. Be aware that setting an Enum to a value that is not defined for the enumeration is permitted and must be accommodated. If the value is not contained within the enumeration, the ToString() method returns that value in lieu of the enumeration entry name.
Each method is fully compatible with Option Strict On. Several methods require the array of values for the enumeration. Option Strict will present a compile-time late-bound error if a method references an element of a dynamically created array as shown in Figure 1. The work-around is to return the array from a separate method that simply creates the array and returns it, as provided by the GetNames() and GetValues() methods. See Figure 1.
[Click on image for larger view.] |
Figure 1. Avoiding late binding errors with Option Strict On. |
The following twenty-one methods are defined for our sample Enum:
- AddEntry: Adds a specific integer to an enumeration entry, returns Unknown if not a valid result
- BottomEntry: Returns the first entry in the enumeration
- DateAttribute: Return any custom date attribute by specifying the entry and the name of the attribute
- DisplayName: Return the DisplayName attribute for the specified enumeration entry
- Entry: Returns the enumeration entry for the specified value or string
- GetNames: Retrieves the array of enumeration names to avoid late-binding errors with Option Strict
- GetValues: Retrieves the array of enumeration values to avoid late-binding errors with Option Strict
- IndexValue: Returns the index position within the enumeration, either by enumeration entry or integer
- IsValid: Replacement for [Enum].IsDefined that does not use reflection and thus is faster
- ItemList: Returns a List(of EnumItem) suitable for binding, optionally use DisplayName for text
- ItemName: Returns the string name of the Enum entry, either by enumeration entry or integer
- Length: Returns the number of entries in the enumeration
- MaximumEntry: Returns the highest value entry in the enumeration
- MaximumValue: Returns the highest value in the enumeration
- MinimumEntry: Returns the lowest value entry in the enumeration
- MinimumValue: Returns the lowest value in the enumeration
- NextEntry: Returns the next entry in the enumeration ignoring Unknown, optionally circling to the bottom entry if at the end of the enumeration
- PrevEntry: Returns the prior entry in the enumeration ignoring Unknown, optionally circling to the top entry if at the start of the enumeration
- PublicationDate: Return the PublicationDate attribute for the specified enumeration entry
- StringAttribute: Return any custom string attribute by specifying the entry and the name of the attribute
- SubtractEntry: Subtracts a specific integer from an enumeration entry, returns Unknown if not a valid result
Download the sample code to see all the methods but I would like to point out some interesting methods.
The Entry method uses Language Integrated Query (LINQ) to determine the matching enumeration entry for a string, rather than the [Enum].TryParse(), in order to provide a case-insensitive match.
'Return the Enum entry for the specified string
'TryParse is case-sensitive, so use LINQ instead
Public Shared Function Entry(ByVal Value As String) As OnVBArticles
Dim Result As OnVBArticles = OnVBArticles.Unknown
Dim names = GetNames()
Dim match = (From item In names
Where item.ToLower() = Value.ToLower
Select item).FirstOrDefault
If (String.IsNullOrEmpty(match) = False) Then
[Enum].TryParse(match, Result)
End If
Return Result
End Function
The IsValid() method uses Enumerable.Range to traverse the list and determine if the entry is contained within the enumeration. This is preferred to using the [Enum].IsDefined() method since that invokes reflection and is a relatively expensive call if reflection would not otherwise be needed.
'Return True if the enum value is defined in the enumeration
'Avoid IsDefined since it loads reflection and should be avoided if possible
Public Shared Function IsValid(ByVal Value As OnVBArticles) As Boolean
Dim Index = -1
Dim values = GetValues()
Try
Index = Enumerable.Range(0, values.Count).Where(Function(s) values(s) = Value).First
Catch ex As Exception
End Try
Dim Result = (Index >= 0)
Return Result
The MaximumEntry() method uses LINQ to determine the maximum value.
'Return the entry for the maximum numeric value of the Enum
Public Shared Function MaximumEntry() As OnVBArticles
Dim values = GetValues()
Dim maxValue = (From item In values Select item).Max()
Dim Result = (CType(maxValue, OnVBArticles))
Return Result
End Function
A simple EnumItem class is used to hold the enumeration names and values for binding with an override of ToString() to provide the enumeration name for the list entry's name. Optionally the DisplayName() method is used to provide a more user-friendly name for the list item.
Public Class EnumItem
Public Property Name As String
Public Property Value As Integer
Public Overrides Function ToString() As String
Return Name
End Function
End Class
Public Shared Function ItemList(ByVal IncludeUnknown As Boolean, ByVal UseDisplayName As Boolean)
As List(Of EnumItem)
Dim Result As New List(Of EnumItem)
Dim names = GetNames()
Dim values = GetValues()
For i = 0 To names.Length - 1
If (IncludeUnknown OrElse names(i).ToLower <> "unknown") Then
If (UseDisplayName) Then
Dim itemName = DisplayName(CType(values(i), OnVBArticles))
Result.Add(New EnumItem With {.Name = itemName, .Value = values(i)})
Else
Result.Add(New EnumItem With {.Name = names(i), .Value = values(i)})
End If
End If
Next
Return Result
End Function
To bind the List(Of EnumItem) to a windows forms ComboBox, we use the code:
Dim enumList = ItemList(IncludeUnknown:=False, UseDisplayName:=True)
Me.ComboBox1.DataSource = enumList
Me.ComboBox1.DisplayMember = "Name"
Me.ComboBox1.ValueMember = "Value"
Extending Enum with Custom Attributes
The IsValid() method was coded to avoid reflection, but reflection is required if custom attributes are to be used.
An enumeration entry that is decorated with both the custom DisplayName and PublicationDate attributes looks like so:
<DisplayName("Win 32 API Calls")>
<PublicationDate("4/20/2010")>
Win32APICalls = 20
The DisplayName custom attribute accepts the desired user-friendly name as a string.
<AttributeUsage(AttributeTargets.Enum Or AttributeTargets.Field)> _
Public Class DisplayName
Inherits Attribute
Dim m_DisplayName As String = String.Empty
Public Sub New(ByVal DisplayName As String)
m_DisplayName = DisplayName
End Sub
Public Overrides Function ToString() As String
Return m_DisplayName
End Function
End Class
The DisplayName() method calls the StringAttribute() method, which uses reflection to retrieve the custom attribute
Imports System.Reflection
Public Shared Function DisplayName(ByVal Value As OnVBArticles) As String
Dim Result = StringAttribute(Value, "DisplayName")
Return Result
End Function
Public Shared Function StringAttribute(ByVal Value As OnVBArticles, ByVal AttributeName As String)
As String
Dim Result = String.Empty
Dim Field As FieldInfo = GetType(OnVBArticles).GetField(Value.ToString,
BindingFlags.GetField Or BindingFlags.Public Or BindingFlags.Static)
Dim attribs = Field.GetCustomAttributes(True)
Dim dn = (From item In attribs
Where item.GetType().Name = AttributeName Select item).FirstOrDefault
If (Not IsNothing(dn)) Then Result = dn.ToString
Return Result
End Function
The PublicationDate custom attribute accepts the date as a string and returns a string to simulate a Date attribute.
<AttributeUsage(AttributeTargets.Enum Or AttributeTargets.Field)> _
Public Class PublicationDate
Inherits Attribute
Dim m_PublicationDate As Date = Nothing
Public Sub New(ByVal PublicationDate As String)
m_PublicationDate = Date.Parse(PublicationDate)
End Sub
Public Overrides Function ToString() As String
Return m_PublicationDate.ToShortDateString
End Function
End Class
The PublicationDate() method calls the DateAttribute() method which uses reflection to retrieve the custom attribute.
Imports System.Reflection
Public Shared Function PublicationDate(ByVal Value As OnVBArticles) As Nullable(Of DateTime)
Dim Result As Nullable(Of DateTime) = DateAttribute(Value, "PublicationDate")
Return Result
End Function
Public Shared Function DateAttribute(ByVal Value As OnVBArticles, ByVal AttributeName As String)
As Nullable(Of DateTime)
Dim Result As DateTime
Dim Field As FieldInfo = GetType(OnVBArticles).GetField(Value.ToString,
BindingFlags.GetField Or BindingFlags.Public Or BindingFlags.Static)
Dim attribs = Field.GetCustomAttributes(True)
Dim dn = (From item In attribs
Where item.GetType().Name = AttributeName Select item).FirstOrDefault
If (Not IsNothing(dn)) Then Result = DateTime.Parse(dn.ToString)
Return Result
End Function
Conclusion
The simple Enum is actually far from simple and can be extended with many of the behaviors of an array, converted to bind to a collection, and even carry a virtually unlimited number of custom attributes on each enumeration element. Taking advantage of the techniques demonstrated in this article can help reduce coding and also improve the efficiency of the remaining code. I invite you to use the code download as a pattern to take advantage of these techniques within your own application.
About the Author
Joe Kunk is a Microsoft MVP in Visual Basic, three-time president of the Greater Lansing User Group for .NET, and developer for Dart Container Corporation of Mason, Michigan. He's been developing software for over 30 years and has worked in the education, government, financial and manufacturing industries. Kunk's co-authored the book "Professional DevExpress ASP.NET Controls" (Wrox Programmer to Programmer, 2009). He can be reached via email at [email protected].