Provide undo and redo in C#

[undo]

This example adds undo and redo features to the example Save and restore pictures drawn by the user in C#. It’s not terribly hard, but it is long so I’ll break it into sections to make it easier to understand.

That example uses XML to serialize and deserialize pictures drawn by the user. Once you can serialize and deserialize the data used by an application, providing undo and redo is relatively easy. Whenever the user makes a change, you can serialize the data to make a snapshot. To undo a change you restore the most recent snapshot. To redo, you reapply the snapshot you took before the undo.


Saving Snapshots

This example uses the following undo and redo lists.

// The undo and redo history lists.
private Stack<string> UndoList = new Stack<string>();
private Stack<string> RedoList = new Stack<string>();

Whenever the user makes a change, the program calls the following SaveSnapshot method to save a snapshot of the current data.

// Save a snapshot in the undo list.
private void SaveSnapshot()
{
    // Save the snapshot.
    UndoList.Push(GetSnapshot());

    // Empty the redo list.
    if (RedoList.Count > 0) RedoList = new Stack();

    // Enable or disable the Undo and Redo menu items.
    EnableUndo();
}

This code calls the GetSnapshot method to get a serialization of the data. (That method simply serializes the data. See the previous post for details about how it works.) It pushes that serialization onto the UndoList.

If the RedoList is not empty, the code creates a new empty RedoList. (If the user undoes some changes, they are saved in the RedoList so the user can reapply them. If the user then makes a change, this code discards the RedoList snapshots.)

This method finishes by calling the following EnableUndo method.

// Enable or disable the Undo and Redo menu items.
private void EnableUndo()
{
    mnuEditUndo.Enabled = (UndoList.Count > 0);
    mnuEditRedo.Enabled = (RedoList.Count > 0);
}

This method simply enables the undo or redo lists if there are snapshots in the corresponding lists.


Undo

The following code shows how the program undoes a change.

// Undo.
private void mnuEditUndo_Click(object sender, EventArgs e)
{
    // Move the most recent change to the redo list.
    RedoList.Push(UndoList.Pop());

    // Restore the top item in the Undo list.
    RestoreTopUndoItem();

    // Enable or disable the Undo and Redo menu items.
    EnableUndo();
}

When the user selects the Undo menu item, this code pops the most recent serialization from the undo list and pushes it onto the redo list. It then calls the following RestoreTopUndoItem method to restore the previous serialization and finishes by calling EnableUndo to enable the appropriate menu undo and redo items.

// Use an XML serialization to load a drawing.
private void RestoreTopUndoItem()
{
    if (UndoList.Count == 0)
    {
        // The undo list is empty. Display a blank drawing.
        Polylines = new List<Polyline>();
    }
    else
    {
        // Restore the first serialization from the undo list.
        XmlSerializer xml_serializer =
            new XmlSerializer(Polylines.GetType());
        using (StringReader string_reader =
            new StringReader(UndoList.Peek()))
        {
            List<Polyline> new_polylines =
                (List<Polyline>)
                    xml_serializer.Deserialize(string_reader);
            Polylines = new_polylines;
        }
    }
    picCanvas.Refresh();
}

This code uses the undo list’s Peek method to get the most recent serialization without removing it from the list. The code then deserializes it and displays the result.


Redo

The following code shows how the program reapplies an undone change.

// Redo.
private void mnuEditRedo_Click(object sender, EventArgs e)
{
    // Move the most recently undone item back to the undo list.
    UndoList.Push(RedoList.Pop());

    // Restore the top item in the Undo list.
    RestoreTopUndoItem();

    // Enable or disable the Undo and Redo menu items.
    EnableUndo();
}

This code removes the most recent serialization from the redo list (which is the serialization that was most recently undone) and pushes it back onto the undo list. It then calls RestoreTopUndoItem. That method restores the top serialization in the undo list, so it reapplies this serialization.

That’s all there is to it. As I said at the beginning, it’s not really all that complicated. Most of the real work is done by the code that does serialization and deserialization. See the previous example for information about how that works.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in drawing, graphics, serialization and tagged , , , , , , , , , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

