I thought I'd continue where I left off in my previous blog post. In this post, I will discuss cleaning up enumerations that are tied to database values.
In the past, I've created a helper method for each of my enumerations, providing helper methods that convert back and forth between enum values and their corresponding display and database values. While this has served me fairly well in the past, it's always kind of bugged me. Today I thought I'd actually sit down and figure out a better way, and hey presto, generics came to my rescue once more.
To get you up to speed, I started with this kind of pattern in the past:
public enum DemoEnumType{
Value1,
Value2,
Value3
}
public class DemoEnumHelper
{
public string GetDbValue(DemoEnumType value)
{
switch(value)
{
case Value1:
return "det1";
case Value2:
return "det2";
case Value3:
return "det3";
}
}
public string GetDisplayValue(DemoEnumType value)
{
switch(value)
{
case Value1:
return "Custom Display Value 1";
case Value2:
return "Custom Display Value 2";
case Value3:
return "Custom Display Value 3";
}
}
public DemoEnumType ParseFromDbValue(string dbValue)
{
switch(value)
{
case "det1":
return DemoEnumType.Value1;
case "det2":
return DemoEnumType.Value2;
case "det3":
return DemoEnumType.Value3;
}
}
public DemoEnumType ParseFromDisplayValue(string dispValue)
{
switch(value)
{
case "Custom Display Value 1":
return DemoEnumType.Value1;
case "Custom Display Value 2":
return DemoEnumType.Value2;
case "Custom Display Value 3":
return DemoEnumType.Value3;
}
}
}
Now I will make a small disclaimer, these are rapidly produced from an Excel spreadsheet of values that generate both the enum and the helper class, so while this excerpt doesn't necessarily follow the principles of DRY, it performs well and it's quickly generated. But that doesn't mean I'm necessarily happy with the result.
So what can I do about it? Well, first off, I'd prefer if all my values, the enum value itself, the display value and the database value were all held in a more concise manner, something like:
public enum DemoEnum
{
[DisplayValue("Custom Display Value 1")]
[DatabaseValue("det1")]
Value1,
[DisplayValue("Custom Display Value 2")]
[DatabaseValue("det2")]
Value2,
[DisplayValue("Custom Display Value 3")]
[DatabaseValue("det3")]
Value3
}
In order to do this, I've gotta create a couple of classes that inherit from the Attribute class that allows me to decorate my enum values with such features:
public interface IAttribute
{
object Value{ get; }
}
public class DisplayValue : Attribute, IAttribute
{
public object Value{ get; private set; }
public DisplayValue(object value)
{
this.Value = value;
}
}
public class DatabaseValue : Attribute, IAttribute
{
public object Value{ get; private set; }
public DatabaseValue(object value)
{
this.Value = value;
}
}
Sweet, so now I've got my enum and all my custom values in one place instead of scattered across my enum and a corresponding helper method. However, that doesn't help me retrieve the attribute values, nor does it help me get the corresponding enum value for any given attribute.
Here's where it starts getting complicated...ish. I'll start off at the bottom end, I want to be able to grab the value of one of the attributes, I'm going to write a helper method that will grab either one (or indeed any other) of the custom attributes.
We're going to need a little bit of reflection, so don't forget to add using System.Reflection; to the file containing your helper class...
public static class EnumHelper
{
public static object GetAttribute<TEnum, TAttribute>(TEnum enumValue)
{
Type t = typeof(TEnum);
FieldInfo fi = t.GetField(enumValue.ToString());
object attr = fi.GetCustomAttributes(typeof(TAttribute), false)[0];
return (IAttribute)Convert.ChangeType(attr, typeof(TAttribute));
}
public static object GetAttributeValue<TEnum, TOutput>(TEnum enumValue)
{
return GetAttribute<TEnum, TOutput>(enumValue).Value;
}
}
I can very easily use these methods to get either of my attributes and their values using:
string dispVal =
(string)EnumHelper
.GetAttributeValue<DemoEnum, DisplayValue>(DemoEnum.Value3);
or
string dbVal =
(string)EnumHelper
.GetAttributeValue<DemoEnum, DatabaseValue>(DemoEnum.Value2);
Perfect. So now what about the other way? What if I have an attribute value, and I need to parse out an enum from it? Well, even that isn't so difficult with a couple of tricks:
public static object Parse(Type enumType,
Type attributeType,
object attributeValue)
{
FieldInfo fi = enumType.GetFields();
/* We start at one because the first field in an enum
* isn't relevant to us for this purpose */
for (int i = 1; i < fi.Length; i++)
{
IAttribute attr =
(IAttribute)fi[i].GetCustomAttributes(attributeType, false)[0];
if (attributeValue.Equals(attr.Value))
return fi[i].GetValue(fi[i]);
}
return null;
}
This will allow us to parse an enum value from a specified attribute value like so:
DemoEnum enumVal = (DemoEnum)EnumHelper.Parse( typeof(DemoEnum), typeof(DatabaseValue), "det1");
Or
DemoEnum enumVal = (DemoEnum)EnumHelper.Parse( typeof(DemoEnum), typeof(DisplayValue), "Custom Display Value 1");
So now we can convert between enum values and their corresponding attributes and attribute values, the only thing left to handle is converting from one attribute to another. This can very easily be achieved by converting from the known attribute to its corresponding enumeration value, and then back to the desired attribute value:
public static object GetAttributeValue<TEnum, TMatchAttribute, TReturnAttribute>(
object matchAttributeValue)
{
TEnum obj = (TEnum)Parse(typeof(TEnum), typeof(TMatchAttribute), matchAttributeValue);
return GetAttribute<TEnum, TReturnAttribute>(obj).Value;
}
Now all we've got to do is add a couple of methods to give us some syntactic sugar for our basic attributes which we commonly use and we're done like dinner:
public static object GetDisplayValue<TEnum>(TEnum enumValue)
{
return GetAttribute<TEnum, DisplayValue>(enumValue).Value;
}
public static object GetDisplayValue<TEnum>(string databaseValue)
{
return GetAttributeValue<TEnum, DatabaseString, DisplayString<(databaseValue);
}
And I'm sure you can do the same for your database values without any fuss, so I'll leave that as an exercise for you.
You can download the example project from my public dropbox at http://dl.dropbox.com/u/3029830/Prototypes/EnumHelpers.zip
10 comments: