Introduction
I have always believed that the best programmers are lazy programmers in the sense that they hate repetitious coding and want to find ways to automate and reduce the amount of code that must be produced. One area where this comes into practice is with fixed-value combo boxes that usually translate into values that need to be worked with in code. Most programmers end up coding various kinds of string lists and KeyValuePair
s trying to make this work, and end up doing it over and over.
The EnumFunctions module with the accompanying demo form is an attempt to automate this and to do so in a way that is language independent. By the way, the code and examples are provided in both VB.Net and C#.
Background
I usually try to write applications and products that are language and culture independent so that they can be translated to different countries and languages. This means that in practice, I do not store human-readable values (e.g. Strings) in the database but instead rely on bytes with a limited range of values, mostly using Enums in my code. Many programmers simply store the string selected in a drop-down list which is fast, but makes the application difficult to translate and can cause errors from trying to do string comparisons. In contrast, using Enums is easy and allows for type checking to be done at both compile and run time to improve the quality and reliability of the code. This article shows how to easily 'internationalize' your application to support different languages on combo boxes.
Using the Code
First, start with an Enum for the particular set of values you want to work with. I usually use Byte values as this works well for storing it in the database and a drop-down list with more than 128 items would be a bad design. You can also use 16 and 32 bit integers as well. The problem with just using the names and values from the Enum is that they don't look terribly good to end-users. A list like:
Public Enum PreferredContactMethodEnum As Byte
HomePhone = 0
WorkPhone = 1
CellPhone = 2
EMail = 3
Mail = 4
FAX = 5
Other = 6
End Enum
public enum PreferredContactMethodEnum : byte
{
HomePhone = 0,
WorkPhone = 1,
CellPhone = 2,
EMail = 3,
Mail = 4,
FAX = 5,
Other = 6
}
produces a fairly ugly set of choices in the drop-down list if we just extract the list of values and names and assign it to the ComboBox Item list. It turns out that Microsoft has already thought of this and has provided a very nice mechanism to allow more information to be provided using the DisplayAttribute. To use it you need to add a reference to System.ComponentModel.DataAnnotations
to your project. We can now prefix each item with an attribute that gives us a nice human-readable text:
<Display(Name:="Work Phone")>
WorkPhone = 1
[Display(Name:="Work Phone")]
WorkPhone = 1,
This will look a little better in our drop-down list, but we need to do some extra work to extract the display text, which is covered later in the article. However, it would be even better if we could make the list language-independent. The DisplayAttribute
supports that as well by allowing you to specify the local resources file and the name of a resource in the file:
<Display(ResourceType:=GetType(My.Resources.Resources), name:="ContactMethodWorkPhone")>
WorkPhone = 1
[Display(ResourceType:=GetType(Properties.Resources), name:="ContactMethodWorkPhone")]
WorkPhone = 1,
We can now put all of the definitions in our local Resource file which will automatically load the correct version for a specific language and culture (provided you have created them). So this is a good start, we can now create human-readable language-independent enum's in code. The next step is to load that into a ComboBox
or a DataGridViewComboBoxColumn
. The code which accompanies this article uses a set of functions in the EnumFunctions module that I have written and have included with the example code and demo. Version for both VB and C# are included.
In the form provided, I have two records, one created as a record in a Data Set Table and the other as an object that might be stored using Entity Framework. The major difference between them is that Entity Framework (and similar data stores) allow me to save and restore objects which can then have properties that translate numeric fields into Enums. The Data Set Table definition is limited to only using various sizes of integers, so we need to translate our Enum's and use a data-type that matches, but that will still support nullable values.
Our Contact record looks like this, we are keeping it very simple with the MaritalStatus
being optional and the PreferredContactMethod
being required:
Public Class Contact
Public Property Name As String
Public Property MaritalStatus As MaritalStatusEnum?
Public Property PreferredContactMethod As PreferredContactMethodEnum = PreferredContactMethodEnum.HomePhone
End Class
public class Contact
{
public string Name { get; set; }
public MaritalStatus? { get; set; }
public PreferredContactMethodEnum PreferredContactMethod = PreferredContactMethodEnum.HomePhone { get; set; }
}
Our data table record just uses bytes for the two enums with the MaritalStatus as nullable and the PreferredContactMethod
as non-nullable. We want to make sure that our enum's are translated into the proper data type otherwise our list will not display anything or our values will not 'stick' after being selected. To use the Enum functions for a data table row, the following line is included in the Form_Load and is used to populate the combo box bound to the MaritalStatus
field in the dataset:
EnumFunctions.PopulateDropdownlistFromEnum(Of Byte?, MaritalStatusEnum)(DSMaritalStatusComboBox, True)
EnumFunctions.PopulateDropdownlistFromEnum<byte?, MaritalStatusEnum>(DSMaritalStatusComboBox, True);
The generic parameters tell the function that we are using a list which allows for optional values. The MaritalStatusEnum is to be used to populate the list and the AddEmptyValue
parameter set to True tells the function to add an extra blank first line with a null value. This function reads the enum values, looks up the resource strings and populates the combo box with KeyValuePair
objects that are mapped to the DisplayMember
and the ValueMember
of the ComboBox
. For mandatory fields, it is even easier:
EnumFunctions.PopulateDropdownlistFromEnum(Of Byte, PreferredContactMethodEnum)(DSPreferredContactMethodComboBox)
EnumFunctions.PopulateDropdownlistFromEnum<byte, PreferredContactMethodEnum>(DSPreferredContactMethodComboBox);
In this case, since the field is required, no nulls are allowed, and no optional line is generated. When working with objects, it is even simpler as we can code everything without having to specify a type:
EnumFunctions.PopulateDropdownlistFromEnum(Of MaritalStatusEnum?)(EFMaritalStatusComboBox, True)
EnumFunctions.PopulateDropdownlistFromEnum(Of PreferredContactMethodEnum)(EFPreferredContactMethodComboBox)
EnumFunctions.PopulateDropdownlistFromEnum<MaritalStatusEnum?>(EFMaritalStatusComboBox, True);
EnumFunctions.PopulateDropdownlistFromEnum<PreferredContactMethodEnum>(EFPreferredContactMethodComboBox);
We can pass our Enum directly into the function and it will use that type because our Entity Framework class property has already been declared using the same Enum. Our combox must have its SelectedValue
property bound against the data field as this expects a value and uses it to select the appropriate text from the array.
The Inner Workings
The code uses Reflection to pull the values and attributes from the specified enum type, places them into an array of KeyValuePair
s and then uses that array to initialize the CombBox
control or the DataGridViewComboBoxColumn
. The DisplayMember
is set to "Key" and the ValueMember
is set to "Value".
To create the list I make use of the Enum's functions to get the list of values and the names from the list. The code is similar to the following, but the Enum is hard-coded rather than passed as a generic type:
Dim values As System.Array = [Enum].GetValues(GetType(MaritalStatusEnum))
Dim list As New List(Of [Enum])(values.Length)
For Each value As [Enum] In values
list.Add(value)
Next
System.Array values = Enum.GetValues(typeof(MaritalStatusEnum));
List <enum> list = new List>Enum>(values.Length);
foreach (Enum value in values) {
list.Add(value);
}
We can also use similar functions to get the list of names. As described above, we use of Microsoft's DisplayAttribute
class to annotate our Enum items. The trick to making use of the attributes is to use the Reflection methods to dig the attributes out of each of the Enum items. This is done with a publicly exposed function (so you can use it) that takes an Enum value and returns the DisplayAttribute
for it:
Public Function GetEnumDisplayAttribute(Of E)(value As E) As <code>DisplayAttribute</code>
public DisplayAttribute GetEnumDisplayAttribute<e>(E value)
{
System.Type type = value.GetType();
if (!type.IsEnum) {
throw new ArgumentException(String.Format("Type '{0}' is not Enum", type));
}
MemberInfo[] members = type.GetMember(value.ToString());
if (members.Length == 0) {
throw new ArgumentException(String.Format("Member '{0}' not found in type '{1}'", value, type.Name));
}
MemberInfo member = members(0);
object[] attributes = member.GetCustomAttributes(typeof(DisplayAttribute), false);
if (attributes.Length == 0) {
return null;
}
DisplayAttribute attribute = (DisplayAttribute)attributes(0);
return attribute;
}
The tricky bit from the above code was in getting a single item out of an Enum. This is accomplished by using the Reflection function GetMember
. Also, if I don't find a DisplayAttribute
, I return a null so that the calling code can deal with it by just using the Enum item name.
The DisplayAttribute
itself handles the heavy lifting of looking up the resource string. Once I have it, I just have to ask it for the related name. If there is a resource, it will return the resource string, if not, it will just return the name. This is done by the following method:
Dim da As DisplayAttribute = GetEnumDisplayAttribute(Of E)(EnumTypeValue)
If da Is Nothing Then
Return [Enum].GetName(EnumTypeValue.[GetType](), EnumTypeValue)
Else
Return da.GetDescription()
End If
DisplayAttribute da = GetEnumDisplayAttribute<e>(EnumTypeValue);
if (da == null) {
return Enum.GetName(EnumTypeValue.GetType(), EnumTypeValue);
} else {
return da.GetDescription();
}
Points of Interest
You must remember to set the modifier for the Project Resources Page Access Modifier to Public on the project that contains your Enums, if you are putting the EnumFunctions module in a separate DLL.
Researching and figuring out how attributes work has made me appreciate them and to use them liberally through-out my projects. I use attributes to annotate records for logging purposes - the attributes tell my logger which fields to compare and which to ignore. I use them to decorate my forms with additional information that can be displayed to the user while they are being loaded. I use the Entity Framework/Component attributes to add validations such as 'Required'. I would strongly recommend most developers to read up on these and to find ways to make use of the existing attributes or to create new ones as required.
I used the Telerik code translation service at http://converter.telerik.com/ to do the initial conversion from VB to C#. It is pretty cool and does about 95% of the work. The rest you have to figure out yourself.
If I ever meet the Microsoft Developer responsible for creating the concept of Attributes, I will buy them as many alcoholic beverages as they can handle!
History
V2.0 - Added checks for Nullable Enums being passed as second generic type.