ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Object Serialization with the Memento Pattern

by Budi Kurniawan
09/09/2002

In my previous article, "C# Object Serialization", I explained how you could persist objects into a file. However, in many cases, not all objects are suitable for serialization. Some objects contain sensitive data that should not be saved into a file. Other objects are too bulky to persist in whole, so you may want to choose to persist only part of the objects. In this article, I discuss more advanced object serialization using the memento design pattern. At the end of the article, there is a vector- based application, similar to the one in the previous article, that demonstrates the use of the memento pattern for object serialization. You should read the previous article, if you have not done so.

The object serialization technology in the .NET Framework works well and is an excellent technology. Once your application becomes more complex, however, you might face problems with object serialization. There are cases where it might not be possible for your object's class to implement the ISerializable interface or to mark it with the [Serializable] attribute.

For instance, in a vector-based drawing application, a shape may be represented by a class derived from the System.Windows.Forms.Control class. Unfortunately, even if you marked the class with the [Serializable] attribute, you still cannot serialize instances of the class.

Related Reading

Programming C#
By Jesse Liberty

In addition, there are two other problems to solve. The first relates to efficiency: suppose you have a very large object that you want to persist, but only a small fraction of the states inside the object need to be saved. Serializing the whole object will require greater space than necessary, not to mention that it will take more time to serialize it and later retrieve it.

The second problem has to do with security. You may want to protect some internal state of the object. In other words, you may want to persist some states but not some other states.

These issues make the object serialization technique seem useless, don't they? Not really. We can still use the .NET Framework object serialization technique, but with the help of the memento design pattern.

The idea of this pattern is simple. In this pattern, the object whose state you want to persist is called the originator. To apply the pattern, you create another object called the memento, an external object that will hold the states of the originator. Therefore, if you need to save the states of the variables a, b, c, and d from the originator, the memento will have the same variables a, b, c, and d.

But what if some of the states in the originator are private variables, which of course are not accessible from outside the originator? The solution to this problem shows the beauty of this pattern. The originator will have two extra methods. One is called CreateMemento and the other is SetMemento.

The CreateMemento method instantiates a memento object, copies all of the states that need to be persisted to the memento object, and then returns the memento object. Therefore, to persist the originator, you call its CreateMemento to obtain a memento object and then serialize the memento object.

The SetMemento method is used to get the states back. After you deserialize the previously-saved memento object, you call the SetMemento object of the originator by passing the memento object obtained from the deserialization. Clever, isn't it? It's now time to put theory into practice with a vector-based drawing application.

Example: Vector-based Drawing Application

In this vector-based drawing application, there is only one type of shape that the user can draw: ellipses. An ellipse is represented by the Ellipse class, which is derived from the System.Windows.Forms.UserControl class.

There are several advantages to deriving from the UserControl class. For example, each ellipse object can receive user events, such as mouse click and keyboard input. Each ellipse drawn is added to the Controls collection of the main form. Because each ellipse is a control, it is not necessary for the form to override its OnPaint method. Consequently, the Ellipse class is responsible for drawing itself by overriding the OnPaint method. When the ellipse objects need to be persisted, it is not practical to serialize Ellipse objects directly. Each Ellipse object carries with it the states from its base class and most have not been changed.

The Ellipse class's constructor accepts a System.Drawing.Rectangle object, which it uses to set its bounds. This Rectangle object provides the top-left corner position, width, and height of the Ellipse object. This Rectangle needs to be saved when the Ellipse object is serialized. In fact, this is the only state that we need to persist.

For this purpose, the Ellipse class contains two special methods: CreateMemento and SetMemento. The CreateMemento method constructs a Memento object, populates it with the necessary state, and returns the Memento object. The SetMemento method is useful when we need to reconstruct the Ellipse object after deserializing it from a disk file.

Example 1 offers the Ellipse class and Example 2 the Memento class.

Example 1. The Ellipse class


using System;
using System.Drawing;
using System.Runtime.Serialization;
using System.Windows.Forms;
  public class Ellipse : UserControl
  {
    Pen pen = Pens.Red;
    Rectangle rect;

    public Ellipse()
    {
    }

    public Ellipse(Rectangle rect) 
    {
      this.rect = rect;
      this.SetBounds(rect.Left, rect.Y, rect.Width, rect.Height);
    }

    override protected void OnPaint(PaintEventArgs e) 
    {
      Graphics g = e.Graphics; 
      g.DrawEllipse(pen, 0, 0, rect.Width - 1, rect.Height - 1);
    }

    public Memento CreateMemento() 
    {
      Memento memento = new Memento();
      memento.Rect = rect;
      return memento;
    }

    public void SetMemento(Memento memento) 
    {
      this.rect = memento.Rect;
      this.SetBounds(rect.Left, rect.Y, rect.Width, rect.Height);
    }
    
  }

Example 2. The Memento class


using System;
using System.Drawing;
using System.Runtime.Serialization;
 [Serializable]
  public class Memento
  {
    private Rectangle rect;

    public Rectangle Rect
    {
      get
      {
        return rect;
      }
      set
      {
        rect = value;
      }
    }
  }

Now, let's shift our attention to the application's form.

The client area of the form contains nothing. However, the form wires the MouseDown and the MouseUp events to the this_MouseDown and this_MouseUp event handlers, respectively. To draw a shape, the user clicks on the client area, drags the mouse, and releases the mouse button. The shapes drawn are added to the form's Controls collection.

The File menu item contains the following menu items: New, Open, Save, Save As, and Exit.

The form class for this application is shown in Example 3.

Example 3. The application's form class


using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

namespace MyCSharp
{
  /// <summary>
  /// Summary description for Form1.
  /// </summary>
  public class Form1 : System.Windows.Forms.Form
  {
    private System.Windows.Forms.MainMenu mainMenu1;
    private System.Windows.Forms.MenuItem fileMenuItem;
    private System.Windows.Forms.MenuItem fileNewMenuItem;
    private System.Windows.Forms.MenuItem fileOpenMenuItem;
    private System.Windows.Forms.MenuItem fileSaveMenuItem;
    private System.Windows.Forms.MenuItem fileSaveAsMenuItem;
    private System.Windows.Forms.MenuItem fileExitMenuItem;
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.Container components = null;

    private Point startPoint;
    private String filename;

    public Form1()
    {
      //
      // Required for Windows Form Designer support
      //
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
    }

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if (components != null) 
        {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }

		#region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      this.mainMenu1 = new System.Windows.Forms.MainMenu();
      this.fileMenuItem = new System.Windows.Forms.MenuItem();
      this.fileNewMenuItem = new System.Windows.Forms.MenuItem();
      this.fileOpenMenuItem = new System.Windows.Forms.MenuItem();
      this.fileSaveMenuItem = new System.Windows.Forms.MenuItem();
      this.fileSaveAsMenuItem = new System.Windows.Forms.MenuItem();
      this.fileExitMenuItem = new System.Windows.Forms.MenuItem();
      // 
      // mainMenu1
      // 
      this.mainMenu1.MenuItems.AddRange(
	      new System.Windows.Forms.MenuItem[] {
                                          this.fileMenuItem});
      // 
      // fileMenuItem
      // 
      this.fileMenuItem.Index = 0;
      this.fileMenuItem.MenuItems.AddRange(
	       new System.Windows.Forms.MenuItem[] {
                                           this.fileNewMenuItem,
                                           this.fileOpenMenuItem,
                                           this.fileSaveMenuItem,
                                           this.fileSaveAsMenuItem,
                                           this.fileExitMenuItem});
      this.fileMenuItem.Text = "File";
      // 
      // fileNewMenuItem
      // 
      this.fileNewMenuItem.Index = 0;
      this.fileNewMenuItem.Shortcut = 
	       System.Windows.Forms.Shortcut.CtrlN;
      this.fileNewMenuItem.Text = "&New";
      this.fileNewMenuItem.Click += new System.EventHandler(
	       this.fileNewMenuItem_Click);
      // 
      // fileOpenMenuItem
      // 
      this.fileOpenMenuItem.Index = 1;
      this.fileOpenMenuItem.Shortcut = 
	       System.Windows.Forms.Shortcut.CtrlO;
      this.fileOpenMenuItem.Text = "&Open";
      this.fileOpenMenuItem.Click += new System.EventHandler(
	       this.fileOpenMenuItem_Click);
      // 
      // fileSaveMenuItem
      // 
      this.fileSaveMenuItem.Index = 2;
      this.fileSaveMenuItem.Shortcut = 
	       System.Windows.Forms.Shortcut.CtrlS;
      this.fileSaveMenuItem.Text = "&Save";
      this.fileSaveMenuItem.Click += new System.EventHandler(
	       this.fileSaveMenuItem_Click);
      // 
      // fileSaveAsMenuItem
      // 
      this.fileSaveAsMenuItem.Index = 3;
      this.fileSaveAsMenuItem.Text = "Save &As";
      this.fileSaveAsMenuItem.Click += new System.EventHandler(
	       this.fileSaveAsMenuItem_Click);
      // 
      // fileExitMenuItem
      // 
      this.fileExitMenuItem.Index = 4;
      this.fileExitMenuItem.Text = "E&xit";
      this.fileExitMenuItem.Click += new System.EventHandler(
	       this.fileExitMenuItem_Click);
      // 
      // Form1
      // 
      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
      this.BackColor = System.Drawing.Color.White;
      this.ClientSize = new System.Drawing.Size(292, 273);
      this.Menu = this.mainMenu1;
      this.Name = "Form1";
      this.Text = "Form1";
      this.MouseDown += new System.Windows.Forms.MouseEventHandler(
	       this.this_MouseDown);
      this.MouseUp += new System.Windows.Forms.MouseEventHandler(
	       this.this_MouseUp);

    }
		#endregion

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main() 
    {
      Application.Run(new Form1());
    }

