JAL Computing
|
Chapter 22 "Version Safe Object Serialization"The .NET framework has built in support for object persistence using serialization. This is done using Serialization.Formatters.Binary or Serialization.Formatters.Soap in the System.Runtime namespace. As long as the objects in the serialization graph support serialization, adding object persistence to the Draw program is surprisingly easy. The more challenging tasks include:
Saving program state is implemented by using a boolean flag isDirtyDocument.
This flag is set to false when the user opens a file, saves a file or clears the
screen. The flag is set to true on a mouse up click event. Version safety is
implemented by creating an instance of a Version class and adding the version
instance to the object graph.
The Version class includes an equality ( == )
operator that checks for content equivalence as in: if (this.serializationVersion
== fileVersion) Verify a Class is [Serializable]Many of the built in .NET classes support serialization and are marked with the meta data attribute [Serializable]. You can verify that an object is marked as [Serializable] by obtaining the Attribute property using typeof(someClass) and then checking to see if the bit TypeAttributes.Serializable has been set. Here is the code in the new class SerializableDrawableFactory that does input checking to insure that only types that are marked Serializable are added to the HashTable of types. class SerializableDrawableFactory{ private Hashtable hash= new Hashtable(); public bool Add(Type t) { if ((t != null) && !hash.Contains(t.Name) && (typeof(IDrawableShape).IsAssignableFrom(t) && ((t.Attributes & TypeAttributes.Serializable) != 0))) { hash.Add(t.Name,t); return true; } else return false; } ... } Note: The enum TypeAttributes.Serializable is distinct from the interface ISerializable which is used to write a customized serialization routine. Add the [Serializable] AttributeThe drawHistory object graph can be persisted in compact binary form using the BinaryFormatter class or in human readable XML format using the SoapFormatter class. To add persistence to the Draw program, mark the AbstractDrawableShape, Circle, Square and Triangle classes with the [Serializable] attribute and recompile the projects JALInterfaces, DrawablePlugIns and TestDrawFactory. Here is the new Circle class marked with the serializable attribute: [Serializable] class Circle : AbstractDrawableShape { public override void DrawYourself(Graphics g) { Pen p= new Pen(Color,BrushWidth); g.DrawEllipse(p,Position.X,Position.Y,Size,Size); } } The classes in the inheritance hierarchy and object graph need to be [Serializable] if the object is to be persisted using serialization. Note: You will need to delete the outdated references to JALInterfaces and add a new reference to the updated projects where appropriate. Finally, drag the DrawablePlugIns.dll to the TestDrawFactory "plugins" folder. Add Open..., Save, and Save As... Menus and Instance FieldsYou can now add menu items for an Open, Save and SaveAs dialog boxes. Rename the menu items menuItemOpen, menuItemSave and menuItemSaveAs and double click on the menu item to generate the event handlers. private void menuItemSaveAs_Click(object sender, System.EventArgs e)private void menuItemSave_Click(object sender, System.EventArgs e) private void menuItemOpen_Click(object sender, System.EventArgs e) Add the private field pathToFile to store the path to an open file and a boolean flag isDirtyDocument to keep track of save state. // support object persistenceprivate string pathToFile= null; private bool isDirtyDocument= false; Implementing the Save As ... DialogHere is the screen shot of the "Save As" dialog that is launched on the call to saveFileDialog1.ShowDialog()
// ? bug in SaveFileDialog which does not wait for user private void menuItemSaveAs_Click(object sender, System.EventArgs e) { Stream outStream ; SaveFileDialog saveFileDialog1 = new SaveFileDialog(); saveFileDialog1.Filter = "Draw files (*.jal)|*.jal"; saveFileDialog1.FilterIndex = 1 ; saveFileDialog1.RestoreDirectory = true ; if(saveFileDialog1.ShowDialog() == DialogResult.OK) { string pathToFile= saveFileDialog1.FileName; if (!Serialize(pathToFile)) { MessageBox.Show("File Error.","Error"); this.pathToFile= null; } else { this.pathToFile= pathToFile; this.isDirtyDocument= false; } } } Implementing the Open ... File DialogHere is a screen shot of the Open dialog launched by the call to openFileDialog1.ShowDialog(). Drag the OpenFileDialog component onto the form and then implement the menuItemOpen_Click event handler. If the Open Dialog box returns DialogResult.OK, the chosen file is opened and de-serialized. Save the name of the file to the field pathToFile. If the name appears valid, the isDirtyDocument flag is set to false. // add support for object peristence private void menuItemOpen_Click(object sender, System.EventArgs e) { if (isDirtyDocument) { if (MessageBox.Show ("Really? Open New Document Without Saving?", "Draw", MessageBoxButtons.YesNo, MessageBoxIcon.Question)== DialogResult.No) { return; } } Stream inStream; OpenFileDialog openFileDialog1 = new OpenFileDialog(); openFileDialog1.InitialDirectory = "" ; openFileDialog1.Filter = "draw files (*.jal)|*.jal" ; openFileDialog1.FilterIndex = 1 ; openFileDialog1.RestoreDirectory = true ; if(openFileDialog1.ShowDialog() == DialogResult.OK) { string pathToFile= openFileDialog1.FileName; if (!Deserialize(pathToFile)) { MessageBox.Show("File Error.","Error"); this.pathToFile= null; } else { this.pathToFile= pathToFile; this.isDirtyDocument= false;this.Invalidate(); } } } Implement the Save... DialogOnce the user opens a file or saves to a file, the selected file path is stored in the variable pathToFile. If the user selects "Save" from the menu, any changes to the screen are saved to that file. Here is the menuItemSave_Click handler: private void menuItemSave_Click(object sender, System.EventArgs e){ if (!this.isDirtyDocument) { return; } if (this.pathToFile == null) { menuItemSaveAs_Click(sender, e); return; } if (!Serialize(pathToFile)) { MessageBox.Show("File Error.","Error"); this.pathToFile= null; } else { this.isDirtyDocument= false; } } Modify the Clear Screen Menu Item Event HandlerYou should modify the logic in the "Clear Screen" handler to prompt the user to save any changes to the screen. The boolean flag isDirtyDocument is used to determine if the screen has been changed and might need to be saved. Here is the modified menuItemClear_Click event handler: private void menuItemClear_Click(object sender, System.EventArgs e){ if (isDirtyDocument) { if (MessageBox.Show ("Really? Clear Screen Without Saving?", "Draw", MessageBoxButtons.YesNo, MessageBoxIcon.Question)== DialogResult.No) { return; } } drawHistory.Clear(); isDirtyDocument= false; pathToFile= null; this.Invalidate(); // clear the entire screen! } Verify OnClosingFinally, subscribe to the Closing event to verify before exiting the application. public Form1(){ // // Required for Windows Form Designer support // InitializeComponent(); // // TODO: Add any constructor code after InitializeComponent call // // add Custom event handlers this.MouseUp +=new MouseEventHandler(Form1_MouseUp); this.Closing += new CancelEventHandler(this.Form1_Closing); Now you can handle the request to close the form in the Form1_Closing handler: private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e){ if (isDirtyDocument) { if (MessageBox.Show ("Really? Quit Without Saving?", "Draw", MessageBoxButtons.YesNo, MessageBoxIcon.Question)== DialogResult.No) { // Cancel the Closing event from closing the form. e.Cancel = true; } } } Note that the call to this.Close() in the menuItemExit_Click handler will eventually call the Form1_Closing handler! // will verify in Closing event handlerprivate void menuItemExit_Click(object sender, System.EventArgs e) { this.Close(); } The Curious CatIt is actually rather interesting to look at the XML code generated by the SoapFormatter's Serialize method. Here is the SOAP serialization of an Arraylist containing a Circle, Triangle and Square object: <SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <a1:ArrayList id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/ns/System.Collections"> <_items href="#ref-2"/> <_size>3</_size> <_version>3</_version> </a1:ArrayList> <SOAP-ENC:Array id="ref-2" SOAP-ENC:arrayType="xsd:anyType[16]"> <item href="#ref-3"/> <item href="#ref-4"/> <item href="#ref-5"/> </SOAP-ENC:Array> <a3:Circle id="ref-3" xmlns:a3="http://schemas.microsoft.com/clr/nsassem/TestDrawFactory/ TestDrawFactory%2C%20Version%3D1.0.1856.31181%2C%20Culture%3Dneutral%2C%20 PublicKeyToken%3Dnull"> <AbstractDrawableShape_x002B_size>50</AbstractDrawableShape_x002B_size> <AbstractDrawableShape_x002B_position> <x>99</x> <y>60</y> </AbstractDrawableShape_x002B_position> <AbstractDrawableShape_x002B_color> <value>0</value> <knownColor>35</knownColor> <state>1</state> <name xsi:null="1"/> </AbstractDrawableShape_x002B_color> <AbstractDrawableShape_x002B_brushWidth>2</AbstractDrawableShape_x002B_brushWidth> </a3:Circle> <a5:Triangle id="ref-4" xmlns:a5="http://schemas.microsoft.com/clr/nsassem/DrawablePlugIns/DrawablePlugIns %2C%20Version%3D1.0.1856.27550%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> <AbstractDrawableShape_x002B_size>50</AbstractDrawableShape_x002B_size> <AbstractDrawableShape_x002B_position> <x>86</x> <y>22</y> </AbstractDrawableShape_x002B_position> <AbstractDrawableShape_x002B_color> <value>0</value> <knownColor>141</knownColor> <state>1</state> <name xsi:null="1"/> </AbstractDrawableShape_x002B_color> <AbstractDrawableShape_x002B_brushWidth>2</AbstractDrawableShape_x002B_brushWidth> </a5:Triangle> <a3:Square id="ref-5" xmlns:a3="http://schemas.microsoft.com/clr/nsassem/TestDrawFactory/TestDrawFactory %2C%20Version%3D1.0.1856.31181%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> <AbstractDrawableShape_x002B_size>50</AbstractDrawableShape_x002B_size> <AbstractDrawableShape_x002B_position> <x>160</x> <y>38</y> </AbstractDrawableShape_x002B_position> <AbstractDrawableShape_x002B_color> <value>0</value> <knownColor>37</knownColor> <state>1</state> <name xsi:null="1"/> </AbstractDrawableShape_x002B_color> <AbstractDrawableShape_x002B_brushWidth>2</AbstractDrawableShape_x002B_brushWidth> </a3:Square> </SOAP-ENV:Body> </SOAP-ENV:Envelope> Add Version SupportOne potential problem with serialization is versioning conflicts. One solution is to define an IVersionable interface and implement the interface in a Version class. You can then serialize the Version instance. What follows is a sample IVersionable interface, AbstractVersion class and concrete Version class. Note the support for Equals, GetHashCode, operator == and operator !=. This allows the comparison of versions at runtime. To support version safety, the code for serialization and de-serialization has been broken out into the following functions: private bool
Serialize(string
pathToFile) Here is the code for IVersionable, the AbstractVersion class and the concrete Version class. The user of the class can compare the properties MajorVersion, MinorVersion, Build, UpdateVersion, ApplicationName and CultureInfo at runtime. Alternatively, the AbstractVersion class provides a default implementation of the VersionAsString property that concatenates all of these properties into a string. The VersionAsString property is used to implement the Equals, ==, != and GetHashCode methods. You can override the VersionAsString method to return a subset of the IVersionable properties in your class that inherits from AbstractVersion. Your custom implementation of VersionAsString will then determine how the Equals method is implemented on your concrete IVersionable class. In the following example, the concrete class Version inherits from AbstractVersion, but does not override the default implementation of VersionAsString. However, the Version class passes null for CultureInfo to the base class constructor. In other word, the CultureInfo is not used to discriminate between versions. Note: Known colors appear to be encoded in numeric form, at least in the SOAP serialization object graph. // add support for versionable persistence of complex objects interface IVersionable { int MajorVersion {get;} int MinorVersion {get;} int Build {get;} int UpdateVersion {get;} // Default is [ApplicationName] //[MajorVersion].[MinorVersion].[Build].[UpdateVersion] //[CultureInfo.Name] string VersionAsString {get;} System.Globalization.CultureInfo CultureInfo {get;} string ApplicationName {get;} } // user should override VersionAsString // usage MajorVersion.MinorVersion.Build.Update [Serializable] public abstract class AbstractVersion : IVersionable { private int majorVersion; private int minorVersion; private int build; private int updateVersion; private string applicationName; private string cultureInfoName; private CultureInfo cultureInfo; public AbstractVersion(int majorVersion, int minorVersion, int build, int updateVersion, string applicationName, CultureInfo cultureInfo) { if (applicationName == null) { this.applicationName= ""; } else { this.applicationName= applicationName; } if (cultureInfo == null) { this.cultureInfoName= ""; } else { this.cultureInfoName= cultureInfo.Name; } this.majorVersion= majorVersion; this.minorVersion= minorVersion; this.updateVersion= updateVersion; this.build= build; this.cultureInfo= cultureInfo; } // override this, used in Equals, ==, != // MajorVersion.MinorVersion.Build.UpdateVersion public virtual string VersionAsString { get { return this.applicationName + " " + this.majorVersion.ToString() + "." + this.minorVersion.ToString() + "." + this.build + "." + this.updateVersion.ToString() + " " + this.cultureInfoName; } } // override GetHashCode() public override int GetHashCode() { return this.VersionAsString.GetHashCode(); } // override Equals public override bool Equals(object o) { if (((Version)o).VersionAsString == this.VersionAsString) { return true; } else return false; } // define operator == // called for concrete class! // NOT called for references of type object! public static bool operator == (AbstractVersion v1, AbstractVersion v2) { //System.Console.WriteLine("operator == was called"); return( v1.Equals(v2)); } // define operator != public static bool operator != (AbstractVersion v1, AbstractVersion v2) { return !v1.Equals(v2); } public int MajorVersion {get {return this.majorVersion;}} public int MinorVersion {get {return this.minorVersion;}} public int Build {get {return this.build;} } public int UpdateVersion {get {return this.updateVersion;}} public System.Globalization.CultureInfo CultureInfo {get {return this.cultureInfo;}} public string ApplicationName {get{return this.applicationName;}} } // concrete class that uses default VersionToString // but passes null for CultureInfo [Serializable] public class Version : AbstractVersion { public Version(int majorVersion, int minorVersion, int build, int updateVersion, string applicationName) : base(majorVersion,minorVersion,build,updateVersion, applicationName,null) {;} } Here are the class fields that store and create the Version structure: // version properties note use of const const int MINOR_VERSION= 1; const int BUILD= 1; const int UPDATE_VERSION= 0; const string SERIALIZATION_NAME= "DrawSerialization"; private Version serializationVersion= new Version(MAJOR_VERSION, MINOR_VERSION, BUILD, UPDATE_VERSION, SERIALIZATION_NAME); Now you can serialize the Version structure: private bool Serialize(string pathToFile){ Stream outStream= null; // returns false if pathToFile is null if (!File.Exists(pathToFile)) {return false;} outStream = File.OpenWrite(pathToFile); if(outStream == null){return false;} // Code to write the stream goes here. try { //SoapFormatter formatter= new SoapFormatter(); // Serialize our application's screen history BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(outStream, serilizationVersion); formatter.Serialize(outStream, drawHistory); return true; } catch (Exception e) { System.Console.WriteLine(e); return false; } finally { outStream.Close(); } } On de-serialize, you can check for the version of the persisted object graph: private bool Deserialize(string pathToFile){ Stream inStream= null; // returns false if pathToFile is null if (!File.Exists(pathToFile)) {return false;} inStream = File.OpenRead(pathToFile); if(inStream == null){return false;} try { BinaryFormatter formatter = new BinaryFormatter(); //SoapFormatter formatter= new SoapFormatter(); object o= formatter.Deserialize(inStream); if (!(o is Version)) { MessageBox.Show("Invalid File Type.","Error"); return false; } Version fileVersion= (Version)o; if (this.serializationVersion == fileVersion) { drawHistory= (ArrayList)formatter.Deserialize(inStream); } // else if (otherVersion){handleHere;} else { MessageBox.Show("Cannot Open File Version: " + fileVersion.VersionAsString,"Error"); return false; } return true; catch (Exception e) { System.Console.WriteLine(e); return false; } finally { inStream.Close(); } } Congratulations! You now have the tools to serialize an object graph in a version safe manner. Learn MoreHere are links to three articles by Jeffrey Richter on Serialization: Run-time
Serialization, Part 1 Download the ProjectYou can download a version of this project that implements versioning and object persistence here. See ya. |
Send mail to [email protected]
with questions or comments about this web site. Copyright © 2001, 2002, 2003,
2004, 2005, 2006, 2007, 2008, 2009 ©
|