Update: Jeremy has fixed the english mistakes and we’ll update the article from now in the Boxerp wiki: Check it out here
This article tryes to describe the implementation of two AOP mechanisms that generate classes on runtime inheriting from base ones and adding extra functionality to them without the need for changing the base class code. We’re using this in Boxerp to make the business objects fully bindable to the UI. To understand the problem you have to understand databinding in WindowsForms at least. Let say we have a business object (the base class) like this:
1: [Serializable]
2: public class SimpleBusinessObject
3: {
4: private string _name;
5: private string _description;
6: private int _age;
7:
8: public virtual string Name
9: {
10: get
11: {
12: return _name;
13: }
14: set
15: {
16: _name = value;
17: }
18: }
19: public virtual string Description
20: {
21: get
22: {
23: return _description;
24: }
25: set
26: {
27: _description = value;
28: }
29: }
30: public virtual int Age
31: {
32: get
33: {
34: return _age;
35: }
36: set
37: {
38: _age = value;
39: }
40: }
41: public SimpleBusinessObject()
42: {
43: }
We can bind the object to the UI (say, 3 text boxes) straight away so that the UI will read the values of the object at the moment of the binding and then the object will be updated everytime the text boxes are changed. But, what happen if we update the object from code?. The UI won’t get refreshed. In order for the UI to get refreshed, the object has to implement the System.ComponentModel.INotifyPropertyChanged and throw the PropertyChanged event everytime a property is changed. So you end up writing code that has to do with the UI in your business object:
1: public virtual string Name
2: {
3: get
4: {
5: return _name;
6: }
7: set
8: {
9: _name = value;
10: if (PropertyChanged != null)
11: {
12: PropertyChanged(this, new PropertyChangedEventArgs("Name");
13: }
14: }
15: }
Well, what we’re doing with Boxerp.Client.DynamicPropertyChangedProxy, is taking the base class, and generating another class in runtime, that inherits it, implements the INotifyPropertyChanged and modify the properties setters code to throw the PropertyChanged event.
BindableWrapper myFullyBindableObject = new BindableWrapper(mySimpleBusinessObjectInstance);
In addition, we are keeping track of the changes in the business object so everytime the property is changed we save a snapshot of the object in a queue and we are able to undo, and redo changes:
myFullyBindableObject.Undo();
You don’t have to change your business object, it does not contain UI code, just business logic. Now lets deep into the implementation details as they are really complex and interesting. At the same time I’ll explain the details of the Castle.DynamicProxy2 implementation (partially). The capability of creating new types (System.Type) in runtime is performed though Reflection.Emit. You have to write CIL code, that is pretty much like an assembler and this is reasonable as the application is running and it can’t compile C# code. To write CIL code is nothing intuitive at the begining so I’ve developed the dynamic proxy by writing the desired code in C#, disassembling it with ILDASM, figuring it out and writing the CIL. Most of the time, the dissasembled is exactly what you have to write in the CIL program. In the Boxerp.Client.DynamicPropertyChanged implementation we’ve writted naived CIL like this:
1: mthdIL.Emit(OpCodes.Nop);
2: mthdIL.Emit(OpCodes.Ldarg_0);
3: mthdIL.Emit(OpCodes.Ldfld, eventHandler);
4: mthdIL.Emit(OpCodes.Ldnull);
5: mthdIL.Emit(OpCodes.Ceq);
6: mthdIL.Emit(OpCodes.Ldc_I4_0);
7: mthdIL.Emit(OpCodes.Ceq);
8: mthdIL.Emit(OpCodes.Stloc_0);
9: mthdIL.Emit(OpCodes.Ldloc_0);
10: mthdIL.Emit(OpCodes.Ret);
This is because we’re not writing a generic dynamic proxy but just a proxy that extends the base type and makes it implement the INotifyPropertyChanged (actually the ICustomNotifyPropertyChanged that inherits from the former one). However, what we are really creating is a double proxy: the first one is this and the second one is performed by Castle.DynamicProxy2 (DP2). The second overrides the object properties so that the access to them is intercepted and you can fire the events. The code of DP2 is brilliant. They don’t write CIL code directly but using an impressive API that makes the code much more clean and understable:
1: MethodEmitter getObjectData = emitter.CreateMethod("GetObjectData", typeof(void), arg1, arg2);
2: LocalReference typeLocal = getObjectData.CodeBuilder.DeclareLocal(typeof(Type));
3: getObjectData.CodeBuilder.AddStatement(
4: new AssignStatement(typeLocal, new MethodInvocationExpression(null,
5: typeof(Type).GetMethod("GetType", get_type_args),
6: new ConstReference(
7: typeof(ProxyObjectReference).AssemblyQualifiedName)
8: .ToExpression(),
9: new ConstReference(1).ToExpression(),
10: new ConstReference(0).ToExpression())));
11: getObjectData.CodeBuilder.AddStatement(
12: new ExpressionStatement(new MethodInvocationExpression(
13: arg1, typeof(SerializationInfo).GetMethod("SetType"),
14: typeLocal.ToExpression())));
DP2 is a generic proxy that allows you to apply the Interception pattern easily. You can read about that in the Castle documentation. It is very powerful and stable.
So far so good. Now there is a big issue with the dynamic types: The Serialization.
The BinaryFormatter needs to load the type that is going to be serialized and specially deserialized from its assembly. The assembly is dynamic and it is created everytime we run the application so we could not deserialized a dynamic type that was serialized on a former execution of the application. And even worse, we can’t send the object to a remote point because the remote point won’t have the dynamic assembly so I won’t be able to deserialize. We’ve copied the solution that DP2 implements in DPCP, and I think that withouth DP2 it would have been really difficult to find it out. So DPCP also makes the base class implement the ISerializable interface. The tipical and right implementation of ISerializable would be this:
1: public class MySerializableClass : ISerializable
2: {
3: // Deserilization constructor
4: protected MySerializableClass(SerializationInfo info, StreamingContext c)
5: {
6: object[] data = (object[])info.GetValue("_serializedData",
7: typeof(object[]));
8: MemberInfo[] members =
9: FormatterServices.GetSerializableMembers(this.GetType());
10: FormatterServices.PopulateObjectMembers(this, members, data);
11: }
12:
13: // the method that is invoked at the serialization
14: public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
15: {
16: MemberInfo[] serializableMembers =
17: FormatterServices.GetSerializableMembers(this.GetType());
18: object [] data = FormatterServices.GetObjectData(this,
19: serializableMembers);
20: info.AddValue("_serializedData", data);
21: }
22: }
If you implement the ISerializable interface by reading the class fields using reflection (this.GetType().GetField(…)) you won’t get the fields that are inherited from base classes because it is not possible. So this is a better way to do it. However this implememtation don’t serialize static fields, so you have to take that into account as well.
Unfortunately this implementation does not avoid the need for loading the assembly so we are still in the same issue. The work around for this, is to tell the BinaryFormatter to serialize a different class, not this one. We can relay on another class, which is not dynamic, to be serialized and deserialized, and, get it in charge of the deserialization of the dynamic class. Let’s see the implementation of that class in DPCP:
1: [Serializable]
2: public class DynamicProxyHelper : IObjectReference, ISerializable, IDeserializationCallback
3: {
4: private object _proxy;
5: private Type[] _argumentsForConstructor;
6: private object[] _valuesForConstructor;
7: private readonly SerializationInfo _info;
8: private readonly StreamingContext _context;
9: private string _baseTypeName;
10: private Type _baseT;
11:
12: public DynamicProxyHelper(SerializationInfo info, StreamingContext context)
13: {
14: _info = info;
15: _context = context;
16:
17: _argumentsForConstructor = (Type[])info.GetValue(DynamicPropertyChangedProxy.ARGUMENTS_TYPES4CONSTRUCTOR, typeof(Type[]));
18: _valuesForConstructor = (object[])info.GetValue(DynamicPropertyChangedProxy.VALUES4CONSTRUCTOR, typeof(object[]));
19: _baseTypeName = (string)info.GetValue(DynamicPropertyChangedProxy.OBJECT_BASE_TYPE, typeof(string));
20: bool isBusinessObj = (bool)info.GetValue(DynamicPropertyChangedProxy.IS_BUSINESS_OBJECT, typeof(bool));
21: _baseT = Type.GetType(_baseTypeName, true, false);
22: Type firstProxyType;
23: object[] baseMemberData = (object[])_info.GetValue(DynamicPropertyChangedProxy.SERIALIZED_DATA, typeof(object[]));
24: ProxyGenerator generator = new ProxyGenerator();
25:
26: if (isBusinessObj)
27: {
28: // the business objects have to have a default constructor
29: firstProxyType = DynamicPropertyChangedProxy.CreateBusinessObjectProxy(_baseT, _argumentsForConstructor);
30: _proxy = generator.CreateClassProxy(firstProxyType, (IInterceptor[])baseMemberData[0]);
31: }
32: else
33: {
34: // if it is not a business object then it has to be a BindableFields class which constructor has always an interceptor
35: firstProxyType = DynamicPropertyChangedProxy.CreateBindableWrapperProxy(_baseT, _argumentsForConstructor, _valuesForConstructor);
36: _proxy = generator.CreateClassProxy(firstProxyType, (IInterceptor[])baseMemberData[0], _valuesForConstructor);
37: }
38:
39:
40:
41: }
42:
43: #region IObjectReference Members
44:
45: public object GetRealObject(StreamingContext context)
46: {
47: return _proxy;
48: }
49:
50: #endregion
51:
52: #region ISerializable Members
53:
54: public void GetObjectData(SerializationInfo info, StreamingContext context)
55: {
56: // do nothing
57: }
58:
59: #endregion
60:
61: #region IDeserializationCallback Members
62:
63: public void OnDeserialization(object sender)
64: {
65: object[] baseMemberData = (object[]) _info.GetValue (DynamicPropertyChangedProxy.SERIALIZED_DATA, typeof (object[]));
66: MemberInfo[] members = FormatterServices.GetSerializableMembers (_proxy.GetType());
67: FormatterServices.PopulateObjectMembers (_proxy, members, baseMemberData);
68: }
69:
70: #endregion
71: }
In the deserialization constructor of the class, we take the necessary that to recreate a proxy, and at the end, the OnDeserialization method is called and it populates the proxy (dynamic type) with the data it sent.
The whole process is as follows:
The dynamic type implements ISerializable and its GetObjectData method its executed on the serialization. In that method we put all the required information for the deserialization into the info object and also we tell the BinaryFormatter that it should serialize the DynamicProxyHelper and not the current type (through the SerializationInfo.SetType method):
1: info.SetType(typeof(DynamicProxyHelper));
2: MemberInfo[] serializableMembers =
3: FormatterServices.GetSerializableMembers(this.GetType());
4: object[] data = FormatterServices.GetObjectData(this, serializableMembers);
5: info.AddValue(DynamicPropertyChangedProxy.SERIALIZED_DATA, data);
Then the DynamicProxyHelper is serialized. At the deserialization it is able to create again a proxy and it is able to populate the fields of the proxy. In Castle.DynamicProxy2, the helper is the Castle.DynamicProxy/Serialization/ProxyObjectReference class. The Castle.DynamicProxy/Generators/BaseProxyGenerator contains the first part of the GetObjectData implementation and the ClassProxyGenerator contains the second part. It takes into account that, if the base class implements the ISerializable, we have to call its GetObjecData method first. DPCP behaves equally.
Here you go a question to see if you have understood this:
Will the deserialization constructor of the dynamic type (proxy) be ever called?
The answer is, NO.
The dynamic type takes part of the serialization process but not in the deserialization.
This article will have future updates and explanations. The license of DPCP and the whole Boxerp project is BSD and the first release contanining all this features will be launched very soon (0.2).
CastleProject license is Apache2 and the version 1.0 RC3 was released few days ago.
Here you have the files:
Boxerp:
- DynamicPropertyChangedProxy.cs
- DataBinding/DynamicPropertyChangedProxy_Generation.cs
- DynamicProxyHelper.cs
CastleProject: