65.9K
CodeProject is changing. Read more.
Home

MDI Case Study Purchasing - Part IX - Undo / Redo

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (5 votes)

Dec 8, 2015

CPOL

5 min read

viewsIcon

10764

downloadIcon

318

Introduction

In Part IX we will complete our Undo and Redo functionality. To achieve this, we will create 2 stack collections, each containing PurchaseOrder objects, one for Undo states, and one for Redo states. Each time a change is made to the document, a copy of the object BEFORE the change will be added to the top of the Undo stack. When a change is "Undone" the current PurchaseOrder will be placed on the Redo stack, and then the topmost Undo object will become the live object. The reason we use stacks is because they offer the LIFO functionality we want, (L)ast (I)n (F)irst (O)out. Check out this graphic 

Figure 1

Figure 2

Figure 3

Now is when it will become more clear why we created both a Changing and Changed event. In the Changing event, we will place a clone of our current PurchaseOrder into the Undo stack BEFORE allowing the change to be made. It's important to remember, we are using an Object Oriented framework, thus our Data objects are reference objects. If we were to simply store our object in the stacks, we would of course be storing the references to them, and thus any changes made to the live object would also be made to the objects in the stacks, since they shared the same reference. That's where our Clone() method comes into play. Whenever we store a state into either of the stacks, or when we pull a state off of a stack and make it live, we want to clone the state so that a new reference instance is created. So let's create a Clone() method for our PurchaseOrder class. In this case we also want to be sure and create a "deep clone", so that any reference properties within PurchaseOrder are also cloned. Here is what the PurchaseOrder.Clone() method should look like

public PurchaseOrder Clone()
{
    PurchaseOrder clone = new PurchaseOrder();
    clone.PurchaseOrderNumber = this.PurchaseOrderNumber;
    clone.Vendor = this.Vendor;
    clone.VendorAddress = this.VendorAddress;
    clone.BillingAddress = new BillingAddress();
    clone.BillingAddress.Attention = this.BillingAddress.Attention;
    clone.BillingAddress.CompanyName = this.BillingAddress.CompanyName;
    clone.BillingAddress.AddressLine1 = this.BillingAddress.AddressLine1;
    clone.BillingAddress.AddressLine2 = this.BillingAddress.AddressLine2;
    clone.BillingAddress.City = this.BillingAddress.City;
    clone.BillingAddress.State = this.BillingAddress.State;
    clone.BillingAddress.ZipCode = this.BillingAddress.ZipCode;
    clone.BillingAddress.PhoneNumber = this.BillingAddress.PhoneNumber;
    clone.BillingAddress.FaxNumber = this.BillingAddress.FaxNumber;
    clone.ShippingAddress = new ShippingAddress();
    clone.ShippingAddress.Attention = this.ShippingAddress.Attention;
    clone.ShippingAddress.CompanyName = this.ShippingAddress.CompanyName;
    clone.ShippingAddress.AddressLine1 = this.ShippingAddress.AddressLine1;
    clone.ShippingAddress.AddressLine2 = this.ShippingAddress.AddressLine2;
    clone.ShippingAddress.City = this.ShippingAddress.City;
    clone.ShippingAddress.State = this.ShippingAddress.State;
    clone.ShippingAddress.ZipCode = this.ShippingAddress.ZipCode;
    clone.ShippingAddress.Reference = this.ShippingAddress.Reference;
    clone.Items = new ItemCollection();
    foreach (Item i in this.Items)
    {
        Item cloneItem = new Item();
        cloneItem.ProductNumber = i.ProductNumber;
        cloneItem.Description = i.Description;
        cloneItem.Cost = i.Cost;
        cloneItem.PurchaseQuantity = i.PurchaseQuantity;
        clone.Items.Add(cloneItem);
    }
    return clone;
}

Note - In our application, Vendor and Vendor Address needs to stay tied to the reference of the original, so in our Clone() method we don't want to clone those 2 properties, just pass them along. As an example, if we cloned the Vendor, then let's say we make a change to the document, and then the Vendor information changes, and then we undo the document change, the document would not have the old outdated Vendor data in it.

