DiskDiff: Saving and Restoring

One of the points of DiskDiff is to be able to compare the current state of a directory tree to a previous state. It’s therefore important to be able to store and retrieve that state.

The logical way to do this in the .NET Framework is to use serialization. This is one of the areas in which a managed environment really shines; the runtime already has enough informa­tion in the metadata associated with a class to be able to do the serialization. You must write only the serialization code and define how you want your classes to be serialized.

The .NET Framework supports serialization either to SOAP format (the same XML format used by Web services) or to a binary format. For this example, we’ve decided to use SOAP formatting since it’s easy to look at the resulting file and see what’s happening.

After adding Save and Open menu items on the File menu, add the following code in the save event handler:

protected void FileSave_Click (object sender, System.EventArgs e)

{

SaveFileDialog dialog = new SaveFileDialog();

dialog.Filter = “DiskDiff files (*.diskdiff)|*.diskdiff|” +

“All files (*.*)|*.*”;

dialog.ShowDialog();

Stream streamWrite = File.Create(dialog.FileName);

SoapFormatter soapWrite = new SoapFormatter();

soapWrite.Serialize(streamWrite, directoryNode);

streamWrite.Close();

}

SaveFileDialog is a class that comes with the system, and the Filter property controls which files are shown. Once a filename is obtained from the dialog box, it’s simply a matter of creating a file with that name, creating a new SoapFormatter, and calling the Serialize() function. The open event handler is only a bit more complicated:

protected void FileOpen_Click (object sender, System.EventArgs e)

{

OpenFileDialog dialog = new OpenFileDialog();

dialog.Filter = “DiskDiff files (*.diskdiff)|*.diskdiff|”+

“All files (*.*)|*.*”;

dialog.ShowDialog();

try

{

Stream streamRead = File.OpenRead(dialog.FileName);

SoapFormatter soapRead = new SoapFormatter();

directoryNode = (DirectoryNode) soapRead.Deserialize(streamRead);

streamRead.Close();

rootDirectory = directoryNode.Root;

treeView1.Nodes.Clear();

PopulateTreeNode(treeView1.Nodes, directoryNode, 1.0f);

}

catch (Exception exception)

{

MessageBox.Show(exception.ToString());

}

}

In this handler, the Deserialize() call reconstructs the objects in the stream passed to it. If everything goes correctly in this code, the rootDirectory field of the form is set to the top-level directory that was deserialized, and the TreeView object is populated.

1. Controlling Serialization

Chapter 34 covers serialization in detail. Two attributes control the behavior of the serialization:

[Serializable]

[NonSerialized]

The Serializable attribute is placed on classes that should be serialized, and the NonSerialized attribute is placed on class members that shouldn’t be serialized. When doing serialization, a class is serialized only if it has the Serialized attribute, and each member in that class is serialized if it doesn’t have the NonSerialized attribute.

In this example, you’ll be serializing a DirectoryNode object, which can contain FileNode objects, so you’ll have to annotate both of those classes. This is simple; just look through the definition of the object, and figure out which members shouldn’t be saved. DirectoryNode looks like this:

[Serializable]

public class DirectoryNode: IComparable<DirectoryNode>

{

string root;

List<FileNode> files = new List<FileNode>();

List<DirectoryNode> dirs = new List<DirectoryNode>();

Directory directory; // this directory

long? size = null;                         // size of dir in bytes

long? sizeTree = null;                          // size of dir and subdirs

[NonSerialized]

bool cancelled = false;

}

The cancelled field is set as NonSerialized since you don’t need to save it.

DiskDiff uses the BinaryFormatter to serialize the data to disk. As well as having better performance and a smaller output size than the SoapFormatter, BinaryFormatter has been updated to support generics and nullable types. The SoapFormatter isn’t part of the future of the .NET Framework libraries and hasn’t been updated.

2. Finer Control of Serialization

For the FileNode object, the minimum amount of information to save is the full path to the file. But the full path is stored in the FileInfo object that’s part of the FileNode object, and you have no way to change how FileInfo serializes.

One option is to have the full filename stored along with the File object, and not serialize the file object, but that results in a duplication of data. Not only is that wasteful but you also have to keep the two variables in sync.

The CLR serialization code provides a way for your object to take over how serialization occurs. You do this by implementing the ISerializable interface on your object. Add the following routines to FileNode:

// Routines to handle serialization

// The full name and size are the only items that need

// to be serialized.

FileNode(SerializationInfo info, StreamingContext content)

{

file = new FileInfo(info.GetString(“N”));

size = info.GetInt64(“S”);

}

public void GetObjectData(SerializationInfo info, StreamingContext content)

{

info.AddValue(“N”, file.FullName);

info.AddValue(“S”, this.size);

}

In this code, the first change was to have the FileNode object store the size itself, rather than use the Length property on the File object. The GetObjectData() function is implemented for the ISerializable interface, and it’s called with each object during serialization. The function saves the values for the lull name of the file and the size of the file.

To deserialize an object, the runtime creates a new instance of an object and then calls the special constructor listed previously. It extracts the two fields from the object and creates the contained File object.

Incidentally, the constructor isn’t part of the ISerializable interface because constructors can’t be members of interfaces. Adding this constructor is something you need to remember to do, or you’ll get an exception when you try to deserialize your object.

The code changes to the DirectoryNode class are similar:

// Routines to handle serialization

// The full name and size are the only items that need

// to be serialized.

DirectoryNode(SerializationInfo info, StreamingContext content)

{

root = info.GetString(“R”);

directory = new Directory(root);

files = (List<FileNode>) info.GetValue(“F”, typeof(List<FileNode>));

dirs = (List<DirectoryNode>) info.GetValue(“D”, typeof(List<DirectoryNode>));

size = info.GetInt64(“S”);

sizeTree = info.GetInt64(“ST”);

}

public void GetObjectData(SerializationInfo info, StreamingContext content) {

info.AddValue(“R”, root);

info.AddValue(“F”, files);

info.AddValue(“D”, dirs);

info.AddValue(“S”, size);

info.AddValue(“ST”, sizeTree);

}

One possible way to further reduce the file size is to not serialize the FileNode objects at all but to save the names and sizes of files from within the DirectoryNode serialization code. This requires naming the fields File1, Size1, File2, Size2, and so on.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *