A formatter is an object that knows how to write arbitrary objects to a stream. A formatter exposes this functionality by implementing the IFormatter information from the System.Runtime.Serialization namespace: Interface IFormatter ' Properties Property Binder() As SerializationBinder Property Context() As StreamingContext Property SurrgateSelector() As ISurrogateSelector ' Methods Function Deserialize(serializationStream As Stream) As Object Sub Serialize(serializationStream As Stream, graph As Object) End Interface A formatter has two jobs. The first is to serialize arbitrary objects, specifically their fields, including nested objects. [1] The formatter knows which fields to serialize using Reflection, [2] which is the .NET API for finding out type information about a type at run time. An object is written to a stream via the Serialize method and is read from a stream via the Deserialize method.
The second job of a formatter is to translate the data into some format at the byte level. The .NET Framework provides two formatters: BinaryFormatter and the SoapFormatter. Just like BinaryWriter, the BinaryFormatter class, from the System.Runtime.Serialization.Formatters.Binary namespace, writes the data in a binary format. SoapFormatter, from the System.Runtime.Serialization.Formatters.Soap namespace, [3] writes data in XML according to the Simple Object Access Protocol (SOAP) specification. Although SOAP is the core protocol of Web services, using the SOAP formatter for the purposes of serializing settings or document data has nothing to do with Web services or even the Web. However, it is a handy format for a human to read.
There is one stipulation on any type that a formatter is to serialize: It must be marked with SerializableAttribute, or else the formatter will throw a run-time exception. After the type (and the type of any contained field) is marked as serializable, serializing an object is a matter of creating a formatter and asking it to serialize the object: Imports System.Runtime.Serialization Imports System.Runtime.Serialization.Formatters Imports System.Runtime.Serialization.Soap ... <Serializable()> _ Class MyData ' NOTE: Public fields should be avoided in general, ' but are useful to simplify the code in this case Public s As String = "Wahoo!" Public n As Integer = 452 End Class Shared Sub DoSerialize() Dim data As MyData = New MyData() Dim mystream As Stream = New FileStream("c:\temp\mydata.xml", _ FileMode.Create) ' Write to the stream Dim formatter As IFormatter = New SoapFormatter() Formatter.Serialize(mystream, data) ' Reset the stream to the beginning mystream.Seek(0, SeekOrigin.Begin) ' Read from the stream Dim data2 As MyData = _ CType(formatter.Deserialize(mystream), MyData) ' Do something with the data MsgBox(data2.s & " " & data2.n) mystream.Dispose() End Sub After creating the formatter, the code makes a call to Serialize, which writes the type information for the MyData object and then recursively writes all the data for the fields of the object. To read the object, we call Deserialize and make a cast to the top-level object, which reads all fields recursively. Because we chose the text-based SOAP formatter and a FileStream, we can examine the data that the formatter wrote: <SOAP-ENV:Envelope ...> <SOAP-ENV:Body> <a1: Form1_x002B_MyData id="ref-1" ...> <s id="ref-3">Wahoo!</s> <n>452</n> </a1: Form1_x002B_MyData > </SOAP-ENV:Body> </SOAP-ENV:Envelope> Here we can see that an instance of Form1.MyData was written and that it contains two fields: one (called "s") with the value "Wahoo!", and a second one (called "n") with the value "452". This was just what the code meant to write. Skipping a Nonserialized FieldWe have some control over what the formatter writes, although probably not in the way you'd expect. For example, if we decide that we want to serialize the MyData class but not the n field, we can't stop the formatter by marking the field as protected or private. To be consistent at deserialization, an object will need the protected and private fields just as much as it needs the public ones (in fact, fields shouldn't be public at all!). However, if we apply NonSerializedAttribute to a field it will be skipped by the formatter: <Serializable()> _ Class MyData Public s As String = "Wahoo!" <NonSerializable()> Public n As Integer = 452 End Class Serializing an instance of this type shows that the formatter is skipping the nonserialized field: <SOAP-ENV:Envelope ...> <SOAP-ENV:Body> <a1: Form1_x002B_MyData id="ref-1" ...> <s id="ref-3">Wahoo!</s> </a1: Form1_x002B_MyData > </SOAP-ENV:Body> </SOAP-ENV:Envelope> IDeserializationCallbackGood candidates for the nonserialized attribute are fields that are calculated, cached, or transient, because they don't need to be stored. However, when an object is deserialized, the nonserialized fields may need to be recalculated to put the object into a valid state. For example, if we expand the duties of the n field of the MyData type to be a cache of the s field's length, there's no need to persist n, because it can be recalculated at any time. However, to keep n valid, the MyData object must be notified when s changes. Using properties keeps the n and s fields controlled. However, when an instance of MyData is deserialized, only the s field is set, and not the n field (recall that the n field is nonserialized). To cache the length of the s field in n after deserialization, we must implement the IDeserializationCallback interface: Interface IDeserializationCallback Sub OnDeserialization(sender As Object) End Interface The single method, OnDeserialization, will be called after the formatter has deserialized all the fields. This is the time to make sure that the nonserialized fields of the object have the appropriate state: <Serializable()> _ Class MyData Implements IDeserializationCallback Dim s As String = "Wahoo!" <NonSerializable()> n As Integer = 6 Public Property MyString() As String Get Return s End Get Set s = value n = s.Length End Set End Property Public ReadOnly Property Length() As Integer Get Return n End Get End Property #region Implementation of IDeserializationCallback Public Sub OnDeserialization(sender As Object) Implements _ IDeserializationCallback.OnDeserialization ' Cache the string's length n = s.Length End Sub #endregion End Class If you've got any fields marked as nonserialized, chances are you should be handling IDeserializationCallback to set those fields at deserialization time. |