    private void fileExitMenuItem_Click(object sender, 
	     System.EventArgs e)
    {
      this.Close(); 
    }

    private void fileNewMenuItem_Click(object sender, 
	     System.EventArgs e)
    {
      this.Controls.Clear();
    }

    private void fileOpenMenuItem_Click(object sender, 
	     System.EventArgs e)
    {
      OpenDocument();
    }

    private void fileSaveMenuItem_Click(object sender, 
	     System.EventArgs e)
    {
      Save();
    }

    private void fileSaveAsMenuItem_Click(object sender, 
	     System.EventArgs e)
    {
      SaveAs();
    }

    private void OpenDocument()
    {
      OpenFileDialog openFileDialog = new OpenFileDialog();

      if (openFileDialog.ShowDialog() == DialogResult.OK)
      {
        filename = openFileDialog.FileName;
        Stream myStream = openFileDialog.OpenFile();
        if (myStream != null)
        {
          this.Controls.Clear();
          IFormatter formatter = new BinaryFormatter();
          ArrayList mementos = (ArrayList) formatter.Deserialize(
		       myStream);
          IEnumerator mementoEnum = mementos.GetEnumerator();
          while (mementoEnum.MoveNext())
          {
            Memento memento = (Memento) mementoEnum.Current;
            Ellipse ellipse = new Ellipse();
            ellipse.SetMemento(memento);
            this.Controls.Add(ellipse);
          }
          myStream.Close();
          this.Refresh();
        }
      }
    }

    private bool Save() 
    {
      if (filename==null) 
      {
        return SaveAs();
      }
      else {
        Stream myStream ;
        myStream = File.OpenWrite(filename);
        if (myStream != null)
        {
          IFormatter formatter = new BinaryFormatter();
          // serialize shapes
          ArrayList mementos = new ArrayList();
          foreach (Control ctl in this.Controls)
          {
            mementos.Add( ((Ellipse)ctl).CreateMemento()); 
          }
          formatter.Serialize(myStream, mementos);
          myStream.Close();
          return true;
          }
        }
      return false;
    }

    private bool SaveAs() 
    {
      SaveFileDialog saveFileDialog = new SaveFileDialog();
      if (saveFileDialog.ShowDialog() == DialogResult.OK)
      {
        filename = saveFileDialog.FileName;
        return Save();
      }
      return false;
    }

    private void this_MouseDown(object sender, 
	     System.Windows.Forms.MouseEventArgs e)
    {
      if (e.Button==System.Windows.Forms.MouseButtons.Left) 
      {
        startPoint = new Point(e.X, e.Y);
      }
    }

    private void this_MouseUp(object sender, 
	     System.Windows.Forms.MouseEventArgs e)
    {
      if (e.Button==System.Windows.Forms.MouseButtons.Left) 
      {
        Point endPoint = new Point(e.X, e.Y);
        this.Controls.Add(new Ellipse(GetRectangleFromPoints(
		     startPoint, endPoint))); 
      }
      this.Refresh();
    }

    private Rectangle GetRectangleFromPoints(Point p1, Point p2) 
    {
      int x1, x2, y1, y2;
      if (p1.X < p2.X) 
      {
        x1 = p1.X;
        x2 = p2.X;
      }
      else 
      {
        x1 = p2.X;
        x2 = p1.X;
      }

      if (p1.Y < p2.Y) 
      {
        y1 = p1.Y;
        y2 = p2.Y;
      }
      else 
      {
        y1 = p2.Y;
        y2 = p1.Y;
      }
      // x2 > x1 and y2 > y1
      return new Rectangle(x1, y1, x2 - x1, y2 - y1);
    }
  }

}

Serializing the Objects

Serializing the objects is done in the Save method. The part that does this job is as follows.


      if (myStream != null)
        {
          IFormatter formatter = new BinaryFormatter();
          // serialize shapes
          ArrayList mementos = new ArrayList();
          foreach (Control ctl in this.Controls)
          {
            mementos.Add( ((Ellipse)ctl).CreateMemento()); 
          }
          formatter.Serialize(myStream, mementos);
          myStream.Close();
          return true;
          }
        }

First, it constructs a BinaryFormatter object.


IFormatter formatter = new BinaryFormatter();

Then, the method instantiates an ArrayList method that will contains the mementos of each Ellipse object.


ArrayList mementos = new ArrayList();

Next, it uses a foreach statement to iterate all the controls in the form's Controls collection, cast each control to Ellipse, and add them to the mementos ArrayList.


          foreach (Control ctl in this.Controls)
          {
            mementos.Add( ((Ellipse)ctl).CreateMemento()); 
          }

To serialize, we use the Serialize method of the IFormatter interface.


formatter.Serialize(myStream, mementos);

Deserializing the Objects

Deserializing the previously-persisted objects happens in the OpenDocument method. Here is the part that does that.


        if (myStream != null)
        {
          this.Controls.Clear();
          IFormatter formatter = new BinaryFormatter();
          ArrayList mementos = (ArrayList) formatter.Deserialize(
		       myStream);
          IEnumerator mementoEnum = mementos.GetEnumerator();
          while (mementoEnum.MoveNext())
          {
            Memento memento = (Memento) mementoEnum.Current;
            Ellipse ellipse = new Ellipse();
            ellipse.SetMemento(memento);
            this.Controls.Add(ellipse);
          }
          myStream.Close();
          this.Refresh();
        }

It starts by constructing a BinaryFormatter object.


IFormatter formatter = new BinaryFormatter();

Then, it calls the Deserialize method of the Iformatter interface to deserialize the object, and casts it into ArrayList.


ArrayList mementos = (ArrayList) formatter.Deserialize(myStream);

Next, we need to reconstruct the Ellipse objects. For each object, we cast it to Memento and create an Ellipse object. Reconstruction happens when we call the SetMemento method of the Ellipse class.


            Memento memento = (Memento) mementoEnum.Current;
            Ellipse ellipse = new Ellipse();
            ellipse.SetMemento(memento);

Finally, we add the Ellipse object to the form's Controls collection.


this.Controls.Add(ellipse);

Summary

In this article, we have learned to use the memento design pattern to serialize objects which are otherwise unfit to be persisted. In the memento pattern, the original object is called the originator, and we add two methods to the originator's class: CreateMemento and SetMemento. The CreateMemento method constructs a Memento object, populates it with states that need to be persisted, and returns the Memento object. The SetMemento method does the opposite and is called to construct the original object.

Budi Kurniawan is a senior J2EE architect and author.


Return to .NET DevCenter

Copyright © 2009 O'Reilly Media, Inc.