10 Responses to Provide undo and redo in C#

  1. Pingback: Provide autosave in C# - C# HelperC# Helper

  2. Aso says:

    Dear Sir
    You are greate
    I do appritiate all your coding…
    I do have a question:
    Is it possible to save the list to a database and then retrive it again to our pciture box (redraw it) instead of serialization, which can be lost on our computer…
    I do appritiate you answer

    • RodStephens says:

      You can do that, but it will be relatively slow because the serializations can be fairly large and database access isn’t terribly fast. In fact ADO.NET (the latest .NET DB tool) only loads and saves data in batches. It’s not really designed to save many changes frequently.

      What I would do is save a snapshot every now and then. Perhaps every 10 or 20 changes, or perhaps every few minutes. That way if the program crashes, you can restore most of the picture.

      I would probably store those backups in text files rather than the database so they will be easier to manage. You could store a picture’s backup in a file with the same name and a .bak extension and then overwrite it whenever you save a new backup. The user can then open that file with the program if necessary. (You might also want the program to look for a .bak file when you start, see if it is newer than the most recent version of the file, and let the user revert to that version if desired.)

      I would still store the undo and redo lists within the program. That’s much faster and won’t fragment the database.

      (In fact, this whole thing might make a good example. I’ll look more at it when I have the time.)

  3. Aso says:

    Dear Sir
    I try this code bellow, but without any result, could you please tell my why?
    I have a rectangle shape in WPF C#, which’s filled with an imagebrush as you see, and I used inline if, but it doesn’t worked as I planed…

    Rec1.Fill = Rec1.Fill ==
        new ImageBrush(new BitmapImage(new Uri(@"C:\1.png", UriKind.Relative))) ?
        new ImageBrush(new BitmapImage(new Uri(@"C:\2.png", UriKind.Relative))) :
        new ImageBrush(new BitmapImage(new Uri(@"C:\1.png", UriKind.Relative)));

    Note that:
    Rec1 is my Rectangle;
    ImageBrush source is 1.png and I want it to be changed to 2.png
    That’s all I want…
    but it doesn’t worked..
    could you please tell me why?

    My regards for you as always…

    • RodStephens says:

      The problem is that brushes are class objects. If you compare two objects using ==, by default they are equal if they are exactly the same instance of the class. In this example you have multiple different brushes that happen to use the same images, but they are different objects.

      Practically speaking it would be hard to compare two image brushes to see it they give the same result. You might need to compare every pixel of the brushes’ images. So the default behavior is compare only their references to see if they point to the same object.

      For this program, load the brushes into class-level variables when the program starts and then use those instead of creating new brushes each time. That way they are the same object so == will detect that.

      Declare the brushes outside of any method like this.

      private ImageBrush Brush1 = new ImageBrush(new BitmapImage(new Uri(@"C:\1.png", UriKind.Relative)));
      private ImageBrush Brush2 = new ImageBrush(new BitmapImage(new Uri(@"C:\2.png", UriKind.Relative)));

      Then in the method that changes the brush, use code like this:

      Rec1.Fill = Rec1.Fill == Brush1 ? Brush2 : Brush1;

      Now the program can compare Rec1.Fill to the same instances of the brushes every time.

  4. Aso says:

    my greeting
    I have made a DataTable to store ListBox’s Item (convert each item to an array), then loop through all DataTable.Rows. thereafter insert DataTable.Rows to a DataBase.
    But it doesn’t work….It doesn’t fill the DataTable.Rows….

    DTable = new DataTable();
    DTable.Columns.Add("PName");
    DTable.Columns.Add("DName");
    DTable.Columns.Add("TType");
    DTable.Columns.Add("From");
    DTable.Columns.Add("To");
    
    string[] arr = new string[LBox.Items.Count];
    for (int i = 0; i < LBox.Items.Count; i++)
    {
        arr[i] = LBox.Items[i].ToString();
        string[] str1 = arr[i].ToString().Split(' ');
        DataRow Drow = DTable.NewRow();
        Drow["PName"] = PName.Text;
        Drow["DName"] = DName.Text;
        Drow["TType"] = str1[0];
        Drow["From"] = str1[1];
        Drow["To"] = str1[2];
         i++;
    }
    string sql = "";
    for (int j = 0; j < DTable.Rows.Count; j++)
    {
        sql = "insert into Operation_Tbl (PName, DName, TType,From,To) values('"
        +
        DTable.Rows[j]["PName"].ToString().Trim() + "','"
        +
        DTable.Rows[j]["DName"].ToString().Trim() + "','"
         +
        DTable.Rows[j]["TType"].ToString().Trim() + "','"
         +
        DTable.Rows[j]["From"].ToString().Trim() + "','"
        +
        DTable.Rows[j]["To"].ToString().Trim() + "')";
     
        using (OleDbConnection con = new OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source =" + System.IO.Directory.GetCurrentDirectory() + @"../../../DBase/Dentist_DBase.accdb"))
        {
            con.Open();
            OleDbCommand cmd = new OleDbCommand(sql, con);
            cmd.ExecuteNonQuery();
        }
    }
    • RodStephens says:

      I see a couple of problems.

      First, the NewRow method creates a new row for a table but does not add the row to the table. After you set the new row’s fields, you need to add this line:

      DTable.Rows.Add(Drow);

      Second, in the first for loop, the for statement increments i and later the loop includes the line i++ so i is incremented twice. That will make the code skip half of the lines in the list box. You should remove the i++ statement inside the loop.

      Next, you have the using OleDbConnection statement inside a loop. That should work, but there’s no need to create, open, use, and dispose off the connection every time through the loop. Move the using statement so it encloses the loop. The program should open the connection, loop through the records, and then close the connection.

      You’re also not really using the table for much. If you’re just going to use it to create INSERT statements, then you may as well use the first loop to create and execute those statements and not bother with the table. (Unless some other piece of code that is not shown here is using the table.) But you could use the table to create all of the records at once without using the second loop to create INSERT statements. See this example:

      Updating Data Sources with DataAdapters

      It may be a bit harder to figure out, but it may also be more efficient.

      I hope that helps.

  5. Aso says:

    I don’t know what to say…
    Its very kind of you
    I do appreciate your reply
    Thank you very much dear Rod Stephens

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.