Several months back I wrote an article wherein I described creating an IDE. This IDE was (and still is) pretty cool. It has the ability to take an ASP.NET .ascx file, parse it for custom controls, render the controls using alternative text, and associate property values with what I refer to as a meta-instance. In fact, that is one of the cool, geeky elements of this application; the application creates instances of these controls dynamically without having a container for the controls and can map the control text in the textbox back to an actual in-memory instance of a control that isn't defined within the code.
The magic that facilitates this capability is dynamic IL generation via the System.Reflection and System.Reflection.Emit namespaces. I will not take the time to illustrate how these work or even my motivations in designing the application in this way. For those explanations, please see the original posts (Part I and Part II).
I had to make some alterations of the code and wanted to post some insights and strategies that I employed to achieve my goals. Firstly, the IL that I spit in the previous iteration had some pretty simple and brainless methods. In other words, the property methods were simply getters and setters - they took the value and assigned it to the private field or returned the private field's value, as in the following code.
NOTE: All of the code in this article has been greatly simplified and altered to protect NDA and IP. While this code is pretty specific and not very extensible, the actual code is much more extensible and reusable.
// the getter method which is functionally equivalent to the following:
// public int Age {
// get { return _age; }
// }
//
// variables already initialized:
// tb == TypeBuilder
// pb == PropertyBuilder
// fb == FieldBuilder
//
MethodBuilder getter = tb.DefineMethod("getAge", MethodAttributes.Public, typeof(int), Type.EmptyTypes);
ILGenerator il = getter.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, fb);
il.Emit(OpCodes.Ret);
pb.SetGetMethod(getter);
Similarly, my setter method was very vanilla in that all it did was accept a value and blindly assign it to the underlying field:
// the setter method which is funcionally equivalent to the following:
// public int Age {
// set { _age = value; }
// }
//
MethodBuilder setter = tb.DefineMethod("setAge", MethodAttributes.Public, null, new Type[] { typeof(int) });
ILGenerator il = setter.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, fb);
il.Emit(OpCodes.Ret);
pb.SetSetMethod(setter);
This is all well and good - especially for properties that simply contain their assigned values. No sweat. However, what happens in the event that we need to perform validation on the values being assigned? The proposition gets trickier, because now we need to emit IL that is a tad more complicated and we have to understand how to manipulate the stack in IL and know when to push and when to pop...that, or know how to interpret IL (via ILDASM.exe) and retrofit it into our code - something that is quite easy, but fraught with a learning curve.
Due to the nature of this application, I could make some allowances and take some shortcuts. Suppose I want the following code:
public int Age{
set {
if ( isValidAge(value) )
_name = value;
else
MessageBox.Show("Invalid age: " + value);
}
}
Now I have to perform validation on the arguments, call a function, concatenate a string, pass it to the message box, etc. The ante has been raised, but it's not something that can't be overcome. Notice the isValidName() function. I don't want to have to emit a validation method in the dynamic assembly; instead, I can create a generic validation method in my source and have my dynamic assembly call back into the host assembly. This would, of course, require that we know, upon creating the dynamic method, what function to call, what parameters to supply, and how to validate the value.
For this purpose, I propose to create a class in the host assembly that performs this validation in a generic way. We could then provide the builder with the metadata necessary to call the appropriate validation function, and have it return the appropriate result. Following the pattern outlined above, we might end up with the following:
public sealed class Validators {
public static bool IsValidAge(int value) {
return ( value < 0 ) ? false : true;
}
}
Ok, very contrived, but I'm illustrating a point...and a point that we'll rectify here momentarily.
The IL necessary to generate the new setter that calls this function is substantially more involved, but we'll illustrate it with comments:
// get the pointer to the methods.
// in order to call the various methods, we need to first have the appropriate metadata so the
// emitter can effectively map the calls
//
MethodInfo methodValidator = typeof(Validators).GetMethod("IsValidAge", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new Type[] { typeof(int) }, null);
MethodInfo methodStringConcat = typeof(string).GetMethod("Concat", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new Type { typeof(string), typeof(string) }, null);
MethodInfo methodMessageBox = typeof(System.Windows.Forms.MessageBox).GetMethod("Show", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new Type[] { typeof(string) }, null);
// now we need to create the jump points to control program flow.
// Despite the simplicity of the code we're emitting, we have a conditional block
// which requires application branching and flow control.
//
Label labelInvalidProp = il.DefineLabel();
Label labelEndOfMethod = il.DefineLabel();
il.Emit(OpCodes.Ldarg_1); // .. push the value parameter
il.EmitCall(OpCodes.Call, methodValidator, null); // if ( isAgeValid(...) ) {
il.Emit(OpCodes.Brfalse_S, labelInvalidProp); //
il.Emit(OpCodes.Ldarg_0); // .. push the this parameter
il.Emit(OpCodes.Ldarg_1); // .. push the value parameter
il.Emit(OpCodes.Stfld, fb); // _field = value;
il.Emit(OpCodes.Br_S, labelEndOfMethod); // }
il.MarkLabel(labelInvalidProp); // else {
il.Emit(OpCodes.Ldstr, "Invalid age: "); // .. push the first string to concat
il.Emit(OpCodes.Ldarg_1); // .. push the value parameter to concat
il.EmitCall(OpCodes.Call, methodStringConcat, null); // .. push the result of the concatenation
il.EmitCall(OpCodes.Call, methodMessageBox, null); // MessageBox.Show(...);
il.Emit(OpCodes.Pop); // .. ignore return value
il.MarkLabel(labelEndOfMethod); // }
il.Emit(OpCodes.Ret);
This code is pretty slick, but it isn't without its problems and issues. These issues are made manifest mostly by the problem domain of the issues that I'm solving. First of all, the only mechanism that I'm enabling for changing property values is a PropertyGrid control. If I display my own message box, I steal focus from the control and it makes the application more difficult to use.
Second, this method of data validation is not very 'OO'. I have to create a validator method for each property that I want to validate that falls outside the scope of the object. What if I had another object with an Age property and I needed to validate it differently, or against a different range? Using this method, I'd have to create a separate method for each object, for each property to be validated. If I did that, I might as well forego the entire dynamic code generation approach and create hard classes with their own properties and validators...but that violates my original design. I want to keep this cleaner.
Therefore, I've come up with a solution that is substantially more flexible, leverages the functionality of the PropertyGrid control, is ultimately easier with respect to the validator methods, and the IL is cleaner. All in all, it sure seems like a win-win. In order to implement it properly, I have to go back to how the properties are created in the first place. After reading Posts I and II, you'll undoubtedly recall that I have an interface (IToolPropertyDefn) that provides a mechanism for referencing properties of the control/object. Well, in order to provide control-specific validation, I've extended the interface to support a new property called ValidationData. This class contains information regarding the method to call and the parameters needed.
internal sealed class ValidationData {
private MethodInfo _validatorMethod;
private Type[] _parameters;
private object[] _values;
private ValidationData(string methodName, Type[] parameters, object[] values) {
_validatorMethod = typeof(Validators).GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, parameters, null);
_parameters = parameters;
_values = values;
}
internal static ValidationData GetIntRangeValidator(int min, int max) {
return new ValidationData("ValidateIntRange", new Type[] { typeof(int), typeof(int), typeof(int) }, new object[] { min, max });
}
// methods and properties to return private fields
// ...Type[] GetParameters();
// ...object[] GetValues();
// ...MethodInfo ValidatorMethod;
}
public sealed class Validators {
public static void ValidateIntRange(int value, int min, int max) {
if ( value < min || value > max )
throw new ArgumentException(string.Format("Invalid value. Value must be between {0} and {1}.", min, max));
}
}
I am, of course, simplifying my code substantially for a variety of reasons mentioned above, but am still leaving the gist of it so that it's understandable. The idea is the ValidationData constructor takes a method name, parameters, and the set of values to be passed to the validator. The constructor then resolves to the actual method (found in a different class) to perform the validation. At this point, we must change the generated IL code to call this new validation method.
Note also that the validator method no longer returns a boolean value but instead throws an exception if the value fails validation. This takes advantage of the functionality built into the PropertyGrid that if an exception is thrown from the property setter the PropertyGrid will catch it and display an 'Invalid property value' error message with the description of the exception in the 'Details'. Additionally, the focus will remain on the PropertyGrid which is a good thing.
The updated IL generator then looks more reminiscent of the following where defn is the IToolPropertyDefn implementation (note: greatly simplified for clarity):
MethodInfo setter = tb.DefineMethod("set" + defn.Name, MethodAttributes.Public, null, new Type[] { defn.DataType });
ILGenerator il = setter.GetILGenerator();
if ( null != defn.ValidateData ) {
il.Emit(OpCodes.Ldarg_1); // .. push the value parameter
// .. push the other parameters
object[] values = defn.ValidateData.GetValues();
Type[] parameters = defn.ValidateData.GetParameters();
if ( null != values ) {
for ( int i = 0; i < values.Length; i++ ) {
// evaluate the type of parameters and push them onto the stack appropriately
// for simplicity, show just the 'int' variety for illustration purposes:
il.Emit(OpCodes.Ldc_I4, (int)values[i]);
}
}
il.EmitCall(OpCodes.Call, defn.ValidateData.ValidatorMethod, null);
}
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, fb);
il.Emit(OpCodes.Ret);
pb.SetSetMethod(setter);
Again, the code has been greatly simplified, but it makes for a much more extensible model.