The BinaryReader and BinaryWriter classes are okay when it comes to storing small classes to disk and retrieving them later, as you saw in the last section. But classes can become quite complicated. What happens when your class has numerous member variables and/or linked objects? How do you figure out which data type belongs with which class? In what order were they saved? It can become quite a mess very quickly. Wouldn't it be nice if you didn't have to worry about the details and could just say, "Here's the file I want the class saved to. Now, save it." I'm sure you know where I'm going with this; this is the job of serialization.
Serialization is the process of storing the class off (most probably to disk, but not necessarily) for later retrieval. Deserialization is the process of restoring a class from disk (or wherever you saved it). Sounds tough, but the .NET Framework class library actually makes it quite simple to do.
The process of setting a class up for serialization is probably one of the easiest things that you can do in Managed C++. You simply place the [Serializable] attribute in front of the managed class you want to serialize. Yep, that is it!
[Serializable] __gc class ClassName { //... };
The reason this is possible is because all the class's information is stored in its metadata. This metadata is so detailed that all the information regarding serializing and deserializing the class is available at runtime for the CLR to process the serialization or deserialization request.
Listing 8-7 shows the entire process of setting up the Player class for serialization. To make things interesting, I split PlayerAttr off into its own class. As you will see, even the serialization of a linked object like this only requires placing the [Serializable] attribute in front of it.
Listing 8-7: Making a Class Ready for Serialization
[Serializable] __gc class PlayerAttr { Int32 Strength; Int32 Dexterity; Int32 Constitution; Int32 Intelligence; Int32 Wisdom; Int32 Charisma; public: PlayerAttr(Int32 Str, Int32 Dex, Int32 Con, Int32 Int, Int32 Wis, Int32 Cha) { this->Strength = Str; this->Dexterity = Dex; this->Constitution = Con; this->Intelligence = Int; this->Wisdom = Wis; this->Charisma = Cha; } void Print() { Console::WriteLine(S"Str: {0}, Dex: {1}, Con {2}", __box(Strength), __box(Dexterity), __box(Constitution)); Console::WriteLine(S"Int: {o}, Wis: {1}, Cha {2}", __box(Intelligence), __box(Wisdom), __box(Charisma)); } }; [Serializable] __gc class Player { String *Name; String *Race; String *Class; PlayerAttr *pattr; public: Player (String *Name, String *Race, String *Class, Int32 Str, Int32 Dex, Int32 Con, Int32 Int, Int32 Wis, Int32 Cha) { this->Name = Name; this->Race = Race; this->Class = Class; this->pattr = new PlayerAttr(Str, Dex, Con, Int, Wis, Cha); } void Print() { Console::WriteLine(S"Name: {0}", Name); Console::WriteLine(S"Race: {0}", Race); Console::WriteLine(S"Class: {0}", Class); pattr->Print(); } };
If you can't tell, I play Dungeons and Dragons (D&D). These classes are a very simplified player character. Of course, you would probably want to use enums and check minimums and maximums and so forth, but I didn't want to get too complicated.
Before you actually serialize a class, you have to make a choice. In what format do you want to store the serialized data? Right now, the .NET Framework class library supplies you with two choices. You can store the serialized class data in a binary format or in an XML format or, more specifically, in a Simple Object Access Protocol (SOAP) format.
The choice is up to you. Binary is more compact, faster, and works well with the CLR. SOAP, on the other hand, is a self-describing text file that can be used with a system that doesn't support the CLR. Which formatter type you should use depends on how you plan to use the serialized data.
It is also possible to create your own formatter. This book does not cover how to do this, because this book is about .NET, and the main reason that you might want to create your own formatter is if you are interfacing with a non-CLR (non- .NET) system that has its own serialization format. You should check the .NET Framework documentation for details on how to do this.
As I hinted at previously, the process of serializing a class is remarkably easy. First off, all the code to handle serialization is found in the mscorlib.dll assembly. This means you don't have to worry about loading any special assemblies. The hardest thing about serialization is that you have to remember that the BinaryFormatter is located in the namespace System::Runtime::Serialization::Formatters::Binary. You have the option of using the fully qualified version of the formatter every time, but I prefer to add a using statement and save my fingers for typing more important code.
using namespace System::Runtime::Serialization::Formatters::Binary;
The simplest constructor for the BinaryFormatter is just the standard default, which takes no parameters.
BinaryFormatter *bf = new BinaryFormatter()
To actually serialize a class, you need to call the BinaryFormatter's Serialize() method. This method takes a Stream and a class pointer. Make sure you open the Stream for writing. You also need to truncate the Stream or create a new copy each time. And don't forget to close the Stream when you're done.
BinaryFormatter *bf = new BinaryFormatter(); FileStream *plStream = File::Create(S"Player.dat"); bf->Serialize(plStream, Joe); plStream->Close();
The process of deserializing is only slightly more complicated. This time, you need to use the deserialize() method. This method only takes one parameter, a pointer to a Stream open for reading. Again, don't forget to close the Stream after you're finished with it. The tricky part of deserialization is that the deserialize() method returns a generic Object class. Therefore, you need to typecast it to the class of the original serialized class.
plStream = File::OpenRead(S"Player.dat"); Player *JoeClone = dynamic_cast<Player*>(bf->Deserialize(plStream)); plStream->Close();
Listing 8-8 shows the entire process of serializing and deserializing the Player class.
Listing 8-8: Serializing and Deserializing the Player Class
#using <mscorlib.dll> using namespace System; using namespace System::IO; using namespace System::Runtime::Serialization::Formatters::Binary; Int32 main(void) { Player *Joe = new Player(S"Joe", S"Human", S"Thief", 10, 18, 9, 13, 10, 11); Console::WriteLine(S"Original Joe"); Joe->Print(); FileStream *plStream = File::Create(S"Player.dat"); BinaryFormatter *bf = new BinaryFormatter(); bf->Serialize(plStream, Joe); plStream->Close(); plStream = File::OpenRead(S"Player.dat"); Player *JoeClone = dynamic_cast<Player*>(bf->Deserialize(plStream)); plStream->Close(); Console::WriteLine(S"\nCloned Joe"); JoeClone->Print(); return 0; }
Figure 8-12 shows the results of BinFormSerial.exe displayed to the console. Figure 8-13 shows the resulting binary-formatted serialization output file generated.
Figure 8-12: Console results of BinFormSerial.exe
Figure 8-13: Binary-formatted file output of the serialization of the Player class
There is very little difference in the code required to serialize using the SoapFormatter when compared with the BinaryFormatter. One obvious difference is that you use the SoapFormatter object instead of a BinaryFormatter object. There is also one other major difference, but you have to be paying attention to notice it, at least until you finally try to compile the serializing application. The SoapFormatter is not part of the mscorlib.dll assembly. To use the SoapFormatter, you need to reference the .NET assembly system.runtime.serialization.formatters.soap.dll. You will also find the SoapFormatter class in the namespace System::Runtime::Serialization::Formatters::Soap, which also differs from the BinaryFormatter.
#using <system.runtime.serialization.formatters.soap.dll> using namespace System::Runtime::Serialization::Formatters::Soap;
The biggest difference is one that doesn't occur in the code. Instead, it's the serialized file generated. BinaryFormatted serialization files are in an unreadable binary format, whereas SoapFormatted serialization files are in a readable XML text format.
Listing 8-9 shows the entire process of serializing and deserializing the Player class using the SoapFormatter. Notice that the only differences between SOAP and binary are the #using and using statements and the use of SoapFormatter instead of BinaryFormatter.
Listing 8-9: Serializing and Deserializing the Player Class Using SoapFormatter
#using <mscorlib.dll> #using <system.runtime.serialization.formatters.soap.dll> using namespace System; using namespace System::IO; using namespace System::Runtime::Serialization::Formatters::Soap; Int32 main(void) { Player *Joe = new Player(S"Joe", S"Human", S"Thief", 10, 18, 9, 13, 10, 11); Console::WriteLine(S"Original Joe"); Joe->Print(); FileStream *plStream = File::Create(S"Player.xml"); SoapFormatter *sf = new SoapFormatter(); sf->Serialize(plStream, Joe); plStream->Close(); plStream = File::OpenRead(S"Player.xml"); Player *JoeClone = dynamic_cast<Player*>(sf->Deserialize(plStream)); plStream->Close(); Console::WriteLine(S"\nCloned Joe"); JoeClone->Print(); return 0; }
Figure 8-14 shows the resulting SOAP-formatted serialization output file generated by SoapFormSerial.exe.
Figure 8-14: SOAP-formatted file output of the serialization of the Player class