Serialization and Events

Andrea Zanetti
Riccardo Munisso

Downolad code from here

Introduction

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.

Serialization Graph

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

Implementing ISerializable

The implementation of ISerializable is quite easy due to the power of reflection. The basic steps of the GetObjectData are the following

  1. Retrieve all events info
  2. Retrieve all fields info
  3. 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:

  1. Stepping through the EventNameEvent fields, cashing the values (Multicast Delegates) in a collection and setting the field value to nothing
  2. Serialize the object
  3. 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

Gc.Collect()Gc.WaitForPendingFinalizers() 

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.

Conclusions

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.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

The Latest

microsoft careers

Top Careers at Microsoft

Microsoft has gained its position as one of the top companies in the world, and Microsoft careers are flourishing. This multinational company is efficiently developing popular software and computers with other consumer electronics. It is a dream come true for so many people to acquire a high paid, high-prestige job

your company's audio

4 Areas of Your Company Where Your Audio Really Matters

Your company probably relies on audio more than you realize. Whether you’re creating a spoken text message to a colleague or giving a speech, you want your audio to shine. Otherwise, you could cause avoidable friction points and potentially hurt your brand reputation. For example, let’s say you create a

chrome os developer mode

How to Turn on Chrome OS Developer Mode

Google’s Chrome OS is a popular operating system that is widely used on Chromebooks and other devices. While it is designed to be simple and user-friendly, there are times when users may want to access additional features and functionality. One way to do this is by turning on Chrome OS