Saturday, May 15, 2004
« Remoting, web services, SOA and trust bo... | Main | Services, Objects and People, Oh my! »

In .NET 1.x there's a problem serializing objects that raise events when those events are handled by a non-serializable object (like a Windows Form). In .NET 2.0 there's at least one workaround in the form of Event Accessors.

The issue in question is as follows.

I have a serializable object, say Customer. It raises an event, say NameChanged. A Windows Form handles that event, which means that behind the scenes there's a delegate reference from the Customer object to the Form. This delegate that is behind the event is called a backing field. It is the field that backs up the event and actually makes it work.

When you try to serialize the Customer object using the BinaryFormatter or SoapFormatter, the serialization automatically attempts to serialize any objects referenced by Customer - including the Windows Form. Of course Windows Form objects are not serializable, so serialization fails and throws a runtime exception.

Normal variables can be marked with the NonSerialized attribute to tell the serializer to ignore that variable during serialization. Unfortunately, an event is not a normal variable. We don't actually want to prevent the event from being serialized, we want to prevent the target of the event delegate (the Windows Form in this example) from being serialized. The NonSerialized attribute can't be applied to targets of delegates, and so we have a problem.

In C# it is possible to use the field: target on an attribute to tell the compiler to apply the attribute to the backing field rather than the actual variable. This means we can use [field: NonSerialized()] to declare an event, which will cause the backing delegate field to be marked with the NonSerialized attribute. This is a bit of a hack, but does provide a solution to the problem. Unfortunately VB.NET doesn't support the field: target for attributes, so VB.NET doesn't have a solution to the problem in .NET 1.x.

Though there is a solution in C#, the solution is not an elegant solution, so in both VB.NET and C# we really need a better answer. I have spent a lot of time talking with the folks in charge of the VB compiler, and hope they come up with an elegant solution for VB 2005. In the meantime, here’s an answer that will work in either language in .NET 2.0.

In VB 2005 we’ll have the ability to declare an event using a “long form” using a concept called an Event Accessor. Rather than declaring an event using one of the normal options like:

Public Event NameChanged()

or

Public Event NameChanged As EventHandler

where the backing field is managed automatically, you’ll be able to declare an event in a way that you manage the backing field:

  Public Custom Event NameChanged As EventHandler

    AddHandler(ByVal value As EventHandler)

    End AddHandler

    RemoveHandler(ByVal value As EventHandler)

    End RemoveHandler

    RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)

    End RaiseEvent

  End Event

In this model we have direct control over management of each event target. When some code wants to handle our event, the AddHandler block is invoked. When they detach from our event the RemoveHandler block is invoked. When we raise the event (using the normal RaiseEvent keyword), the RaiseEvent block is invoked.

This means we can declare our backing field to be NonSerialized if we so desire. Better yet, we can have two backing fields – one for targets that can be serialized, and another for targets that can’t be serialized:

  _

  Private mNonSerializableHandlers As New Generic.List(Of EventHandler)

  Private mSerializableHandlers As New Generic.List(Of EventHandler)

Then we can look at the type of the target (the object handling our event) and see if it is serializable or not, and put it in the appropriate list:

  Public Custom Event NameChanged As EventHandler

    AddHandler(ByVal value As EventHandler)

      If value.Target.GetType.IsSerializable Then

        mSerializableHandlers.Add(value)

      Else

        If mNonSerializableHandlers Is Nothing Then

          mNonSerializableHandlers = _

            New Generic.List(Of EventHandler)()

        End If

        mNonSerializableHandlers.Add(value)

      End If

    End AddHandler

    RemoveHandler(ByVal value As EventHandler)

      If value.Target.GetType.IsSerializable Then

        mSerializableHandlers.Remove(value)

      Else

        mNonSerializableHandlers.Remove(value)

      End If

    End RemoveHandler

    RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)

      For Each item As EventHandler In mNonSerializableHandlers

        item.Invoke(sender, e)

      Next

      For Each item As EventHandler In mSerializableHandlers

        item.Invoke(sender, e)

      Next

    End RaiseEvent

  End Event

The end result is that we have declared an event that doesn’t cause problems with serialization, even if the target of the event isn’t serializable.

This is better than today’s C# solution with the field: target on the attribute, because we maintain events for serializable target objects, and only block serialization of target objects that can’t be serialized.

Monday, May 17, 2004 9:16:23 AM (Central Standard Time, UTC-06:00)
Rock:

This is great. I have to admit that I hadn't thought about the issue, and therefore it's solution. I'm thrilled that there is a current version solution (/hack) and that there appears to be a much stronger and elegant real solution in the future.......

Thanks for the heads up.

Later, Shaffer
Mike Shaffer
Thursday, May 20, 2004 8:57:59 PM (Central Standard Time, UTC-06:00)
Today's C# solution is actually exactly like the future VB.NET version, and the [field:...] solution is really a lack of knowledge of very specific C# syntax.


private EventHandler myHandler;

public event EventHandler MyEvent {
add { myHandler = (EventHandler) Delegate.Combine( myHandler, value ); }
remove { myHandler = (EventHandler) Delegate.Remove( myHandler, value );
}

Plus, your VB.NET version would probably be better off duplicating the C# code above so you don't have to keep a list yourself and do any iteration yourself.

This way you can use the System.ComponentModel.EventHandlerList to save on memory if you have a lot of events and a lot of instances of objects with events (this is what windows forms uses so it doesn't need to have a delegate reference for all 50+ events per object).
Tuesday, November 30, 2004 9:44:48 AM (Central Standard Time, UTC-06:00)
Cheers Rockford, just the solution I've been looking for... I can finally do away with the crazy, roundabout, innefficent way that I did it in the past (http://www.codeproject.com/vb/net/serializevbclasses.asp).
Trev Hunter
Thursday, September 29, 2005 7:09:37 AM (Central Standard Time, UTC-06:00)
Should the NonSerialized attribute be applied to the mNonSerializableHandlers member? Until I used NonSerialized in my .NET 2.0 project I received runtime serialization errors.
Bryant
Sunday, July 30, 2006 9:44:40 AM (Central Standard Time, UTC-06:00)
In C# it is possible to use the field: target on an attribute to tell the compiler to apply the attribute to the backing field rather than the actual variable.
Sunday, July 30, 2006 2:09:13 PM (Central Standard Time, UTC-06:00)
Yes, that is true. That is what I did in the .NET 1.0 version of my books. But if you consider what [field: xyz] does, it feels very much like a hack - the worst kind of syntactic sugar.

Now I'm not one to criticize syntactic sugar - I WANT the compiler to do a lot of work for me. That's part of the reason I really like VB, though C# is slowly coming along in this regard. I'm happy to give up some control to get more productive results.

But the field: hack really feels like a hack, not like a solidly thought out productivity feature. So I was extremely happy to find the custom event declaration syntax. Though it requires extra code, this answer most certainly does not feel like a hack.
Friday, September 01, 2006 4:02:13 AM (Central Standard Time, UTC-06:00)
I ran into this problem when trying to use the SQL session state with ASP.NET 2.0. My objects have events that are fired when their properties change and my web controls respond to these events. Unfortunately when i wanted to store an object in a session i was getting an error due to the fact that these webcontrols are not serializable. Have just spent a couple of hours trying to sort this out. Rockford, you are a genius.
James Wood
Friday, December 15, 2006 3:38:13 AM (Central Standard Time, UTC-06:00)
In C# it is possible to use the field: target on an attribute to tell the compiler to apply the attribute to the backing field rather than the actual variable.
Comments are closed.