Advanced Serialization

I l @ ve RuBoard

You've seen how basic serialization is performed and controlled using attributes. But at times you'll need more control over how an object or a collection of objects is serialized and deserialized. We'll look at this next .

Customizing Serialization

You can influence the format of the serialization stream used to serialize an object by implementing the System.Runtime.Serialization.ISerializable interface. This is useful if you need to perform some additional or nonstandard processing as part of the serialization process. A large number of classes in the .NET Framework Class Library implement the ISerializable interface.

For example, consider the FileStream member variable in the Widget class discussed in the previous section. By default, it is pointless to preserve the state of variables such as this because the state is transitory ”once the context changes (when the object is deserialized into another process), file handles and the like become meaningless. For this reason, the member variable was tagged with NotSerializedAttribute . On the other hand, using custom serialization, you could instead save the name of the file that the FileStream member was using as part of the serialization stream. On deserialization, you could read the name of this file back in from the stream, open the file, and point the FileStream member variable at it (all with some suitable error checking, of course). The Widget class (in the Widget.jsl sample file in the CustomSerialization project) illustrates one way of providing this functionality.

The Widget class wraps a FileStream object in such a way that it can be used to manipulate a file and be serialized, and when the object is deserialized the FileStream is recreated and restored to the state it was in; if you read from the FileStream after deserialization, read operations will carry on from the point at which they left off prior to serialization. (You might see some interesting phenomena if the file itself changes in the intervening period, however!)

You should note a few key points about the Widget class. The first is that it is tagged with SerializableAttribute , just like any other serializable class. Second, it implements the ISerializable interface. The class contains three private member variables: a FileStream variable called fs , a String that holds the filename, and a long variable called offset that records the position in the FileStream when serialization occurs:

 importSystem.*; importSystem.IO.*; importSystem.Runtime.Serialization*; /**@attributeSerializableAttribute()*/ publicclassWidgetimplementsISerializable { privateFileStreamfs; privateStringfilename; privatelongoffset; } 

The Widget class provides methods for opening a file stream over a named file ( OpenStream ), returning a handle to the file stream ( GetStream ), and closing the file stream ( CloseStream ). These methods are simply there to give the Widget class some functionality and play no part in the serialization process.

The ISerializable interface comprises a single method called GetObjectData , which is called by the formatter when it decides to serialize the object. Before invoking GetObjectData , the formatter will have performed some initialization, set up some data structures, and written out data such as the assembly information to the serialization stream. Two parameters are passed to Get ­ObjectData : a SerializationInfo object and a StreamingContext structure:

 publicvoidGetObjectData(SerializationInfoinfo,StreamingContextcontext) { } 

The SerializationInfo parameter provides controlled access to the serialization stream. It exposes methods for writing to this stream and reading data back from it (for deserialization). For serialization, the most useful method is AddValue . This method, which is heavily overloaded, writes a name and data pair out to the stream. For example, the following statement outputs the member variable fileName and associates it with the name " File Name" :

 Info.AddValue("FileName",fileName); 

You can send whatever data you need to the serialization stream ”you simply give each item a unique but meaningful name (which does not have to be the same as the name of the variable) so you can identify it when the object is deserialized. The Widget class also calculates the offset of the current position in the FileStream and serializes it with the name " Offset" :

 offset=fs.get_Position(); info.AddValue("Offset",offset); 

The FileStream variable, fs , is not saved because it can be re-created from the filename and the offset when deserialization occurs. The StreamingContext parameter to GetObjectData passes context information that can be useful to the serialization process. The StreamingContext structure also contains a State property. You can query this property to determine the destination of the serialization stream ”it could be a local file, an application in another application domain, an application on a remote machine, or someplace else. The way you perform serialization might vary depending on the ultimate destination (such as transforming the name of the file to reference a network share if the Widget object is being sent to a process running on another computer).

It might seem odd that ISerializable contains only a method for serializing an object and not a corresponding method for deserializing the object. In fact, deserialization is performed by providing a constructor that takes a SerializationInfo parameter and a StreamingContext parameter, just like the GetObjectData method. Conventionally, this constructor is private. (You're unlikely to ever want to call it explicitly.) If you omit this constructor, the object cannot be deserialized.

 privateWidget(SerializationInfoinfo,StreamingContextcontext) { } 

Serialization Surrogates

It is possible for one class to take responsibility for serializing and deserializing objects of another class. Such classes are referred to as serialization surrogates . A serialization surrogate must implement the System.Runtime.Serialization.ISerializationSurrogate interface. This interface contains two methods, GetObjectData and SetObjectData :

 publicvoidGetObjectData(Objectobj,SerializationInfoinfo, StreamingContextcontext) { } publicObjectSetObjectData(Objectobj,SerializationInfo info,StreamingContextcontext,ISurrogateSelectorselector) { } 

The GetObjectData method is similar to that of the ISerializable interface, except that it takes an additional Object parameter that indicates the object to be serialized. SetObjectData is called during deserialization. It is passed an empty object and can use the data in the SerializationInfo parameter to populate this object.

A SerializationSurrogate is discovered and invoked through an ISurrogateSelector object, which is attached to the SurrogateSelector property of the formatter. (By default, this property is null .) ISurrogateSelector is an interface located in the System.Runtime.Serialization namespace, and the .NET Framework Class Library provides a default implementation in the System.Runtime.Serialization.SurrogateSelector class that you can use as-is or extend for your own purposes. An ISurrogateSelector links surrogates together in a list and can iterate through this list to search for a surrogate that is capable of serializing and deserializing an object of the type specified by the formatter object. You can add a SerializationSurrogate to this list by calling the Add method of the SurrogateSelector class (which is not part of the ISurrogateSelector interface). This method's parameters include the type of data the surrogate can handle:

 publicvoidAddSurrogate(Typetype,StreamingContextcontext, ISerializationSurrogatesurrogate); 

You can remove a surrogate from the list by using the Remove ­Surrogate method. You can even chain SurrogateSelectors together using the ChainSelector method of the SurrogateSelector class. (Again, this is not part of the ISurrogateSelector interface.) This allows the selector to forward queries for a matching surrogate to other selectors if it cannot find a match itself.

In the deserialization constructor, you read the contents of the serialization stream, extracting items using the names specified when the object was serialized. (You do not have to read the data back in the same order it was written.) You can also perform any other initialization needed. For example, the constructor in the Widget class populates the offset and fileName member variables and then creates a FileStream over the specified file, setting the read/write position to the location indicated by the offset variable. (There should be some error checking, but it is omitted here for clarity.)

 privateWidget(SerializationInfoinfo,StreamingContextcontext) { offset=info.GetInt64("Offset"); fileName=info.GetString("FileName"); fs=newFileStream(fileName,FileMode.OpenOrCreate, FileAccess.ReadWrite); fs.set_Position(offset); } 

The SerializationInfo class provides a whole raft of GetXXX methods that you can use to extract data of different types from the serialization stream. The Widget constructor uses GetString and GetInt64 (which returns a Java long ).

Handling Object Graphs

The examples shown so far have been simple, inasmuch as the objects being serialized have been small and straightforward. However, you can serialize entire graphs of related objects using the same mechanism ”as long as you're aware of some possible complications. A collection of objects, possibly of different types, might contain dependencies. For example, consider the extended version of the Cake class and the Baker class shown in the Cake.jsl and Baker.jsl sample files (in the GraphSerialization project), respectively. A Baker object encapsulates the details of the chef (currently just the name) that is responsible for creating and delivering the cake:

Baker.jsl
 packageGraphSerialization; importSystem.*; //CakeBakerclass-eachcakeisassignedtoasingleBaker /**@attributeSerializableAttribute()*/ publicclassBaker { privateStringname; publicBaker(StringbakerName) { this.name=bakerName; } /**@property*/ publicStringget_Name() { returnthis.name; } } 

The Cake class has two additional properties: an ID that allows a cake to be uniquely identified and a reference to the Baker who is baking it. The constructor populates the member variables that back these properties. Both properties are immutable: No self-respecting baker would consider assuming control of a partially baked cake started by another baker.

 publicclassCake { //Cakedata privateStringmessage; privateintfilling; privatefloatsize; privateintshape; privateStringid; //Whobakedthecake? privateBakerbaker; //Setdefaultsforsize,shape,filling,andmessage //givesthecakethespecifiedID, //andassignsittothespecifiedBaker publicCake(StringcakeID,BakercakeBaker) { this.filling=Sponge; this.shape=Square; this.size=8; this.message= ""; this.id=cakeID; this.baker=cakeBaker; } //Properties /**@property*/ publicBakerget_Baker() { returnthis.baker; } /**@property*/ publicStringget_ID() { returnthis.id; } } 

A typical application that manages information concerning Bakers and Cakes could employ a collection object such as a System.Collections.Hashtable to assemble them together. The Kitchen class (in Kitchen.jsl) is an example of just such an application:

 publicclassKitchen { /**@attributeSystem.STAThread()*/ publicstaticvoidmain(String[]args) { //Createsomebakers Bakerdiana=newBaker("Diana"); Bakerfrancesca=newBaker("Francesca"); Bakerjames=newBaker("James"); //Getthebakerstomakesomecakes Cakecake1=newCake("Cake1",diana); Cakecake2=newCake("Cake2",diana); Cakecake3=newCake("Cake3",francesca); Cakecake4=newCake("Cake4",james); //StorethebakerandcakeinformationinaHashtable HashtablekitchenData=newHashtable(); kitchenData.Add(diana.get_Name(),diana); kitchenData.Add(francesca.get_Name(),francesca); kitchenData.Add(james.get_Name(),james); kitchenData.Add(cake1.get_ID(),cake1); kitchenData.Add(cake2.get_ID(),cake2); kitchenData.Add(cake3.get_ID(),cake3); kitchenData.Add(cake4.get_ID(),cake4); } } 

The application can then serialize the Hashtable to a file to persist all this data:

 //Serializethehashtabletoafile BinaryFormatterformatter=newBinaryFormatter(); FileStreamstream=newFileStream("Kitchen.bin",FileMode.Create, FileAccess.Write,FileShare.None); formatter.Serialize(stream,kitchenData); stream.Close(); 

Note

You can serialize any collection as long as two conditions are met: first, the collection class itself is serializable, and second, the objects you have placed in it are all serializable. In our example, the Hashtable class implements ISerializable , so the first condition is met. Both the Baker and the Cake classes are tagged with Serializable ­Attribute so the second condition is met also.


Rebuilding the hash table from the serialized stream is simply a matter of deserializing from the Kitchen.bin file. However, behind the scenes the deserialization process has a little more work to do. The issue lies with the Baker references in the Cake objects. Prior to serialization, the contents of the kitchen ­Data hash table would have looked similar to the layout shown in Figure 10-1.

Figure 10-1. The layout of the kitchenData hash table

Notice the multiple references to the various Baker objects. Each cake stores a reference to the associated baker. On deserialization, the formatter must be clever enough to realize that once a Baker object has been instantiated , the references in the Cake objects should be fixed up to refer to this object and not cause the creation of new duplicate Baker objects, as shown in Figure 10-2.

Figure 10-2. Chaos in the kitchen!

Fortunately, the default deserialization mechanism implemented by the .NET Framework classes has a degree of intelligence. As mentioned earlier, all formatters must implement the IFormatter interface. Although this interface is small (it has two methods, Serialize and Deserialize , along with three properties), you'll appreciate that any class implementing this interface has to do a lot of work, but much of this effort is the same regardless of the format being used. For this reason, the serialization architecture involves not just the formatter but also a number of helper objects. (You've already seen one ”the SerializationBinder class.)

Two additional classes that are used throughout binary deserialization are System.Runtime.Serialization.ObjectIDGenerator and System.Runtime.Serialization.ObjectManager . During deserialization, the formatter calls the ObjectManager to determine whether a given object reference refers to an object that has already been deserialized or to an object that is still in the stream. The ObjectManager operates in conjunction with the ObjectIDGenerator . The ObjectIDGenerator generates a unique serial number for each object in the stream (keep in mind that such a serial number is not globally unique, only unique for the stream) as it is presented, but it can also determine whether the object has already been seen; if so, it passes back an indication that the object has been previously rebuilt. Determining the object's deserialization status is possible because the ObjectIDGenerator maintains an internal hash table of object references and serial numbers . If the ObjectManager indicates that the referenced object has already been deserialized, the formatter will resolve the reference immediately. Otherwise, it will register a fixup with the ObjectManager indicating that resolution must be performed later, after the referenced object has been read in. (The ObjectManager exposes a DoFixups method that the formatter can call to resolve these references.)

There is no guarantee of the order in which objects in a graph will be serialized or deserialized. Also note that at no time during the deserialization process will any default constructors be called. (If an object implements ISerializable , the deserialization constructor will be invoked, however.) There might well be occasions when the interdependencies of objects require some additional initialization beyond the scope of that performed by the formatter. To help you in this situation, your classes can implement the System.Runtime.Serialization.IDeserializationCallback interface. This interface specifies a single method with the following signature:

 publicvoidOnDeserialization(Objectsender) { //Placeyourinitializationcodehere } 

At the end of the deserialization process, after every object has been reconstructed, the ObjectManager will call this method (which is actually an event delegate and is invoked by the formatter calling the RaiseDeserializationEvent method of the ObjectManager ) on each object that implements the IDeserializationCallback interface.

There's a lot more to this process than we have space to cover here, but most of the time you don't need to know exactly what's happening as long as you're aware that the default deserialization mechanism should construct an object graph that's the same as the original. However, in case you ever feel the need to implement your own custom formatter, Microsoft has created the System.Runtime.Serialization.Formatter abstract class to assist you (as mentioned earlier in this chapter, in the sidebar titled "Implementing the IFormatter Interface"). This class contains much of the logic needed to interact with the ObjectManager , but remember that you must supply your own implementation of the WriteXXX helper methods that will handle outputting data to the serialization stream as well as the Serialize and Deserialize methods.

I l @ ve RuBoard


Microsoft Visual J# .NET (Core Reference)
Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
ISBN: 0735615500
EAN: 2147483647
Year: 2002
Pages: 128

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net