JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

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:

bulletImplementing the logic to prompt the user to save program state.
bulletProviding version safety. 

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:


private Version serializationVersion= new Version(MAJOR_VERSION,
                MINOR_VERSION,
                BUILD,
                UPDATE_VERSION,
                SERIALIZATION_NAME);

if (this.serializationVersion == fileVersion)
{
    drawHistory= (ArrayList)formatter.Deserialize(inStream);
}

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] Attribute

The 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 Fields

You 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 persistence
private string pathToFile= null;
private bool isDirtyDocument= false;

Implementing the Save As ... Dialog

Here is the screen shot of the "Save As" dialog that is launched on the call to saveFileDialog1.ShowDialog()


Drag the SaveFileDialog component onto the form and implement the menuItemSaveAs_Click method. If the SaveFileDialog returns DialogResult.OK the ArrayList, drawHistory, is serialized to the selected file. The name of the file is stored in the field pathToFile. If the name appears valid, the boolean flag isDirtyDocument is set to false.

// ? bug in SaveFileDialog which does not wait for user
// to click OK if user chooses file with "single click" enabled

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 Dialog

Here 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
// ? bug in OpenFileDialog which does not wait for user
// to click OK if user chooses file with "single click" enabled

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... Dialog

Once 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 Handler

You 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 OnClosing

Finally, 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 handler
private void menuItemExit_Click(object sender, System.EventArgs e)
{
    this.Close();
}

The Curious Cat

It 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 Support

One 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)
private bool Deserialize(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 MAJOR_VERSION= 0;
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 More

Here are links to three articles by Jeffrey Richter on Serialization:

Run-time Serialization, Part 1
Run-time Serialization, Part 2
Run-time Serialization, Part 3

Download the Project

You 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 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1