An Example Program
Example program UndoRedo, shown in Figure 1, uses SOAP (simple object access protocol) serializations to provide undo and redo features. Select a drawing tool from the toolbar. Then click and drag to draw a new object. Click and drag again to create more objects of the same type, or select a new tool.
Figure 1 Program UndoRedo uses SOAP serializations to provide undo and redo features.
After you have drawn something, the program enables the Edit menu's Undo command. Use that command to remove the last thing you drew. Use Undo repeatedly to remove other objects.
After you have undone an object, the program enables the Edit menu's Redo command. Use the Redo command to restore the last object you removed.
The File menu's Save As command lets you save your current drawing in a file. The Open command lets you load a saved picture.
SOAP
Example program UndoRedo uses a SoapFormatter to save its serializations in a SOAP format. I decided to use the SoapFormatter for two reasons:
First, the program saves objects that are subclassed from other objects. These classes confuse the XmlSerializer so that rules out XmlSerializer. That's a shame because XmlSerializer produces a more concise and readable result than SoapFormatter.
Second, the other candidate, BinaryFormatter, produces binary serializations that you cannot read. The program could use a binary serialization, but it's easier to debug a program when you can read and modify its output. When there's a problem saving or loading a serialization, you can take a look and see what's going wrong. You can even make changes to the text serialization to see what happens.
Serializations created by the BinaryFormatter take less space than SOAP serializations, so you may prefer the binary serialization when space is at a premium. You will probably be better off debugging your program with the SoapFormatter first and then switching to a BinaryFormatter after everything works. In most cases, the space saving is a minor issue anyway.
Listing 1 shows the SOAP serialization for a small drawing containing a rectangle and an ellipse. I have added indentation to make the result easier to read.
The SOAP-ENC:Array tag represents the program's main serialization object. The SOAP-ENC:ArrayType attribute indicates that this is an array containing three Drawable objects.
The three item tags contained inside the SOAP-ENC:Array tag represent the objects contained in the main Drawable array. The first item's xsi:null attribute indicates that its item is empty. In the program, the first item in the array is not used so the array does contain an empty entry. The second and third items refer to the XML elements named ref-3 and ref-4 that follow in the serialization.
The next a1:DrawableRectangle element represents the first drawing object. Its id attribute indicates that this is the ref-3 object to which the second item element refers. This item has four properties: X1, Y1, X2, and Y2.
The final object in the serialization is an a1:DrawableEllipse element. This is the ref-4 object to which the third item element refers. This item also has four properties: X1, Y1, X2, and Y2.
Listing 1. SOAP Serialization Representing a Rectangle and an Ellipse
<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/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/UndoRedo/UndoRedo"> <SOAP-ENV:Body> <SOAP-ENC:Array SOAP-ENC:arrayType="a1:Drawable[3]"> <item xsi:null="1"/> <item href="#ref-3"/> <item href="#ref-4"/> </SOAP-ENC:Array> <a1:DrawableRectangle id="ref-3"> <X1>23</X1> <Y1>31</Y1> <X2>262</X2> <Y2>204</Y2> </a1:DrawableRectangle> <a1:DrawableEllipse id="ref-4"> <X1>43</X1> <Y1>39</Y1> <X2>129</X2> <Y2>192</Y2> </a1:DrawableEllipse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
SoapFormatter Namespace
Normally, when you use a class such as SoapFormatter, you use an Imports statement at the beginning of the module to tell Visual Basic to include the class' namespace. Unfortunately, Visual Basic doesn't initially know about the SoapFormatter. To make it available, you need to add a reference to the DLL that defines it.
Right-click on the Solution Explorer's References entry, and select Add Reference. Locate the entry named System.Runtime.Serialization.Formatters.Soap.dll, double-click it, and click the OK button. Now, you can add this Imports statement to include the namespace:
Imports System.Runtime.Serialization.Formatters.Soap
Your program can now create SoapFormatter objects, as in the following code:
Dim soap_formatter As New SoapFormatter()
Drawable Class
The Drawable class and its derivative classes, shown in Listing 2, represent the objects drawn by the UndoRedo program. The Drawable class declares the Draw subroutine with the MustOverride key word, so the derived classes must implement their own versions of this routine. Drawable has four public variables, X1, Y1, X2, and Y2, which define the area the object occupies.
The DrawableEllipse, DrawableRectangle, and DrawableLine classes implement their Draw subroutines in different ways to draw different shapes.
Listing 2. The Drawable Class and its Derivatives Represent Drawing Objects
' Drawable is an object that can draw itself. <Serializable()> Public MustInherit Class Drawable ' Draw the specific shape. Public MustOverride Sub Draw(ByVal gr As Graphics) ' The bounding box. Public X1 As Integer Public Y1 As Integer Public X2 As Integer Public Y2 As Integer End Class <Serializable()> Public Class DrawableEllipse Inherits Drawable ' Draw the shape. Public Overrides Sub Draw(ByVal gr As Graphics) gr.DrawEllipse(Pens.Black, _ Math.Min(X1, X2), _ Math.Min(Y1, Y2), _ Math.Abs(X2 - X1), _ Math.Abs(Y2 - Y1)) End Sub End Class <Serializable()> Public Class DrawableRectangle Inherits Drawable ' Draw the shape. Public Overrides Sub Draw(ByVal gr As Graphics) gr.DrawRectangle(Pens.Black, _ Math.Min(X1, X2), _ Math.Min(Y1, Y2), _ Math.Abs(X2 - X1), _ Math.Abs(Y2 - Y1)) End Sub End Class <Serializable()> Public Class DrawableLine Inherits Drawable ' Draw the shape. Public Overrides Sub Draw(ByVal gr As Graphics) gr.DrawLine(Pens.Black, X1, Y1, X2, Y2) End Sub End Class
You could extend these classes to provide other drawing features such as fill style, fill color, outline color, drawing style, and so forth. The classes shown in Listing 2 are good enough for this example.
Snapshots
Listing 3 shows the UndoRedo program's code that deals most directly with snapshots. The m_DrawingObjects array holds the current picture's Drawable objects. For easier indexing, entry 0 in the array is allocated but not used.
The m_UndoStack collection holds the program's snapshots. Each entry in the collection is a serialization of the m_DrawingObjects array. The variable m_CurrentSnapshot gives the index in m_UndoStack of the snapshot representing the currently displayed drawing.
The Serialization property makes saving and restoring the program's drawing easy. The Serialization property get procedure returns a SOAP serialization for the m_DrawingObjects array. The Serialization property set procedure takes a serialization and deserializes it to reinitialize the m_DrawingObjects array.
When you select the File menu's Save As command, the mnuFileSaveAs_Click event handler saves the Serialization property into the selected file. Some programs, such as WordPad, empty the undo stack when you save. Other programs, such as Word, leave the undo buffer unchanged. This program takes the second approach.
When you select the File menu's Open command, the mnuFileOpen_Click event handler reads the contents of the file you select into a string. It then sets the program's Serialization property to that string, and the Serialization property set procedure uses the serialization from the file to restore the saved drawing.
The SaveSnapshot subroutine adds a snapshot of the current picture to the m_UndoStack collection. The routine begins by removing any snapshots that come after the current one. If you undo several changes and then draw a new shape, the program calls SaveSnapshot. At that point, the program discards the actions you undid earlier.
Next, SaveSnapshot saves the Serialization value into the m_UndoStack collection. If the stack is too big, the routine removes some of the oldest serializations. This program saves at most 10 serializations, so it is easy for you to see how this works. A real application could probably save far more snapshots. For this program, each Drawable object adds about 125 bytes to the serialization. If a picture has 100 Drawable objects, its serialization takes around 12,500 bytes. Saving 100 serializations of this size in the undo stack would take up about 1.25MB of memory. You could probably afford that much memory, although you might want to stop short of 1,000 serializations, which would take up about 12.5MB.
After shrinking the undo stack, if necessary, the event handler sets m_CurrentSnapshot to the index of the most recent snapshot and calls subroutine EnableUndoMenuItems. That routine enables or disables the Undo and Redo commands as appropriate.
The Undo subroutine decrements m_CurrentSnapshot so it points to the previous snapshot. It sets the Serialization property to that snapshot to restore the picture in its previous state and redraws the picture.
The Redo subroutine increments m_CurrentSnapshot so it points to the next snapshot. It sets the Serialization property to that snapshot and redraws the picture.
Listing 3. The UndoRedo Program Uses this Undo/Redo and File Saving/Opening Code
' The drawing objects. Private m_DrawingObjects() As Drawable Private m_MaxDrawingObject As Integer ' The undo stack. Private m_UndoStack As Collection Private m_CurrentSnapshot As Integer ' Get ready. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load ' Code omitted... ' Make an empty 1-entry array of drawing objects. ReDim m_DrawingObjects(0) m_MaxDrawingObject = 0 ' Make the empty undo stack. m_UndoStack = New Collection() m_CurrentSnapshot = 0 ' Save a blank snapshot. SaveSnapshot() End Sub ' Get or set our serialization. Private Property Serialization() As String ' Return a serialization for the current objects. Get Dim soap_formatter As New SoapFormatter() Dim memory_stream As New MemoryStream() ' Serialize the m_DrawingObjects. soap_formatter.Serialize(memory_stream, m_DrawingObjects) ' Rewind the memory stream to the beginning. memory_stream.Seek(0, SeekOrigin.Begin) ' Return a textual representation. Dim stream_reader As New StreamReader(memory_stream) Return stream_reader.ReadToEnd() End Get ' Load objects from the new serialization. Set(ByVal Value As String) Dim string_reader = New StringReader(Value) Dim soap_formatter As New SoapFormatter() ' Load the new objects. Dim memory_stream As New MemoryStream() Dim stream_writer As New StreamWriter(memory_stream) ' Write the serialization into the ' StreamWriter and thus the MemoryStream. stream_writer.Write(Value) stream_writer.Flush() ' Rewind the MemoryStream. memory_stream.Seek(0, SeekOrigin.Begin) ' Deserialize. m_DrawingObjects = soap_formatter.Deserialize(memory_stream) ' Save the new objects. m_MaxDrawingObject = m_DrawingObjects.GetUpperBound(0) ' Display the new objects. DrawObjects(picCanvas.CreateGraphics()) End Set End Property ' Open a saved file. Private Sub mnuFileOpen_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles mnuFileOpen.Click ' Let the user pick a file. If dlgOpen().ShowDialog = DialogResult.OK Then ' Set our serialization to the file's contents. Try Dim stream_reader As New StreamReader(dlgOpen.FileName) Me.Serialization = stream_reader.ReadToEnd() ' Remove all snapshots. m_CurrentSnapshot = 0 ' Save a snapshot of the new objects. SaveSnapshot() Catch exc As Exception MsgBox("Error loading file " & _ dlgOpen.FileName & vbCrLf & exc.Message) End Try End If End Sub ' Save the current drawing objects. Private Sub mnuFileSaveAs_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles mnuFileSaveAs.Click ' Let the user pick a file. If dlgSave().ShowDialog = DialogResult.OK Then ' Save the objects into the file. Try Dim stream_writer As New StreamWriter(dlgSave.FileName) stream_writer.Write(Me.Serialization) stream_writer.Close() Catch exc As Exception MsgBox("Error saving file " & _ dlgSave.FileName & vbCrLf & exc.Message) End Try End If ' Some programs flush the undo stack at this point. ' This program does not. End Sub ' Save a snapshot. Private Sub SaveSnapshot() Const MAX_SNAPSHOTS = 10 ' Remove all snapshots items after the current one. Do While (m_CurrentSnapshot < m_UndoStack.Count) m_UndoStack.Remove(m_CurrentSnapshot + 1) Loop ' Save the new snapshot. m_UndoStack.Add(Serialization) ' If we have too many snapshots, remove the oldest. Do While (m_UndoStack.Count > MAX_SNAPSHOTS) m_UndoStack.Remove(1) Loop ' Save the index of the current snapshot. m_CurrentSnapshot = m_UndoStack.Count ' Enable the proper undo/redo menu items. EnableUndoMenuItems() End Sub ' Enable the undo and redo menu commands appropriately. Private Sub EnableUndoMenuItems() mnuEditUndo.Enabled = (m_CurrentSnapshot > 1) mnuEditRedo.Enabled = (m_CurrentSnapshot < m_UndoStack.Count) End Sub ' Restore the previous snapshot. Private Sub Undo() ' Do nothing if there are no more snapshots. If m_CurrentSnapshot < 2 Then Exit Sub ' Restore the previous snapshot. m_CurrentSnapshot = m_CurrentSnapshot - 1 Serialization = m_UndoStack(m_CurrentSnapshot) ' Redraw. DrawObjects(picCanvas.CreateGraphics) End Sub ' Restore the next snapshot. Private Sub Redo() ' Do nothing if there are no more snapshots. If m_CurrentSnapshot >= m_UndoStack.Count Then Exit Sub ' Restore the next snapshot. m_CurrentSnapshot = m_CurrentSnapshot + 1 Serialization = m_UndoStack(m_CurrentSnapshot) ' Redraw. DrawObjects(picCanvas.CreateGraphics) End Sub
If you look closely at the Serialization property get procedure, you'll see that it doesn't depend on the structure of the m_DrawingObjects array that it is serializing. The procedure tells the SoapFormatter to serialize m_DrawingObjects, and the formatter figures out how to do that. Similarly, the Serialization property set procedure tells the SoapFormatter to re-create m_DrawingObjects from a serialization, and the SoapFormatter figures out how to do it.
This means you do not need to change the serialization code if you change the way the program stores its data. For example, if you change the definition of the Drawable class or its derived classes, or if you create new derived classes, the Serialization procedures will still work. You may need to do some work to load older files using the new class definitions, but the serialization code will run unchanged.
Download the source file here: