Serialisieren eines Objektes, das Events feuert

Objektserialisierung ist mit dem .NET Framework denkbar einfach. Sie müssen eine Klasse nur mit einem Attribut als Serialisierbar markieren. Die Serialisierung erfolgt dann entweder implizit, wenn eine Instanz einer solchen Klasse über eine Remoting-Grenze bewegt wird. Oder Sie führen sie explizit durch, weil Sie z.B. das Objekt speichern oder per TcpClient verschicken wollen. Für die (De)Serialisierung sind Formatter-Objekte zuständig.

So weit, so gut. Im Rahmen des Projektes der Developer LAN Party (www.devlanparty.de), die Christof Sprenger und ich am 20./21.6. in der TU München veranstaltet haben, funktionierte die Serialisierung jedoch nicht so einfach.

Innerhalb der von den Teilnehmern zu realisierenden Anwendung sollte das Model-View-Controller (MVC) Pattern angewandt werden. Ein Model hielt Daten, ein Controller war verantwortlich für deren Manipulation durch die Methoden der Model Objekthierarchie und ein View sollte den Zustand des Model nach Veränderung anzeigen.

Das Pattern zu realisieren war auch kein Problem. Das Model feuerte einen Event, um angeschlossene Views zu informieren, dass es vom Controller verändert wurde. Eine Implementation "nach dem Buch":

Public Class Model
    Public Event OnChange(...)
    ...
    Public Sub DoSomething(...)
        ...
        RaiseEvent OnChange(...)
    End Sub
End Class

Dann aber sollte das Model bei Bedarf auch noch an einen anderen Prozess verschickt werden. Dafür wurde es als serialisierbar markiert:

<Serializable> _
Public Class Model
    Public Event OnChange(...)
    ...
End Class

Bei der Serialisierung trat dann aber ein Fehler auf:

imports System.Runtime.Serialization.Formatters.Binary
...
private withevents _m as new Model()
...
_m.DoSomething(...)
...
dim ms as New IO.MemoryStream
dim s as New BinaryFormatter
s.Serialize(ms, _m)

Serialize() meldete, dass das Formular, in dem _m mit WithEvents deklariert war, nicht serialisierbar sei! Was war los?

Die Lösung liegt im Verständnis, wie Events im .NET Framework implementiert sind. Events sind Delegate-Typen und eine Event-Deklaration in einer Klasse fügt ihr eine weitere Member-Variable hinzu. Die Model-Klasse sah also intern etwas so aus:

Public Delegate Sub OnChangeDelegate(...)

<Serializable> _
Public Class Model
    Public onChange as OnChangeDelegate
    ...
    Public Sub DoSomething(...)
        ...
        onChange(...)
    End Sub
End Class

Und im Formular passierte folgendes, um den Eventhandler an das Model zu binden:

Private _m as new Model()
...
Private Sub _m_OnChange(...)
End Sub
...
_m.onChange = new OnChangeDelegate(AddressOf _m_OnChange)

Damit enthielt die Model-Instanz in onChange einen verweis auf das Formular in Form eines Delegate, der nicht nur die Adresse der Aufzurufenden Funktion speichert, sondern auch einen Verweis auf das Objekt, auf dem sie aufgerufen werden soll.

Da die Serialisierung rekursiv verläuft und immer einen ganzen Objektbaum ausgehend von der an Serialize() übergebenen Wurzel versucht zu serialisieren, stolperte sie natürlich auch über onChange und stellte fest, dass das angebundene Formular eben nicht serialisierbar war.

Ein Ausschluss der Event-Definition von der Serialisierung war allerdings nicht (!) möglich:

<Serializable> _
Public Class Model
    <NonSerialized> _
    Public Event OnChange(...)
    ...
End Class

Die Lösung brachte erst eine Implementation des ISerializable-Interface:

<Serializable> _
Public Class Model
    implements ISerializable

    Event OnChange(...)

    Private _myMember as Integer

    public sub new(info as SerializationInfo, context as StreamingContext)
        _myMember = info.GetInt32("myMember")
    End Sub

    Public Sub GetObjectData(ByVal info As System.Runtime.Serialization.SerializationInfo, _
                             ByVal context As System.Runtime.Serialization.StreamingContext) _
                             Implements System.Runtime.Serialization.ISerializable.GetObjectData
        info.AddValue("myMember", _myMember)
    End Sub
End Class

In der Methode GetObjectData() des Interface wurden nur die wirklich zu serialisierenden Member-variablen des Objekts in den Serialisierungsdatenstrom geschrieben und in einem speziellen Konstruktor auch nur genau diese Member wieder ausgelesen. Der versteckte Member für den Eventhandler-Delegate wurde also ausgespart. Damit war das Model problemfrei zwischen Prozessen übertragbar.

No Comments