Title: Load enums at runtime to understand them and their attributes in C#
This example shows how you can load enums from a file at runtime. It demonstrates several useful techniques including:
- Defining custom attributes
- Defining attributes with multiple parameters
- Creating attributes at both design time and run time
- Loading code at runtime
- Using reflection to get information about enums
- Using reflection to get information attributes
Creating Custom Attributes
Custom attributes ate simply classes that are derived from the Attribute class. They can also optionally be decorated with the AttributeUsage attribute to restrict the kinds of things to which they can be applied. For example, the following code defines a Salary attribute.
[System.AttributeUsage(System.AttributeTargets.Field)]
public class Salary : System.Attribute
{
public float Value;
public Salary(float value)
{
Value = value;
}
}
The AttributeUsage attribute indicates that the Salary attribute can only be applied to fields. The values defined by an enum are fields, so you can apply this attribute to them.
For example, the following code defines a simple enum that demonstrates the Salary attribute.
enum Jobs
{
[Salary(12)] DishWasher,
[Salary(14)] Waiter,
Manager,
}
Notice that the attribute is not required, and the enum omits it for the Manager value.
Defining Attributes at Runtime
The example program's code contains definitions for the following three attribute classes.
- JobTitle(string value)
- Salary(float value)
- IsExempt(bool value)
The test text file test_enum.cs uses the following code to define a fourth attribute class, CustomerType.
[System.AttributeUsage(System.AttributeTargets.Field)]
public class CustomerType : System.Attribute
{
public string TypeName;
public float Discount;
public CustomerType(string type_name, float discount)
{
TypeName = type_name;
Discount = discount;
}
}
The program loads this attribute type at runtime when you load the file. (More about that later.)
Notice that this attribute's constructor takes two parameters: type_name, which is a string, and discount, which is a float.
The test_enum.cs file uses the attributes in the following code, which defines two enumerations.
// Enums.
enum Employees
{
[IsExempt(false)]
Clerk,
[JobTitle("Super"), IsExempt(false)]
Supervisor,
[JobTitle("Manager"), Salary(84000), IsExempt(true)]
Manager,
[JobTitle("Admin"), Salary(72000), IsExempt(true)]
Administrator,
}
enum Customers
{
[CustomerType("Retail", 0)]
Customer,
[CustomerType("Frequent", 10)]
FrequentBuyer,
[CustomerType("Wholesale", 40)]
Wholesale,
}
There are several interesting things to note about this code. First, the Employees values do not all use every attribute. The Clerk value does not use JobTitle or Salary, and Supervisor does not use Salary.
Second, the Customers values use the two-parameter CustomerType attributes.
Finally, note that only the CustomerType attribute is defined inside this file; the other attributes are defined inside the main example program.
Loading Code at Runtime
When you open the example's File menu and select Open Enum File, the program executes the following code.
// Load and compile an enum file.
private void mnuFileOpenEnumFile_Click(object sender, EventArgs e)
{
if (ofdEnum.ShowDialog() != DialogResult.OK) return;
trvEnums.Nodes.Clear();
// Use the C# DOM provider.
CodeDomProvider code_provider =
CodeDomProvider.CreateProvider("C#");
// Generate a non-executable assembly in memory.
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateInMemory = true;
parameters.GenerateExecutable = false;
// Get information about this assembly.
// (So the enum can use attributes defined
// in Attributes.cs.)
string exe_name = Assembly.GetEntryAssembly().Location;
parameters.ReferencedAssemblies.Add(exe_name);
// Load the enum file's text placed
// inside our namespace.
StringBuilder sb = new StringBuilder();
sb.AppendLine("namespace howto_load_runtime_enum");
sb.AppendLine("{");
sb.AppendLine(File.ReadAllText(ofdEnum.FileName));
sb.AppendLine("}");
// Compile the code.
CompilerResults results =
code_provider.CompileAssemblyFromSource(parameters,
sb.ToString());
// If there are errors, display them.
if (results.Errors.Count > 0)
{
trvEnums.Nodes.Add("Error compiling the expression.");
foreach (CompilerError compiler_error in results.Errors)
{
trvEnums.Nodes.Add(compiler_error.ErrorText);
}
return;
}
...
This code displays the ofdEnum OpenFileDialog to let you select a code file. If you cancel the dialog, then the code returns without doing anything else.
If you select a file, the code clears the trvEnums TreeView control and then loads the code file.
The code first creates a CodeDomProvider to work with the C# language. The languages that are available depend on which ones are installed on your system and may include C#, Visual Basic, C++, and JScript. In theory a compiler vendor could provide support for other languages.
Next the program defines compiler parameters to tell the code provider to generate compiled code in memory (as opposed to on disk) and to not create an executable (we are only loading a code fragment).
The code then adds the example program's assembly to the compiler parameters. That allows the code fragment that we will load to use things that are defined by the example program's code. In particular that allows the fragment to use the attribute classes defined by the example.
Now the program creates a string holding the code that we want to compile. To do that it creates a StringBuilder. It reads the selected file into the StringBuilder, wrapping it in a namespace block so the result looks like this.
namespace howto_load_runtime_enum
{
...file contents...
}
The namespace howto_load_runtime_enum is the one that contains the example program's code. If the code fragment were not in the same namespace as the example program, then it could not use the attributes defined by the example.
Next the program calls the code provider's CompileAssemblyFromSource method to compile the code string that we built. If there are errors, the code loops through them and adds them to the TreeView control. (Try changing the namespace placed inside the StringBuilder to see some errors.)
If the program gets this far successfully, the rest of the menu item's event handler uses reflection to examine the enums.
Using Reflection
Before I show you the code that examines the enums, I want to describe the basic approach.
The code fragment in the file test_enum.cs defines the CustomerType attribute and the two enums Employees and Customers. The general approach to learning about the enums is to loop through the types defined by the code fragment and examine those that are enums.
Each enum defines several values. Those are literal values, which means they are defined at design time. For each enum, the code loops through the enum's fields looking for those that are literal values.
Each literal value represents one of the values defined by the enum. For example, the Employees enum defines the literals Clerk, Supervisor, Manager, and Administrator.
Each literal value can have custom attributes. The code loops through those attributes to study them.
An object's attribute is an instance of an attribute class. For example, the Clerk value has as an attribute a IsExempt object that was created with parameter false. To understand a value's attributes, the program must get the values stored in those attribute objects. In the Clerk example, the code needs to get the false that is stored inside the IsExempt field.
Note that the values inside the attribute objects are stored in fields. For example, here's the CustomerType attribute class again.
[System.AttributeUsage(System.AttributeTargets.Field)]
public class CustomerType : System.Attribute
{
public string TypeName;
public float Discount;
public CustomerType(string type_name, float discount)
{
TypeName = type_name;
Discount = discount;
}
}
The values TypeName and Discount are stored in public fields within the class.
To get the attribute object's values, the code uses reflection to loop through the attribute object's fields. (If the values were stored as properties, you would loop through the object's properties instead of its fields.)
Finally the program can look at the field information to get the attribute's values.
To summarize, the code uses the following approach to learn about the file's enums.
- For each object defined by the code fragment:
- If the object is an enum:
- Add the enum's name to the TreeView
- For each field defined by the enum:
- If the field is a literal:
- Add the field's name to the TreeView.
- For each custom attribute held by the literal field:
- Loop through the attribute's fields. For each attribute field:
- Add the field's name and value to the attribute's parameter list.
- Add the attribute name and parameter list to the TreeView.
The following code shows how the program performs those steps.
...
// See what enumerations there are.
foreach (Type type in results.CompiledAssembly.GetTypes())
{
// Only consider types that are enums.
if (type.IsEnum)
{
// Add a TreeView root node for the enum.
TreeNode enum_node = trvEnums.Nodes.Add(type.Name);
// Enumerate the Enum's fields (values).
FieldInfo[] enum_value_infos = type.GetFields();
// Loop over the enum's values.
foreach (FieldInfo value_info in enum_value_infos)
{
// See if this is a literal value (set at compile time).
if (value_info.IsLiteral)
{
// Add it to the Enum's root node.
TreeNode value_node =
enum_node.Nodes.Add(
value_info.Name + " = " +
((int)value_info.GetValue(null)).ToString());
// Loop through the value's attributes.
foreach (Attribute attr in value_info.GetCustomAttributes(false))
{
// Start with the attribute's name.
string attr_text = attr.GetType().Name + "(";
// Add the attribute's fields.
foreach (FieldInfo field_info in attr.GetType().GetFields())
{
attr_text +=
field_info.Name + " = " +
field_info.GetValue(attr).ToString() +
", ";
}
// If we have fields, remove the trailing ", ".
if (attr_text.Length > 0)
attr_text = attr_text.Substring(0, attr_text.Length - 2);
attr_text += ")";
// Add this to the TreeView.
value_node.Nodes.Add(attr_text);
}
}
}
}
}
trvEnums.ExpandAll();
}
This code is somewhat confusing but only because it's not obvious which reflection methods you need to use to perform the steps given above. Walk through the code while referring to those steps to see how it works.
Conclusion
The chances that you'll need to do exactly this are relatively small, but this example provides a useful exercise in reflection. It also demonstrates a few important techniques such as creating custom attribute classes and creating attribute classes that take multiple parameters. One of the less obvious results of this example is that the code loaded at runtime can use classes and other items that are defined by the example program. That's particularly useful if you want to load scripts from text files and then have them interact with the program's objects.
Download the example to experiment with it and to see additional details.
|