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
Dim objHandler As New ObjectThatCatchesEvents
Serializable
Dim objRaiser As New ObjectThatRaisesEvents
AddHandler objRaiser.NameChanged, AddressOf objHandler.NameChangedHandler
Dim objStream As New MemoryStream
Dim objFormatter As New Formatters.Binary.BinaryFormatter
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:
- Create your class as normal, except inherit from
SerializableObject
instead of Object
.
- Add the normal constructor (
Public Sub New()
).
- Add the protected de-serialization constructor and simply call the base
class de-serialization constructor.
- Mark your class with the
<Serializable>
attribute.
Example usage in a class:
<Serializable()>
Public Class ObjectThatRaisesEvents
Inherits SerializableObject
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
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:
SerializableObject.GetObjectData
method is called by the
runtime.
SerializableObject
calls
FormatterServices.GetSerializableMembers
to get all fields that
are to be serialized.
SerializableObject
removes those fields that are of type
System.Delegate
as these are what events are implemented with
and are causing all the trouble.
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:
- The deserialization constructor is called on your derived object.
- Your derived object calls the base class deserialization constructor.
SerializableObject
records the information.
- The
OnDeserialization
method is called.
SerializableObject
calls
FormatterServices.GetSerializableMembers
to get all fields that
are to be de-serialized.
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.