Download sample ObservableObject project
In my previous article I went over how to break up a multiuse object into logical parts. In this article I will talk about an ObservableObject: an object that notifies WPF of changes.
The ObservableObject
There are two type of members that you can bind in a WPF app: properties and methods. Requirements for binding properties is fairly straight forward: one must implement INotifyPropertyChanged. To bind a method, on the other hand, we must actually have a property of type ICommand for each method we want to bind to.
Implementing INotifyPropertyChanged simply consists of raising the PropertyChanged event every time a property changes. The problem lies in making a child class of ObservableObject to raise the event without having the developer of the child class call it himself/herself. The best solution for this problem is to use Reflection.Emit to construct a child class of the templated class and override the properties' set method to raise the event after executing the template's set method logic.
foreach (PropertyInfo property in parentType.GetProperties()) { method = parentType.GetMethod("set_" + property.Name); methodBuilder = typeBuilder.DefineMethod(method.Name, method.Attributes, method.ReturnType, method.GetParameters().Select(pi => pi.ParameterType).ToArray()); il = methodBuilder.GetILGenerator(); locAi = il.DeclareLocal(typeof(ArgIterator)); il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.EmitCall(OpCodes.Call, method, null); il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, property.Name); il.EmitCall(OpCodes.Call, parentType.GetMethod("OnPropertyChange", BindingFlags.NonPublic | BindingFlags.Instance), null); il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ret); typeBuilder.DefineMethodOverride(methodBuilder, method); }
With this in mind, adding ICommands for methods becomes just as easy: we can use the same TypeBuilder to add new properties of type ICommand for each public method.
foreach (MethodInfo mi in parentType.GetMethods(BindingFlags.Instance | BindingFlags.Public).Where(methodInfo => !methodInfo.Name.StartsWith("get_") && !methodInfo.Name.StartsWith("set_") && !methodInfo.Name.StartsWith("add_") && !methodInfo.Name.StartsWith("remove_"))) { FieldBuilder commandField = typeBuilder.DefineField("_" + mi.Name + cExtendedTypesCommandPostfix, typeof(DelegateCommand), FieldAttributes.Private); MethodBuilder commandGetMethod = typeBuilder.DefineMethod("get_" + mi.Name + cExtendedTypesCommandPostfix, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(ICommand), Type.EmptyTypes); ILGenerator commandGetMethodIL = commandGetMethod.GetILGenerator(); var commandNullLabel = commandGetMethodIL.DefineLabel(); var defaultLabel = commandGetMethodIL.DefineLabel(); commandGetMethodIL.Emit(OpCodes.Nop); commandGetMethodIL.Emit(OpCodes.Ldarg_0); commandGetMethodIL.Emit(OpCodes.Ldfld, commandField); commandGetMethodIL.Emit(OpCodes.Ldnull); commandGetMethodIL.Emit(OpCodes.Ceq); commandGetMethodIL.Emit(OpCodes.Brfalse, commandNullLabel); commandGetMethodIL.Emit(OpCodes.Ldarg_0); commandGetMethodIL.Emit(OpCodes.Ldarg_0); commandGetMethodIL.Emit(OpCodes.Ldftn, mi); commandGetMethodIL.Emit(OpCodes.Newobj, typeof(Action).GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })); commandGetMethodIL.Emit(OpCodes.Newobj, typeof(DelegateCommand).GetConstructor(new Type[] { typeof(Action) })); commandGetMethodIL.Emit(OpCodes.Stfld, commandField); commandGetMethodIL.MarkLabel(commandNullLabel); commandGetMethodIL.Emit(OpCodes.Ldarg_0); commandGetMethodIL.Emit(OpCodes.Ldfld, commandField); commandGetMethodIL.Emit(OpCodes.Ret); PropertyBuilder commandProperty = typeBuilder.DefineProperty(mi.Name + cExtendedTypesCommandPostfix, PropertyAttributes.HasDefault, typeof(ICommand), null); commandProperty.SetGetMethod(commandGetMethod); }
Now for all of this to work we need to make sure two things happen: first the consumer of the template object should actually get an instance of the derived type. And second, since the consumer is actually using a child class of the type he/she declared, we must ensure that reflection and typing compatibility is achieved. To do this, we must first of all provide a means of instantiating a new object (with a New() static method) and we must also override the GetType() of the child type we have created to return an instance of the template's Type class.
MethodInfo method = typeof(object).GetMethod("GetType", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { }, null); MethodBuilder methodBuilder = typeBuilder.DefineMethod(method.Name, method.Attributes, typeof(Type), method.GetParameters().Select(pi => pi.ParameterType).ToArray()); ILGenerator il = methodBuilder.GetILGenerator(); LocalBuilder locAi = il.DeclareLocal(typeof(ArgIterator)); il.Emit(OpCodes.Ldtoken, parentType); il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static), null); il.Emit(OpCodes.Stloc_0); il.Emit(OpCodes.Ldloc_0); il.Emit(OpCodes.Ret);
public static T New() { return ((T)Activator.CreateInstance(CreateOverridenType(typeof(T)))); }
In my next article, I'll demonstrate how to create an enterprise level DatabaseObject and DatabaseCollection classes.