Serialization is a very powerful characteristic of the .NET Framework. Serialization make the task of object persistency very easy but there are some point to be aware when performing serialization.
Our team used serialization for a short while and found out that the following issues can lead to problems.
The first point to consider is versioning. Serialized and persistent object can become easily “obsolete” as the class of the object changes. The serialized object is very “fragile” in respect of class modifications, for example member types cannot change otherwise deserialization process will raise an exception.
The second point is a problem with serialization and events. This is the subject of the present work.
Serialization and events
Suppose you have a class which exposes an event. You build an object (called ser1) instancing that class. Then you build an object (called lst1) which respond to the events risen by object ser1.
When you serialize object ser1 the .NET Framework tries to serialize also object lst1.
This situation is very dangerous.
First, not all classes are serializable, so the most common situation is a runtime exception. This exception will appear only when the serialized object has some listener, so serialization can succeed or not “randomly”. On the second hand, even if all classes are serializable you will probably serialize objects you don’t want to, that is all the listeners to your object’s events.
Events handling in .NET
To understand why .NET framework tries to serialize also the listeners to an object’s events we should try to understand how .NET handles events. We know that multicast delegates looks like events, actually they are event.
To explore event handling we just build a very simple class and look at MSIL code generated by VB compiler
Public Class Serialize1 Public Event TestEvent(ByVal sender As Serialize1, ByVal args As Object) Public Sub GenerateEvent() RaiseEvent TestEvent(Me, Nothing) End SubEnd Class
We can notice the following:
- For each event MSIL declares a class called EventNameEventHandler which inherits from System.MulticastDelegate
.class auto ansi sealed nested public TestEventEventHandler extends [mscorlib]System.MulticastDelegate
- For each event MSIL declares a private field called EventNameEvent with a type of the EventNameEventHandler class.
.field private class SerialTests.Serialize1/TestEventEventHandler TestEventEvent
- For each event MSIL declares two functions called remove_EventName and add_EventName.
.method public newslot specialname virtual instance void remove_TestEvent(class SerialTests.Serialize1/TestEventEventHandler obj) cil managed synchronized
.method public newslot specialname virtual instance void add_TestEvent(class SerialTests.Serialize1/TestEventEventHandler obj) cil managed synchronized
The existence of a private field called EventNameEvent can also be noticed trying to declare a field called EventNameEvent. Compiler signals the following error:
“Private Dim TestEventEvent As TestEventEventHandler” implicitly declared for “Public Event TestEvent(sender As Serialize1, args As Object)” in class “Serialize1”.
The previous points mean that the class will keep track of all its listeners through a multicast delegate called EventNameEventHandler. All listeners should then “subscribe” to the notification list for the event calling add_EventName when “connecting” to the event exposed by the class.
An object becomes a listener to the events of another object declaring a variable “with events”, through “Handles” key word or through “AddHandler” instruction.
We can see the MSIL code for this cases building a simple listener class as follows.
Public Class Listener1 Private WithEvents mySer1 As Serialize1 Private myName As String Public Sub New(ByVal Ser As Serialize1, ByVal name As String) mySer1 = Ser myName = name AddHandler mySer1.TestEvent, AddressOf mySer1_TestEvent End Sub Private Sub mySer1_TestEvent(ByVal sender As SerialTests.Serialize1, _ ByVal args As Object) Console.WriteLine(myName + " Listener1.mySer1_TestEvent") End Sub Private Sub mySer1_TestEvent2(ByVal sender As SerialTests.Serialize1, _ ByVal args As Object) Handles mySer1.TestEvent Console.WriteLine(myName + " Listener1.mySer1_TestEvent") End SubEnd Class
The AddHandler instruction leads to the following MSIL code
IL_001f: ldvirtftn instance void SerialTests.Listener1::mySer1_TestEvent(class SerialTests.Serialize1, object)
IL_0025: newobj instance void SerialTests.Serialize1/TestEventEventHandler::.ctor(object, native int)
IL_002a: callvirt instance void SerialTests.Serialize1::add_TestEvent(class SerialTests.Serialize1/TestEventEventHandler)
As we can see there is a call to the add_EventName method.When a field is declared WithEvents and there is a procedure with a “Handles” key word VB compiler add MSIL code in the set_FieldName method calling remove_EventName and add_EventName.
What we discovered explains why .NET framework tries to serialize all the listener of the events of an object.
When building the serialization graph of objects referenced by an object .NET framework goes though all fields of the object. In this operation .NET framework considers also the multicast delegate which keeps a reference to all listeners to the object’s events.
This way all the listener of the object events are added to the serialization graph.
Solution to the serialization of object that exposes events
The serialization process should not consider all listener to an object’s events in the serialization graph. This can be achieved in many ways.
- You could clone the object before serialization. The new object will not have any listener and this will make the serialize method to work properly. This is very convenient if the object implements ICloneable interface and if the cloning process does not use serialization!.
- You could implement ISerializable and a personal serialization, serializing all fields but not the EventNameEvent ones
- You could detach events from the object before serialization and then reattach them to the object.
In the following sections we will explore the last two points
The implementation of ISerializable is quite easy due to the power of reflection. The basic steps of the GetObjectData are the following
- Retrieve all events info
- Retrieve all fields info
- Iterate through the fields info and add the value to the SerializationInfo object if the field is not one of the EventNameEvent fields. This is done using the name of the field. The approach is correct as we saw that it is not possible to declare a field with the same name of a EventNameEvent.
Private Sub GetObjectData(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) Implements Serializable.GetObjectData ' Serializes all public, private and public fields except the one ' which are the hidden fields for the eventhandlers ' Get the list of all events Dim EvtInfos() As EventInfo = Me.GetType.GetEvents() Dim EvtInfo As EventInfo ' Get the list of all fields Dim FldInfos() As FieldInfo = Me.GetType.GetFields( _ BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.Public) ' Loops in each field and decides wether to serialize it or not Dim FldInfo As FieldInfo For Each FldInfo In FldInfos ' Finds if the field is a eventhandler Dim Found As Boolean = False For Each EvtInfo In EvtInfos If EvtInfo.Name + "Event" = FldInfo.Name Then Found = True Exit For End If Next ' If field is not an eventhandler serializes it If Not Found Then info.AddValue(FldInfo.Name, FldInfo.GetValue(Me)) End If Next End Sub
The opposite is done in the special Sub New declared for deserialization
Private Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) ' Get the list of the events Dim EvtInfos() As EventInfo = Me.GetType.GetEvents() Dim EvtInfo As EventInfo ' Get the list of the fields Dim FldInfos() As FieldInfo = Me.GetType.GetFields( _ BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.Public) ' Loops in each field and decideds wether to deserialize it or not Dim FldInfo As FieldInfo For Each FldInfo In FldInfos ' Finds if the field is a eventhandler Dim Found As Boolean = False For Each EvtInfo In EvtInfos If EvtInfo.Name + "Event" = FldInfo.Name Then Found = True Exit For End If Next ' If field is not a eventhandler deserializes it If Not Found Then FldInfo.SetValue(Me, info.GetValue(FldInfo.Name, FldInfo.FieldType)) End If Next End Sub
Using reflection makes it possible to build some standard procedures and to call them in the GetObjectData sub and in the sub new. This procedures could for example inspect if the field has the attribute < NonSerialized()>.
Detaching and Attaching Events
An interesting solution to the serialization problem we are discussing is the possibility to “detach” events from an object.
This can be done setting to nothing the EventNameEvent fields. The technique applied to serialization is the following:
- Stepping through the EventNameEvent fields, cashing the values (Multicast Delegates) in a collection and setting the field value to nothing
- Serialize the object
- Stepping through the EventNameEvent fields and restoring the original values.
' Detaches Events before serialization Dim Evt As New Collection() Dim EvtInfo() As Reflection.EventInfo = ser1.GetType.GetEvents() Dim i As Integer 'Finds all events For i = 0 To EvtInfo.GetLength(0) - 1 Dim FldName As String = EvtInfo(i).Name + "Event" ' Get the hidden field (MulticastDelegate) Dim FldInfo As Reflection.FieldInfo = ser1.GetType.GetField(FldName, _ Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance) ' Save the Delegate in a collection Evt.Add(FldInfo.GetValue(ser1), FldName) ' Sets the field value to nothing FldInfo.SetValue(ser1, Nothing) Next ' It is possible to serialize the object without problems fs1 = New FileStream("ser1", FileMode.Create) f1 = New BinaryFormatter(Nothing, New StreamingContext(StreamingContextStates.File)) f1.Serialize(fs1, ser1) fs1.Close() ' Attaches Events after serialization For i = 0 To EvtInfo.GetLength(0) - 1 Dim FldName As String = EvtInfo(i).Name + "Event" Dim FldInfo As Reflection.FieldInfo = ser1.GetType.GetField(FldName, _ Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance) FldInfo.SetValue(ser1, Evt.Item(FldName)) Next
Notes on the previous techniques
Comparing the two solutions explained we should notice that the first one is much better in terms of design.
The serialization logic is “encapsulated” inside the object and the object “solves its own problems” by itself. Any other procedure that has to serialize the object does not need to guess whether the object will have problems with events.
The second solution implies that the serializer procedure knows that the object can have problems due to events and detaches events before serializing.
On the other hand, as far as performance is concerned, the second solution should be much better.
More on Events
The fact that an objects that exposes events keeps a reference to all the listener has major consequences.
The most important one is that the listener should remove event handler to go out of scope.
Consider the following code:
Dim ser1 As New Serialize1()Dim lst1 As New Listener1(ser1, "1")' Test that the object lst1 responds correctly to event ser1.TestEventser1.GenerateEvent()
After the execution of the following code we will see that lst1 is still responding to the event raised by ser1
lst1 = nothingser1.GenerateEvent()
What is even worse is that the following code will not destroy object lst1
Another problem connected with this behavior is that all objects implementing IDisposable should also detach event handlers in the Dispose procedure. If this is not the case the event handler procedures will respond to event also if the object has been disposed, probably referring to objects that are no more alive. This is particularly true with forms as forms dispose all the controls in the Dispose sub.
As a conclusion we should say that all objects which declares variables WithEvents should implement IDisposable and should detach the event handlers in IDisposable.Dispose.
We saw that the serialization process can lead to problems when serializing objects that expose events.
We described two techniques to avoid this problem, the first one implementing a particular personalized serialization, the other one detaching events before serializing objects.
As a last point we reported some considerations on interaction of events and object lifecycle, stating that each object which have a variable declared WithEvents should implement IDisposable and detach event handlers in IDisposable.Dispose procedure.
Andrea Zanetti ([email protected]) and Riccardo Munisso ([email protected]) work for Cimolai one of the major european structural steelwork fabricator. They are developing personalized application with SQL Server and .NET. The solutions range from logistic to CAM system.