Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / Visual Basic
Article

Serialize VB.NET objects without the events

Rate me:
Please Sign up or sign in to vote.
4.67/5 (19 votes)
26 Jan 20048 min read 269.4K   41   49
Serialize VB.NET objects that have event handlers attached to its events.

Introduction

Because of the way VB.NET implements events, when you serialize an object, its events get serialized too (because events are actually implemented using hidden multicast delegate fields). A side effect of this is that any object which handles events raised by the object being serialized will be considered part of the object graph and will be serialized too.

This can result in the following undesirable situations:

  • You end up serializing objects that you didn't expect, resulting in a larger stream.
  • If the object handling the events is not Serializable, then the serialization process will throw an exception.

There are many different ways to get around this problem, but this is the solution that I've found easiest to implement and which requires the least work when using in practice.

Background

I originally came across this problem while trying to serialize objects written in VB.NET for sending across the remoting infrastructure, but the problem applies to serializing objects in general. The root of the problem is the fact that you cannot apply the <NonSerialized> attribute to events in VB (you can in C# by using the Field: modifier). As a result, there is no simple way of telling the runtime not to serialize the event fields. Note: I'm not 100% sure, but I think the next version of VB.NET will allow this, so you won't need this workaround then.

As a better explanation, consider the following sample code:

Public Sub Main
   ' Create the objects - one that raises
   ' an event and one that handles the event
   Dim objHandler As New ObjectThatCatchesEvents ' This type is NOT 
Serializable
   Dim objRaiser As New ObjectThatRaisesEvents ' This type is Serializable

   ' Attach the event handler
   AddHandler objRaiser.NameChanged, AddressOf objHandler.NameChangedHandler

   ' try and serialize the object that raises the events
   Dim objStream As New MemoryStream
   Dim objFormatter As New Formatters.Binary.BinaryFormatter

   ' Serialization exception will be raised because
   ' objHandler is not serializable - note
   ' we did not intend to serialize objHandler
   objFormatter.Serialize(objStream, objRaiser)

End Sub

A serialization exception will be raised because objHandler is of a type that is not Serializable - even though we're trying to serialize objRaiser, not objHandler.

There are a number of ways to get around this problem. Here are a few of them:

  • Remove all event handlers before serialization:

    This will work, but how can you be sure you have removed all the event handlers? What if you are writing a class library and don't have any control over how clients handle your events? You could access the hidden event delegate (use MyEventEvent.GetInvocationList() where MyEvent is the name of your event) and remove all handlers there, but it can get tedious if you have a lot of events-especially if you want to reattach the handlers after the serialization process has finished.

  • Implement ISerializable:

    You could implement ISerializable in your class and manually determine what information is to be serialized and leave out the event fields. I've found this to be slightly tedious as you have to implement the GetObjectData method and the special constructor in your class (and all derived classes). You also have to remember to add code to these methods to serialize and deserialize all fields - especially when you add or remove a field.

  • Implement a Serialization Surrogate:

    This method effectively takes over from implementing ISerializable in your class. You could implement a generic Surrogate and use it to filter out event delegates from all objects being serialized. The problem with this approach is that it's practically impossible to use with the serialization process in the Remoting infrastructure as the built-in binary and SOAP formatters don't allow you to specify surrogates in Remoting - you'd end up having to write your own formatter like Peter Himschoot did (http://users.pandora.be/Peter.Himschoot/).

  • Implement your events in a separate class that is not serialized:

    You could separate your events into a small object and expose them to clients through a field and mark the field as <NonSerialized>. I don't particularly like this idea because it separates your code functionality across two separate classes.

  • Implement your events in a C# base class:

    Again, you pay the price of your functionality being split across two classes (even worse this time, because it's in a different language!)

  • Drop VB and use C# ;-)

    Not an easy task for VB monkeys like myself!

  • Wait for VB.NET to support the Field: modifier for attributes

    I don't have the patience!

  • Stop using events:

    No way, they're too handy. Besides, aren't events useful in an event driven architecture?

My Solution

The solution that I've come up with is a variant on implementing the ISerializable interface. To cut a long story short, it's a base class written in VB that implements ISerializable, but also knows how to get all the fields of derived objects and add them to the serialization stream without your derived class having to implement the GetObjectData method (you still have to add the special de-serialization constructor though).

Using all my brain power, I came up with an ingenious name for it: SerializableObject ;-)

Using SerializableObject

To use the serializable object, follow these steps:

  1. Create your class as normal, except inherit from SerializableObject instead of Object.
  2. Add the normal constructor (Public Sub New()).
  3. Add the protected de-serialization constructor and simply call the base class de-serialization constructor.
  4. Mark your class with the <Serializable> attribute.

Example usage in a class:

VB
<Serializable()> 
Public Class ObjectThatRaisesEvents
    Inherits SerializableObject

    ' Add the default and de-serialization constructors
    Public Sub New()
        MyBase.New()
    End Sub
    Protected Sub New( _
      ByVal info As System.Runtime.Serialization.SerializationInfo, _
      ByVal context As System.Runtime.Serialization.StreamingContext)

        MyBase.New(info, context)

    End Sub

    'Rest of class implementation....

End Class

There you go. No more problems with event handlers being serialized!

If you still need to perform some custom serialization, simply override the base class GetObjectData method, but remember to call MyBase.GetObjectData to complete the serialization.

SerializableObject also implements the IDeserializationCallback interface in order to do some post-deserialization processing (as mentioned below). If you need to be notified when the OnDeserialization method is called, simply override it, but again - remember to call MyBase.OnDeserialization if you do override it!

In the case that your object to be serialized needs to inherit from a different class (say CollectionBase or ArrayList, then you can implement the ISerializable interface and use the shared SerializableObject.SerializeObject() method to populate the SerializationInfo object. In order to deserialize your object again, you must implement the de-serialization constructor and the IDeserializeCallback interface to call the shared SerializableObject.DeSerializeObject() to repopulate your object. Please see the sample application for more details on how to do this.

How it works

As mentioned above, SerializableObject implements the ISerializable and IDeserializationCallback interfaces in order to control its own serialization. Instead of relying on the derived classes to override the GetObjectData method and serialize themselves, it uses the FormatterServices class and Reflection to get the names and values of all fields in all the derived types - even private ones. This way, the derived classes can forget about serializing every field and leave it up to the base class.

When you serialize an object, the following steps occur:

  1. SerializableObject.GetObjectData method is called by the runtime.
  2. SerializableObject calls FormatterServices.GetSerializableMembers to get all fields that are to be serialized.
  3. SerializableObject removes those fields that are of type System.Delegate as these are what events are implemented with and are causing all the trouble.
  4. SerializableObject uses reflection to get the current values of all the fields and adds them to the serialization stream.

When you deserialize an object, the following steps occur:

  1. The deserialization constructor is called on your derived object.
  2. Your derived object calls the base class deserialization constructor.
  3. SerializableObject records the information.
  4. The OnDeserialization method is called.
  5. SerializableObject calls FormatterServices.GetSerializableMembers to get all fields that are to be de-serialized.
  6. SerializableObject uses reflection to set the current values of all the serialized fields.

Grey Areas

Although I've tested this as much as possible (including letting it go into full applications) and have had no problem with it whatsoever, I must remind you that there could be bugs. I leave it up to you to assess how useful and safe this code is.

One potential grey area is my knowledge of Security in .NET (i.e. None). The use of FormatterServices requires special security privileges. I seemed to get around this problem by using the following attribute where it was needed:

<SecurityPermission(SecurityAction.Assert, _
    Assertion:=True, SerializationFormatter:=True)>

If anyone can suggest a proper way of doing this, let me know.

Another area that I'm not sure about is the lifetime of the SerializationInfo object that is passed to the de-serialzation constructor. As you can see from the implementation, I keep a reference to it between the constructor and the OnDeserialization method (this is necessecary because variable initializers are called after the de-serialization constructor and so could overwrite the de-serialized field values). If the SerializationInfo object becomes invalid somehow, the de-serialization process might not work anymore.

Points Of Interest

I'm surprised nobody has come up with this approach before (maybe they did and I just didn't look hard enough). Surely I'm not the only lazy programmer out there who looks for an easy way out (which I think this is - at least compared to the other ways around it).

Also, because this problem is caused by not being able to specify the <NonSerialized> attribute on events in VB.NET, I hope that this will change in the next version of VB.NET. I guess they missed it between framework 1.0 and 1.1. If anybody can confirm this, it would be nice.

History

  • V1.1: Article updated for minor mistakes. Changed implementation of SerializableObject to use shared methods so you can use the functionality in your own classes that can't inherit from SerializableObject. Also updated the sample project to demonstrate this capability. Again, thanks to Jay B. Harlow and Rowen McDermott for all the help and advice.
  • V1.0: Version first posted to CodeProject.
  • V0.1: The first version of SerializableObject used reflection exclusively to get all the fields and then determine which ones should be serialized. Thanks go to Jay B. Harlow for the tip about the existence of FormatterServices - it made it a lot more efficient with a few less lines of code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Canada Canada
Currently a codemonkey that codes. Soon to be a fully-fledged monkey Wink | ;)

