Thursday, October 30, 2008

Enhancements to the Enum.Parse Method

Enum.Parse Method

The .NET Framework has an Enum class that contains two Parse methods (http://msdn.microsoft.com/en-us/library/system.enum.parse.aspx). Both of these methods take a string value and attempt to convert it into an Enum value. This works very well, but it has a few shortcomings.

Shortcoming 1: Enum.Parse Returns Object

The first shortcoming is that the return value from Enum.Parse is an Object. This requires the caller to typecast the result of the Enum.Parse method to the appropriate type. For example:

<FlagsAttribute()> _
Enum
Colors
    Red = 1
    Green = 2
    Blue = 4
    Yellow = 8
End
Enum

Dim
Choice As Colors

Choice =
CType([Enum].Parse(GetType(Colors), "Red"), Colors)

To get around this shortcoming I wrote the following method that will return an enumerated value of the appropriate type.

Public Shared Function ParseEnum(Of EnumType As Structure)( _
    ByVal value As String, _
    ByVal defaultValue As EnumType) As EnumType

   
Try
  

        ' Attempt to convert string to enumeration using the Parse method
        Return CType([Enum].Parse(GetType(EnumType), value, True), EnumType)

   
Catch ex As ArgumentException

       
' Return the default value
        Return defaultValue

   
End Try

End
Function

When this method is used, no typecasting is required. For example:

Dim Choice As Colors

Choice = ParseEnum(
"Red", Colors.None)

In order to use this method, the enumeration should have some default value that can be used to indicate the string could not be successfully parsed.

Shortcoming 2: Enum.Parse is unaware of the XmlEnum value

The second shortcoming of the Enum.Parse method is it does not take the XmlEnum value into account when trying to parse the string. For example, if the Colors enumeration were defined as follows:

Public Enum Colors
    Invalid
    <Xml.Serialization.XmlEnum("R")> _
    Red
    <Xml.Serialization.XmlEnum("G")> _
    Green
    <Xml.Serialization.XmlEnum("B")> _
    Blue
End Enum

And the following code was executed:

Choice = CType([Enum].Parse(GetType(Colors), "R"), Colors)

A System.ArgumentException("Requested value 'R' was not found.") would be raised to the caller. It would be nice if the Parse method would work for both the ToString and the XmlEnum values, but it only works with the ToString value.

To get around this shortcoming I wrote the following method that will return the correct enumerated value for both the ToString and the XmlEnum values.

Public Shared Function ParseEnum(Of EnumType As Structure)( _
    ByVal value As String, _
    ByVal defaultValue As EnumType) As EnumType

   
Dim Result As EnumType
    Dim ValueFound As Boolean = False

   
' Use the standard Parse method
    Try

       
' Attempt to convert string to enumeration value
        Result = (CType([Enum].Parse(GetType(EnumType), value, True), EnumType))
        ValueFound = True

   
Catch ex As ArgumentException

       
ValueFound =
False

   
End Try

   
' If that does not work, try the XmlEnum values
    If ValueFound = False Then

       
Dim Members() As System.Reflection.FieldInfo
        Dim XmlEnumAttributes() As System.Xml.Serialization.XmlEnumAttribute


       
' Get the list of Enumeration Members
        Members = GetType(EnumType).GetFields()

       
For Each Member As System.Reflection.FieldInfo In Members

           
' Only examine Enum Members
            If Member.IsSpecialName = False AndAlso _
                Member.IsLiteral = True Then

               
' Get the XmlEnum Attributes
                XmlEnumAttributes = CType(Member.GetCustomAttributes( _
                    GetType(System.Xml.Serialization.XmlEnumAttribute), True),  _
                   
System.Xml.Serialization.XmlEnumAttribute())


               
' Check the XmlEnum attribute
                If XmlEnumAttributes.Length > 0 AndAlso _
                    String.Compare(XmlEnumAttributes(0).Name, value, True, _
                    System.Globalization.CultureInfo.InvariantCulture) = 0 Then

                   
' Found Value
                   
Result = CType(Member.GetValue(Nothing), EnumType)
                    ValueFound = True
                    Exit For

               
End If ' If XmlEnumAttributes.Length > 0 AndAlso

           
End If ' If Member.IsSpecialName = False AndAlso

       
Next Member

   
End If ' If ValueFound = False Then

   
' If the value still has not been found, use the default value
    If ValueFound = False Then
        Result = defaultValue
    End If

   
' Return enumeration value
    Return Result

End
Function

When this method is used, either the ToString or the XmlEnum value can be used. For example, both of the calls below will result in Choice being set to Colors.Red:

Dim Choice As Colors

Choice = ParseEnum(
"Red", Colors.Invalid)
Choice = ParseEnum("R", Colors.Invalid)

One possible enhancement that could be made to the ParseEnum function is adding support for parsing comma-separated, XmlEnum values for bit field enumerations (e.g. “R, G” would return the value “Colors.Red Or Colors.Green”). See the FlagsAttribute Class help topic (http://msdn.microsoft.com/en-us/library/system.flagsattribute.aspx) for more details on bit field enumerations.

Visual Studio 2008/.NET Framework 3.5 Enhancement

Visual Studio 2008/.NET Framework 3.5 adds extension methods, which allow for methods to be added to existing data types without creating new derived types. This allows us to add a TryParse method, like the one shown below, to the enumerations to provide this functionality.

<System.Runtime.CompilerServices.Extension()> _
Public
Function TryParse(Of EnumType As Structure)( _ 
    ByVal enumObject As [Enum], _ 
    ByVal value As String, _ 
    ByRef result As EnumType) As Boolean 
 
    Dim ValueFound As Boolean = False 
 
    ' Use the standard Parse method 
    Try 
 
        ' Attempt to convert string to enumeration value 
        result = (CType([Enum].Parse(GetType(EnumType), value, True), EnumType)) 
        ValueFound = True 
 
    Catch ex As ArgumentException 
 
        ValueFound = False 
 
    End Try 
 
    ' If that does not work, try the XmlEnum values 
    If ValueFound = False Then 
 
        Dim Members() As System.Reflection.FieldInfo 
        Dim XmlEnumAttributes() As System.Xml.Serialization.XmlEnumAttribute 
 
        ' Get the list of Enumeration Members 
        Members = GetType(EnumType).GetFields() 
 
        For Each Member As System.Reflection.FieldInfo In Members 
 
            ' Only examine Enum Members 
            If Member.IsSpecialName = False AndAlso
                Member.IsLiteral = True Then 
 
                ' Get the XmlEnum Attributes 
                XmlEnumAttributes = CType(Member.GetCustomAttributes( _ 
                    GetType(System.Xml.Serialization.XmlEnumAttribute), True),  _
                   
System.Xml.Serialization.XmlEnumAttribute()) 
 
                ' Check the XmlEnum attribute 
                If XmlEnumAttributes.Length > 0 AndAlso
                    String.Compare(XmlEnumAttributes(0).Name, value, True, _ 
                    System.Globalization.CultureInfo.InvariantCulture) = 0 Then 
 
                    ' Found Value 
                    result = CType(Member.GetValue(Nothing), EnumType) 
                    ValueFound = True 
                    Exit For 
 
                End If ' If XmlEnumAttributes.Length > 0 AndAlso 
 
            End If ' If Member.IsSpecialName = False AndAlso 
 
        Next Member 
 
    End If ' If ValueFound = False Then 
 
    ' Indicate if the value was successfully parsed. 
    Return ValueFound 
 
End Function

I personally like this solution the best. When this method is used, either the ToString or the XmlEnum value can be used. For example, both of the calls below will result in Choice being set to Colors.Red:

Dim Choice As Colors

Choice.TryParse(
"Red", Choice)
Choice.TryParse("R", Choice)

Ideally the TryParse method would be a Shared (or static) method, but extension methods cannot be Shared (or static).

No comments: