Click here to Skip to main content
15,883,901 members
Articles / Programming Languages / C#

Undo/Redo Method for Databound Objects

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
4 Oct 2012CPOL7 min read 24.2K   322   13   6
Method for implementing Undo/Redo functionality using Databound objects.

Introduction 

So, this is an approach that I've developed, through some research in other topics, to implement Undo/Redo functionality for Windows Forms applications using Databound objects.

Background 

I explored both the Command and Memento based structures, but neither, at least for me, seemed to fit the bill for what I needed. What this approach does is implement an event for any change in the object, that first passes the current state of the object into the event args before making the change. These state objects are then stored in both an Undo and Redo Stack<T> collection. Undo and Redo functions pop off the last state, and bind it to the controls of the form.

Using the code

There are three change types that I am interested in capturing in my Undo/Redo stacks for this application:

  • Property value changes. 
  • Additions or subtractions to List<T> collections nested in my data objects. 
  • Property value changes in the data objects kept in the nested List<T> collections.

First, let me lay out the structure of my example:

  • Restaurant - this is the main data object, it will be the single instance for each form
    • Name, City, State - all String properties, of which changes need to be captured
    • HealthScores - List<HealthScore> collection, of which two types of changes are captured
      • Addition or removal of HealthScore objects into the collection
      • HealthScore data object has one property, of type int - Value - we need to capture changes to this value 

So first of all, we have the Restaurant data object, for which we need the three properties above, and an event to capture their changes:

C#
using System;
 
namespace RestaurantScrores
{
    // This delegate event handles our Restaruant's property changes
    public delegate void RestaurantChangedEventHandler(object sender, RestaurantChangedEventArgs e);
 
    // This event class will contain our state before the change
    public class RestaurantChangedEventArgs : EventArgs
    {
         private Restaurant _oldvalue;
         public RestaurantChangedEventArgs(Restaurant oldvalue) { _oldvalue = oldvalue.DeepClone(); }
         public Restaurant OldValue { get { return _oldvalue; } }
    }
 
    // This is our Restaurant data object - currently incomplete - 
    // we'll add the scores collection later
    public class Restaurant
    {
         public String _name, _city, _state;
 
         // Here is our event
         public event RestaurantChangeEventHandler Changed;
 
         public Restaurant() { _name = _city = _state = String.Empty; }
 
         // Here is the important part - we need 2 setters for each property, 
         // one that triggers the event, and one that doesn't
         public String Name 
         { 
            get { return _name; } 
            set { OnChanged(new RestaurantChangedEventArgs(this)); _name = value; } 
         }
 
         // And here is our silent setter, that will not call the event
         public String NameSilent { set { _name = value; } }
 
         public String City 
         { 
            get { return _city; } 
            set { OnChanged(new RestaurantChangedEventArgs(this)); _city = value; }
         }
         public String CitySilent { set { _city = value; } }
 
         public String State
         { 
            get { return _state; } 
            set { OnChanged(new RestaurantChangedEventArgs(this)); _state = value; } 
         }
         public String State { set { _state = value; } }
 
         // And here is our event trigger
         private void OnChanged(RestaurantChangedEventArgs e) 
         { 
            if (Changed != null) Changed(this, e); 
         }
     }
}

Notice that we are having the EventArgs create a clone for us using the DeepClone() method in our Restaurant object. Storing a clone is vital so that we do not continue to edit the same referenced object that gets stored in the Undo and Redo stacks. This is why its important to have the Silent setter method, because we use that call when creating the clone, so that each clone creation for the Undo and Redo stacks doesn't cause a loop in the normal setter. Here is an example of our DeepClone() method, which is a public method we need to put in any data object we need to store in an Undo/Redo state:

C#
public Restaurant DeepClone()
{
   Restaurant clone = new Restaurant();
   clone.NameSilent = this.Name;
   clone.CitySilent = this.City;
   clone.StateSilent = this.State;
   return clone;
}

This is a very crude example, and on a more sophisticated model, storing the actual object may prove less effective. In this case, you may chose to serialize each state into an XML string, and store that string into the Undo and Redo stacks, deserializing them when needed. If this is the case, then its not necessary to have the Silent setter. You can also choose to create your clone by serializing it into a memory stream, and then deserializing it back into an object, this will break the reference link, creating a clone. Here I only have a few properties, so the setting method is not too bad, but should you have a large complex data object, serialization is probably the best method for cloning. 

So, now we need to declare a data object in our form, and hookup its events: 

C#
private Stack<Restaurant> UndoStack;
private Stack<Restaurant> RedoStack;
private Restaurant _dataobject;
 
public event RestaurantChangedEventHandler Changed;
 
public RestaurantForm() 
{ 
   UndoStack = new Stack<Restaurant>();
   RedoStack = new Stack<Restaurant>();
   _dataobject = new Restaurant();
   _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed); 
   InitializeComponents();
   this.restaurantBindingSource.DataSource = _dataobject;
}
 
private void _dataobject_Changed(object sender, RestaurantChangedEventArgs e)
{
   // a call to this event indicated a new change, therefore we need to Push the
   // OldValue state of the EventArgs into the UndoStack, and clear the RedoStack
   UndoStack.Push(e.OldValue);
   RedoStack.Clear();
   
   // this is also a good time to set any undo/redo button or menu item states, ie
   undobutton.Enabled = true;
   redobutton.Enabled = false;
   
   // this would also be a good time to re-evaluate the states of any Save or SaveAs buttons
   savebutton.Enabled = tre
}

Next, the Undo and Redo methods, have to do the following:

  • Pop the latest state off of the Undo or Redo stack
  • Store a clone of this state into the opposite stack, either Undo or Redo stack by Push
  • Set the _dataobject to this new state
  • Set the DataSource of all related BindingSource objects
  • Hookup the changed event for the new object state
C#
private void undobutton_Click(object sender, EventArgs e)
{
    _dataobject = UndoStack.Pop();
    RedoStack.Push(_dataobject.DeepClone());
    this.restaurantBindingSource.DataSource = _dataobject;
    this.restaurantBindingSource.ResetBindings(false);
    _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed); 
    
    // also lets re-evaluate the states of our buttons here
    undobutton.Enabled = UndoStack.Count > 0;
    redostack.Enabled = RedoStack.Count > 0;
    savebutton.Enabled = true;
} 
 
private void redobutton_Click(object sender, EventArgs e)
{
    _dataobject = RedoStack.Pop();    
    UndoStack.Push(_dataobject.DeepClone());
    this.restaurantBindingSource.DataSource = _dataobject;
    this.restaurantBindingSource.ResetBindings(false);
    _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed);
    
    // also lets re-evaluate the states of our buttons here
    undobutton.Enabled = UndoStack.Count > 0;
    redostack.Enabled = RedoStack.Count > 0; 
    savebutton.Enabled = true;  
}

Nested Object Events 

So far, we've handled the basic property changes for our main data bound object. But let's say we want to have a nested object in our main object, in this case, a List<HealthScore> collection, to hold HealthScores for this restaurant. For simplicity sake I've only put the one int property in this class, but realistically this nested object would have much more detail, perhaps even its own nested objects. Seeing how we handle this one nested object, you can extrapolate how to handle further levels of nesting. Basically, we will be bubbling up our change events. Since our form is doing the Undo/Redo functions, and they are doing so by capturing our Restaurant.Changed event, we have to be sure to bubble nested change events up into calls for the Restaurant.Changed event trigger.

So first, lets describe our HealthScore object:

C#
public delegate void HealthScoreChangedEventHandler(object sender, HealthScoreChangedEventArgs e);
public class HealthScoreChangedEventArgs : EventArgs
{
     private HealthScore _oldvalue;
     public HealthScoreChangedEventArgs(HealthScore oldvalue) { _oldvalue = oldvalue.DeepClone(); }
     public HealthScore OldValue { get { return _oldvalue; } }
}

public class HealthScore
{
     private int _value;
     public event HealthScoreChangedEventHandler Changed;
     public HealthScore() { _value = 0; }
     public HealthScore(int value) { _value = value; }
     public int Value
     {
          get { return  _value; }
          set { OnChanged(new HealthScoreChangedEventArgs(this)); _value = value; }
     }
     public int ValueSilent { set { _value = value; } }
     private void OnChanged(HealthScoreChangedEventArgs e)
     {
          if ( Changed != null ) Changed(this, e);
     }
     public HealthScore DeepClone()
     {
          HealthScore clone = new HealthScore();  
          clone.ValueSilent = this.Value;
          return clone;
     } 
}

Now, we have to bubble up this Changed event to our Restaurant object. We need to make some changes to our Restaurant class. 

C#
// First we need to add the collection that will hold our HealthScores  
private List<HealthScore> _scores;

// We need to initialize this collection in our ctor
public Restaurant()
{
     // Add this line to initialize 
     _scores = new List<HealthScore>();
} 

// Again, our normal getter/setter, and the silent setter for the collection
public List<HealthScore> Scores
{
     get { return _scores; }
     set { OnChanged(new RestaurantChangedEventArgs(this)); _scores = value; }
}
public List<HealthScore> ScoresSilent { set { _scores = value; } }

// Now we need a method for adding scores from our form, that will trigger the changed event
public void Add(HealthScore score)
{
     OnChanged(new RestaurantChangedEventArgs(this));
     
     // We want to attach a handler for any events in the score before adding it
     score.Changed += new HealthScoreChangedEventHandler(score_Changed);

     _scores.Add(score);
}

// And finally, the handler that will bubble up our HealthScore change events
private void score_Changed(object sender, HealthScoreChangedEventArgs e)
{
     // if you have anything else to handle in your nested object on a change, do so now

     // Now bubble up the event
     OnChanged(new RestaurantChangedEventArgs(this));
}

Now whatever functionality you use in your form for adding the new score, just have it call this Add method, and the change bubbles up. Now, if the reference on our data object changes, ie, in the Undo and Redo methods, the events have to be set back up in the new reference. We already covered that for our main data object. We need one final method in our Restaurant class that will hookup our HealthScore events.

C#
// One final method to have in our Restaurant class, to reattach events
public void HookupHealthScoreEvents()
{
     foreach (HealthScore hs in _scores)
         hs.Changed += new HealthScoreChangedEventHandler(score_Changed);
}

Now back to our Undo and Redo methods, we need to account for changes in our nested object.

C#
private void undobutton_Click(object sender, EventArgs e)
{
    _dataobject = UndoStack.Pop();
    RedoStack.Push(_dataobject.DeepClone());
    this.restaurantBindingSource.DataSource = _dataobject;
    this.restaurantBindingSource.ResetBindings(false);
    _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed); 

    // Now we need to hook our data source
    // and events back up for the new data object scores
    this.healthScoresBindingSource.DataSource = _dataobject.Scores;
    this.healthScoresBindingSource.ResetBindings(false);
    _dataobject.HookupHealthScoreEvents();
    
    // also lets re-evaluate the states of our buttons here
    undobutton.Enabled = UndoStack.Count > 0;
    redostack.Enabled = RedoStack.Count > 0;
    savebutton.Enabled = true;
} 
 
private void redobutton_Click(object sender, EventArgs e)
{
    _dataobject = RedoStack.Pop();    
    UndoStack.Push(_dataobject.DeepClone());
    this.restaurantBindingSource.DataSource = _dataobject;
    this.restaurantBindingSource.ResetBindings(false);
    _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed);
    // Now we need to hook our data source and events back up for the new data object scores
    this.healthScoresBindingSource.DataSource = _dataobject.Scores;
    this.healthScoresBindingSource.ResetBindings(false);
    _dataobject.HookupHealthScoreEvents();

    // also lets re-evaluate the states of our buttons here
    undobutton.Enabled = UndoStack.Count > 0;
    redostack.Enabled = RedoStack.Count > 0; 
    savebutton.Enabled = true;  
}

Points of Interest 

This method has worked well for me, in my applications. I am not sure how deep the nesting can go before this method becomes inefficient, if it does. 

Again, this is my approach to this functionality, given the need to capture and store states before data is bound to the data object, so that the undo state can be properly reset. It may be that a better method exists, and I am totally open to learning what it is if anyone has any feedback.

Update

I have updated the Demo Form to include a tool bar, with a SplitButton that accumulates Undo and Redo steps so that the user can roll back to certain states. Again very crude in the examples, but you can see how to extrapolate the example into your own application. 

First, I have added a property to both RestaurantChangedEventArgs, and HealthScoreChangedEventArgs, called Description, which will hold a String describing the change made. This description we will use as the Text component of the ToolStripItems we add to the SplitButtons 

C#
public String Description { get { return _description; } set { _description = value; } }  

You can customize this Message anytime you call OnChanged(); Just pass into the EventArgs a string specific to the call you are making, such as the property name thats changing here, or if you're adding or removing, or whatever. For example, when we have a new value set to Restaurant.Name, in the setter we could have:

public String Name
{
     get { return _name; }
     set { OnChanged(new RestaurantChangedEventArgs(this, "Restaurant.Name"); _name = value; }
} 

So, I added a tool bar, with an Undo and Redo SplitButton. We need to handle two events from each of these. The Click event, and DropDownItemClick event. I have attached the click event to the same event as the Undo and Redo buttons on the form. Just clicking the button portion of the SplitButton will do the same thing as the Undo and Redo Buttons. If the user chooses a particular roll back step, then we handle that with the DropDownItemClick events:

First we need to manage our SplitButtons in our already existing events, RestaurantChanged, UndoButtonClick, and RedoButtonClick

 First the Restaurant.Changed event, which in our example is _dataobject_Changed

// When a new change occurs in our data object, we need to add a new undo state
// drop down item, and clear any drop down items from the redo split button
//
// Add to the _dataobject_Changed handler 


this.redolevelbutton.DropDownItems.Clear();

// e.Message here is the change specific string we pass from the ChangedEventArgs
// from the setter example above, a change in Restaurant.Name would result in
// "Undo change in: Restaurant.Name" being our undo drop down item text
this.undolevelbutton.DropDownItems.Add("Undo change in: " + e.Message);    

// and to the section handling button states, add
this.undolevelbutton.Enabled = _undostack.Count > 0;
this.redolevelbutton.Enabled = _redostack.Count > 0; 

Anytime we undo or redo, we want to roll the most current change drop down item from one split button to the other, from undo to redo whenever we undo, and from redo to undo whenever we redo. To do this, add the following to our undobutton_Click and redobutton_Click events: 

// For the undobutton_Click event, add the lines
ToolStripItem redoitem = this.undolevelbutton.DropDownItems[this.undolevelbutton.DropDownItems.Count - 1];
redoitem.Text = redoitem.Text.Replace("Redo", "Undo");
this.undolevelbutton.DropDownItems.Remove(redoitem);
this.redolevelbutton.DropDownItems.Add(redoitem);

// For the redobutton_Click event, add the lines
ToolStripItem undoitem = this.redolevelbutton.DropDownItems[this.redolevelbutton.DropDownItems.Count - 1];
undoitem.Text = undoitem.Text.Replace("Redo", "Undo");
this.redolevelbutton.DropDownItems.Remove(undoitem);
this.undolevelbutton.DropDownItems.Add(undoitem); 

Now for the meat of this change, the DropDownItemClick event handlers: 

private void undolevelbutton_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
{
      Restaurant restorestate;
      ToolStripItem redoitem;

      while (this.undolevelbutton.DropDownItems[this.undolevelbutton.DropDownItems.Count - 1] != e.ClickedItem)
      {
          restorestate = _undostack.Pop().DeepClone();
          _redostack.Push(_dataobject.DeepClone());
          _dataobject = restorestate.DeepClone();

          // moving last drop down item from undo to redo split button
          redoitem = this.undolevelbutton.DropDownItems[this.undolevelbutton.DropDownItems.Count - 1];
          redoitem.Text = redoitem.Text.Replace("Redo", "Undo");
          this.undolevelbutton.DropDownItems.Remove(redoitem);
          this.redolevelbutton.DropDownItems.Add(redoitem);
      }

      restorestate = _undostack.Pop().DeepClone();
      _redostack.Push(_dataobject.DeepClone());
      _dataobject = restorestate.DeepClone();
      _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed);
      this.restaurantBindingSource.DataSource = _dataobject;
      this.restaurantBindingSource.ResetBindings(false);
      _dataobject.HookupHealthScoreEvents();
      this.scoresBindingSource.DataSource = _dataobject.Scores;
      this.scoresBindingSource.ResetBindings(false);

      redoitem = this.undolevelbutton.DropDownItems[this.undolevelbutton.DropDownItems.Count - 1];
      redoitem.Text = redoitem.Text.Replace("Redo", "Undo");
      this.undolevelbutton.DropDownItems.Remove(redoitem);
      this.redolevelbutton.DropDownItems.Add(redoitem);

      undobutton.Enabled = _undostack.Count > 0;
      undolevelbutton.Enabled = _undostack.Count > 0;
      redobutton.Enabled = _redostack.Count > 0;
      redolevelbutton.Enabled = _redostack.Count > 0;
      savebutton.Enabled = true;
}

private void redolevelbutton_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
{
      ToolStripItem undoitem;
      Restaurant restorestate;

      while (this.redolevelbutton.DropDownItems[this.redolevelbutton.DropDownItems.Count - 1] != e.ClickedItem)
      {
          restorestate = _redostack.Pop().DeepClone();
          _undostack.Push(_dataobject.DeepClone());
          _dataobject = restorestate.DeepClone();

          // moving last drop down item from undo to redo split button
          undoitem = this.redolevelbutton.DropDownItems[this.redolevelbutton.DropDownItems.Count - 1];
          undoitem.Text = undoitem.Text.Replace("Redo", "Undo");
          this.redolevelbutton.DropDownItems.Remove(undoitem);
          this.undolevelbutton.DropDownItems.Add(undoitem);
      }

      restorestate = _redostack.Pop().DeepClone();
      _undostack.Push(_dataobject.DeepClone());
      _dataobject = restorestate.DeepClone();
      _dataobject.Changed += new RestaurantChangedEventHandler(_dataobject_Changed);
      this.restaurantBindingSource.DataSource = _dataobject;
      this.restaurantBindingSource.ResetBindings(false);
      _dataobject.HookupHealthScoreEvents();
      this.scoresBindingSource.DataSource = _dataobject.Scores;
      this.scoresBindingSource.ResetBindings(false);

      undoitem = this.redolevelbutton.DropDownItems[this.redolevelbutton.DropDownItems.Count - 1];
      undoitem.Text = undoitem.Text.Replace("Redo", "Undo");
      this.redolevelbutton.DropDownItems.Remove(undoitem);
      this.undolevelbutton.DropDownItems.Add(undoitem);

      undobutton.Enabled = _undostack.Count > 0;
      undolevelbutton.Enabled = _undostack.Count > 0;
      redobutton.Enabled = _redostack.Count > 0;
      redolevelbutton.Enabled = _redostack.Count > 0;
      savebutton.Enabled = true;
}

So as you can see, undo and redo actions can be rolled back beyond one call. You can handle this many different ways within your form, perhaps a ListBox with each step allowing you to choose the level to roll to. And if you undo to a certain level, you can redo step by step back to the original state. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNeed sample project using your code Pin
Tridip Bhattacharjee2-Oct-12 6:17
professionalTridip Bhattacharjee2-Oct-12 6:17 
AnswerRe: Need sample project using your code Pin
stebo07282-Oct-12 6:36
stebo07282-Oct-12 6:36 
GeneralRe: Need sample project using your code Pin
Tridip Bhattacharjee2-Oct-12 20:38
professionalTridip Bhattacharjee2-Oct-12 20:38 
GeneralRe: Need sample project using your code Pin
stebo07283-Oct-12 6:29
stebo07283-Oct-12 6:29 
GeneralRe: Need sample project using your code Pin
stebo07283-Oct-12 7:07
stebo07283-Oct-12 7:07 
GeneralRe: Need sample project using your code Pin
Tridip Bhattacharjee4-Oct-12 4:09
professionalTridip Bhattacharjee4-Oct-12 4:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.