Comments and Discussions

 
GeneralRe: Still getting the error! Pin
roshec12-Oct-04 20:03
roshec12-Oct-04 20:03 
GeneralRe: Still getting the error! Pin
Trev Hunter14-Oct-04 14:16
Trev Hunter14-Oct-04 14:16 
GeneralAnother VB Serialization Gotcha Pin
Kanangra28-Feb-04 14:32
Kanangra28-Feb-04 14:32 
GeneralRe: Another VB Serialization Gotcha Pin
Trev Hunter8-Feb-04 21:41
Trev Hunter8-Feb-04 21:41 
GeneralYet Another Alternative Pin
Kanangra23-Feb-04 17:57
Kanangra23-Feb-04 17:57 
GeneralRe: Yet Another Alternative Pin
Trev Hunter3-Feb-04 21:43
Trev Hunter3-Feb-04 21:43 
GeneralRe: Yet Another Alternative Pin
Trev Hunter4-Feb-04 7:14
Trev Hunter4-Feb-04 7:14 
GeneralAlternative method Pin
mogwai26-Jan-04 22:39
mogwai26-Jan-04 22:39 
Can't remember how or exactly where I came across this solution to the problem, but I found that it works a treat. As you've pointed out, the event handler contains a private field which gets serialized. All we have to do is skip over that field during the serialization process.

    Public Sub GetObjectData(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext) Implements System.Runtime.Serialization.ISerializable.GetObjectData<br />
<br />
        ' Serializes all public, private and public fields except the ones<br />
        ' which are the hidden fields for the eventhandlers<br />
<br />
        'Get the list of all events <br />
        Dim EvtInfos() As EventInfo = Me.GetType.GetEvents()<br />
        Dim EvtInfo As EventInfo<br />
<br />
        'Get the list of all fields<br />
        Dim FldInfos() As FieldInfo = Me.GetType.GetFields( _<br />
            BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.Public)<br />
<br />
        'Loop through each field and decides whether to serialize it or not<br />
        Dim FldInfo As FieldInfo<br />
        For Each FldInfo In FldInfos<br />
<br />
            'Find if the field is a eventhandler<br />
            Dim Found As Boolean = False<br />
            For Each EvtInfo In EvtInfos<br />
                If EvtInfo.Name + "Event" = FldInfo.Name Then<br />
                    Found = True<br />
                    Exit For<br />
                End If<br />
            Next<br />
<br />
            'If field is not an eventhandler serialize it<br />
            If Not Found Then<br />
                info.AddValue(FldInfo.Name, FldInfo.GetValue(Me))<br />
            End If<br />
<br />
        Next<br />
<br />
    End Sub<br />

GeneralRe: Alternative method Pin
Trev Hunter27-Jan-04 5:22
Trev Hunter27-Jan-04 5:22 
Generalnice idea Pin
csmba21-Jan-04 6:57
csmba21-Jan-04 6:57 
GeneralRe: nice idea Pin
Trev Hunter21-Jan-04 9:06
Trev Hunter21-Jan-04 9:06 
GeneralRe: nice idea Pin
Joe Cleland3-Feb-04 4:56
Joe Cleland3-Feb-04 4:56 
GeneralRe: nice idea Pin
Trev Hunter3-Feb-04 9:08
Trev Hunter3-Feb-04 9:08 
GeneralRe: ActiveX control IUnknown pointer Pin
Elisha Snider11-Jul-05 22:19
sussElisha Snider11-Jul-05 22:19 
GeneralRe: ActiveX control IUnknown pointer Pin
Mathias Graham11-Jul-05 22:19
sussMathias Graham11-Jul-05 22:19 
GeneralRe: ActiveX control IUnknown pointer Pin
George Dimond11-Jul-05 22:19
sussGeorge Dimond11-Jul-05 22:19 
GeneralRe: ActiveX control IUnknown pointer Pin
Daniel Flanders11-Jul-05 22:20
sussDaniel Flanders11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Benedict Corbin11-Jul-05 22:20
sussBenedict Corbin11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Danl Knoblock11-Jul-05 22:20
sussDanl Knoblock11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Matthias Graebe11-Jul-05 22:20
sussMatthias Graebe11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Danl Fleischben11-Jul-05 22:20
sussDanl Fleischben11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Christian Imhoff11-Jul-05 22:20
sussChristian Imhoff11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Jacob Flanders11-Jul-05 22:20
sussJacob Flanders11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Caleb Jackson11-Jul-05 22:20
sussCaleb Jackson11-Jul-05 22:20 
GeneralRe: ActiveX control IUnknown pointer Pin
Frans Irvin11-Jul-05 22:20
sussFrans Irvin11-Jul-05 22:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.