Next, we want to create a new class that will hold the state information for out document for each Undo and Redo action taken, let's call it UndoRedoState, in this class we would hold any information regarding the current state of the document, for now just the PurchaseOrder object. Here is what our new UndoRedoState class should look like

public class UndoRedoState
{
    public PurchaseOrder DataObject { get; set; }
    public UndoRedoState() { DataObject = new PurchaseOrder(); }
    public UndoRedoState(PurchaseOrder dataobject) { DataObject = dataobject; }
}

Now, we need to make a few changes to our PurchaseOrderForm class. First let's add 2 instances of Stack<UndoRedState> one for undo states, and one for redo states. Stack<T> can be found in System.Collections.Generic, so add a using statement for this library if you don't already have it

private Stack<UndoRedoState> _undoStates;
private Stack<UndoRedoState> _redoStates;

Next, we need a public bool CanUndo, and CanRedo, this will aid us in controlling the MDIForm UI undo and redo buttons when we toggle between documents

public bool CanUndo { get { return _undoStates.Count > 0; } }
public bool CanRedo { get { return _redoStates.Count > 0; } }

Now let's create an Undo() and Redo() method in PurchaseOrderForm, which will actually do the changes for us. Here is what they should look like

public void Undo()
{
    if (_undoStates.Count > 0)
    {
        UndoRedoState undoState = _undoStates.Pop();
        UndoRedoState redoState = new UndoRedoState(_purchaseOrder.Clone());
        _redoStates.Push(redoState);
        _purchaseOrder = undoState.DataObject.Clone();
        _purchaseOrder.Changing += purchaseOrder_Changing;
        _purchaseOrder.Changed += purchaseOrder_Changed;
        purchaseOrderBindingSource.DataSource = _purchaseOrder;
        purchaseOrderBindingSource.ResetBindings(false);
    }
}

public void Redo()
{
    if (_redoStates.Count > 0)
    {
        UndoRedoState redoState = _redoStates.Pop();
        UndoRedoState undoState = new UndoRedoState(_purchaseOrder.Clone());
        _undoStates.Push(undoState);
        _purchaseOrder = redoState.DataObject.Clone();
        _purchaseOrder.Changing += purchaseOrder_Changing;
        _purchaseOrder.Changed += purchaseOrder_Changed;
        purchaseOrderBindingSource.DataSource = _purchaseOrder;
        purchaseOrderBindingSource.ResetBindings(false);
    }
}

Lastly, we need to make sure each time a change is occuring, we get a pre-change copy of the data object into our Undo stack, and also we want to clear out the redo stack. Before we can edit the Changing handler, however, we need to make a small change to our PurchaseOrderChangingEventArgs class. We want it to contain an instance of PurchaseOrder, which will be the current state of the data object before the impending change is made, so open up EventArgs.cs and change PurchaseOrderChaningEventArgs to 

public class PurchaseOrderChangingEventArgs : EventArgs
{
    public PurchaseOrder CurrentState { get; set; }
    public PurchaseOrderChangingEventArgs(PurchaseOrder currentState) { CurrentState = currentState; }
}

That will give 3 build errors now in PurchaseOrder because we've changed the contructor to now require an argument, so let's fix them, in PurchaseOrder, change the PurchaseOrderNumber get/set method to

public String PurchaseOrderNumber
{
    get { return _purchaseOrderNumber; }
    set 
    {
        if (Changing != null) OnChanging(new PurchaseOrderChangingEventArgs(this.Clone()));
        _purchaseOrderNumber = value;
        if (Changed != null) OnChanged(new PurchaseOrderChangedEventArgs());
    }
}

 

Now that we are using Clone, each time we set a property and call OnChanging we would end up with a circular reference, and an infinite loop, so we need to add the if(Changing != null) check to avoid this.

Now change the DeleteItems and AddItems methods to the following

public void AddItems(ItemCollection addItems)
{
    OnChanging(new PurchaseOrderChangingEventArgs(this.Clone()));

    foreach (Item i in addItems) _items.Add(i);

    OnChanged(new PurchaseOrderChangedEventArgs());
}

public void DeleteItems(ItemCollection deleteItems)
{
    OnChanging(new PurchaseOrderChangingEventArgs(this.Clone()));

    foreach (Item i in deleteItems) _items.Remove(i);

    OnChanged(new PurchaseOrderChangedEventArgs());
}

Now we need to add out current state into the Undo stack when PurchaseOrder changes, and for this, let's modify the purchaseOrder_Changing handler inside PurchaseOrderForm. We want to add an Undo state, as well as flush all current Redo states

private void purchaseOrder_Changing(object sender, PurchaseOrderChangingEventArgs e)
{
    UndoRedoState undoState = new UndoRedoState(e.CurrentState);
    _undoStates.Push(undoState);
    _redoStates.Clear();
    OnChanging(e);
}

Now we have the guts of Undo/Redo done, all we need to do is handle the UI buttons. Lets move on to MDIForm. In Design View, set the Undo and Redo menu items Enabled property to False, then double click each of them to create their handlers, and all we need to do is get the ActiveMdiChild, and call it's Undo or Redo method, then reset the buttons' Enabled property accordingly

private void undoToolStripMenuItem_Click(object sender, EventArgs e)
{
    PurchaseOrderForm activeChildForm = this.ActiveMdiChild as PurchaseOrderForm;
    activeChildForm.Undo();
    undoToolStripMenuItem.Enabled = activeChildForm.CanUndo;
    redoToolStripMenuItem.Enabled = activeChildForm.CanRedo;
}

private void redoToolStripMenuItem_Click(object sender, EventArgs e)
{
    PurchaseOrderForm activeChildForm = this.ActiveMdiChild as PurchaseOrderForm;
    activeChildForm.Redo();
    undoToolStripMenuItem.Enabled = activeChildForm.CanUndo;
    redoToolStripMenuItem.Enabled = activeChildForm.CanRedo;
}

Next, let's go to OnMdiChildActivate and set our Undo and Redo buttons to enabled when appropriate. First at the top, set both items Enabled to False.

protected void OnMdiChildActivate(object sender, EventArgs e)
{
   undoToolStripMenuItem.Enabled = redoToolStripMenuItem.Enabled = false;
   .....

And in the if(this.ActiveMdiChild != null) block

if (this.ActiveMdiChild != null)
{
   .....
   undoToolStripMenuItem.Enabled = activeChildForm.CanUndo;
   redoToolStripMenuItem.Enabled = activeChildForm.CanRedo;
​}

And finally, in the purchaseOrderForm_Changed handler, let's add a check for enabling or disabling the Undo and Redo button 

private void purchaseOrderForm_Changed(object sender, PurchaseOrderChangedEventArgs e)
{
    PurchaseOrderForm activeChildForm = (PurchaseOrderForm)this.ActiveMdiChild;
    PurchaseOrderForm sendingForm = sender as PurchaseOrderForm;
    if (activeChildForm == sendingForm)
    {
        saveToolStripButton.Enabled = saveToolStripMenuItem.Enabled = true;
        selectAllToolStripMenuItem.Enabled = activeChildForm.PurchaseOrder.Items.Count > 0;
        undoToolStripMenuItem.Enabled = activeChildForm.CanUndo;
        redoToolStripMenuItem.Enabled = activeChildForm.CanRedo;
    }
}

That completes Part IX, Undo and Redo funictionality is now complete. This is the basic framework for the functionality. As you expand your data object you will need to be sure to account for each change, and expand the UndoRedoState class to hold all of the state information. In Part X we will handle premature closure of our application, so that unsaved changes can be caught and the user can be alerted to save before closure.

Points of Interest

  • Undo / Redo
  • Stack<T>

History

Keep a running update of any changes or improvements you've made here.