C# - Multiple undo/redo

By , 8/19/2012
(1 ratings)
Provides undo and redo capabilities for any states or contents.
A demo:


using System;
using Cyrons;

namespace Printversion
{
internal class Program
{
private static void Main()
{
var p = new Person();
var pState = new PersonState();

p.Firstname = "George";
p.Lastname = "Jetson";
pState.SaveState(p);
Console.WriteLine("Launch state : " + p);

p.Firstname = "Testfname";
p.Lastname = "Testlname";
pState.SaveState(p);
Console.WriteLine("Second state : " + p);

p = pState.Undo() as Person;
Console.WriteLine("After undo : " + p);

p = pState.Redo() as Person;
Console.WriteLine("After redo : " + p);

//======================================================================

Console.WriteLine("\r\n");

p.Firstname = "Baxter";
p.Lastname = "Lomax";
pState.SaveState(p);
p.Firstname = "James";
p.Lastname = "Bond";
pState.SaveState(p);

var c = new Car();
var cState = new CarState();
c.TypeName = "VW";
c.Color = "black";
cState.SaveState(c);
c.TypeName = "Aston Martin";
c.Color = "silver";
cState.SaveState(c);
c.TypeName = "Maserati";
c.Color = "gold";
cState.SaveState(c);

Console.WriteLine("Every object type gets its own StateManager ->\n");
p = pState.Undo() as Person;
Console.WriteLine("Baxter Lomax should stand here: " + p);

c = cState.Undo() as Car;
Console.WriteLine("Aston Martin, silver should stand here: " + c);

pState.ClearLists();
Console.WriteLine("CloneStack counter: {0}/ UndoStack counter: {1}",
pState.CloneListCount, pState.UndoListCount);

pState.Dispose();
// Stack of cState is not affected by the dispose:
Console.WriteLine("CloneStack counter: {0}/ UndoStack counter: {1}",
cState.CloneListCount, cState.UndoListCount);

// Prevents automatic closing of the console window.
Console.WriteLine("\nPress any key to terminate the program.");
Console.ReadKey();
}
}

//======================================================================

public class Car : ICloneable
{
#region Public Properties

public string Color { get; set; }

public string TypeName { get; set; }

#endregion

#region Public Methods

public object Clone()
{
return MemberwiseClone();
}

public override string ToString()
{
return String.Format("{0}, {1}", TypeName, Color);
}

#endregion
}

//======================================================================

public class CarState : StateManager
{
#region Public Methods

public override void SaveState(ICloneable car)
{
base.ObjectHandle = car.Clone() as Car;
base.Save();
}

#endregion
}

//======================================================================

public class Person : ICloneable
{
#region Public Properties

public String Firstname { get; set; }

public String Lastname { get; set; }

#endregion

#region Public Methods

public object Clone()
{
return MemberwiseClone();
}

public override string ToString()
{
return String.Format("{0} {1}", Firstname, Lastname);
}

#endregion
}

//======================================================================

public class PersonState : StateManager
{
#region Public Methods

public override void SaveState(ICloneable person)
{
base.ObjectHandle = person.Clone() as Person;
base.Save();
}

#endregion
}
}

/* Output:
Launch state : George Jetson
Second state : Testvorname Testnachname
After undo : George Jetson
After redo : Testvorname Testnachname


Every object type gets its own StateManager ->

Baxter Lomax should stand here: Baxter Lomax
Aston Martin, silver should stand here: Aston Martin, silver
CloneStack counter: 0/ UndoStack counter: 0
CloneStack counter: 2/ UndoStack counter: 1
*/




The State Manager is part of a DotNetExpansions Framework:
http://cyrons.beanstalkapp.com/general/browse/DotNetExpansions/tags/(latest release number)/

The package includes the framework and a detailed help file in chm format.

More information about the DotNetExpansions Framework:
http://dotnet-forum.de/blogs/rainerhilmer/archive/2009/09/28/dotnet-expansions-framework.aspx

Author: Rainer Hilmer, translation by Michael List
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Security.Permissions;

[assembly: PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
namespace DotNetExpansions
{
   /// <summary>
   /// Provides undo and redo capabilities in an extended class.
   /// </summary>   
   /// <example>
   /// <code>
   /// <![CDATA[
   /// using System;
   /// using DotNetExpansions;
   /// 
   /// namespace Printversion
   /// {
   ///    internal class Program
   ///    {
   ///       private static void Main()
   ///       {
   ///          var p = new Person();
   ///          var pState = new PersonState();
   /// 
   ///          p.Firstname = "George";
   ///          p.Lastname = "Jetson";
   ///          pState.SaveState(p);
   ///          Console.WriteLine("Launch state    : " + p);
   /// 
   ///          p.Firstname = "Testfname";
   ///          p.Lastname = "Testlname";
   ///          pState.SaveState(p);
   ///          Console.WriteLine("Second state    : " + p);
   /// 
   ///          p = pState.Undo() as Person;
   ///          Console.WriteLine("After undo      : " + p);
   /// 
   ///          p = pState.Redo() as Person;
   ///          Console.WriteLine("After redo      : " + p);
   /// 
   ///          //======================================================================
   /// 
   ///          Console.WriteLine("\r\n");
   /// 
   ///          p.Firstname = "Baxter";
   ///          p.Lastname = "Lomax";
   ///          pState.SaveState(p);
   ///          p.Firstname = "James";
   ///          p.Lastname = "Bond";
   ///          pState.SaveState(p);
   /// 
   ///          var c = new Car();
   ///          var cState = new CarState();
   ///          c.TypeName = "VW";
   ///          c.Color = "black";
   ///          cState.SaveState(c);
   ///          c.TypeName = "Aston Martin";
   ///          c.Color = "silver";
   ///          cState.SaveState(c);
   ///          c.TypeName = "Maserati";
   ///          c.Color = "gold";
   ///          cState.SaveState(c);
   /// 
   ///          Console.WriteLine("Every object type gets its own StateManager ->\n");
   ///          p = pState.Undo() as Person;
   ///          Console.WriteLine("Baxter Lomax should stand here: " + p);
   /// 
   ///          c = cState.Undo() as Car;
   ///          Console.WriteLine("Aston Martin, silver should stand here: " + c);
   /// 
   ///          pState.ClearLists();
   ///          Console.WriteLine("CloneStack counter: {0}/ UndoStack counter: {1}",
   ///                            pState.CloneListCount, pState.UndoListCount);
   /// 
   ///          pState.Dispose();
   ///          // Stack of cState is not affected by the dispose:
   ///          Console.WriteLine("CloneStack counter: {0}/ UndoStack counter: {1}",
   ///                            cState.CloneListCount, cState.UndoListCount);
   /// 
   ///          // Prevents automatic closing of the console window.
   ///          Console.WriteLine("\nPress any key to terminate the program.");
   ///          Console.ReadKey();
   ///       }
   ///    }
   /// 
   ///    //======================================================================
   /// 
   ///    public class Car : ICloneable
   ///    {
   ///       #region Public Properties
   /// 
   ///       public string Color { get; set; }
   /// 
   ///       public string TypeName { get; set; }
   /// 
   ///       #endregion
   /// 
   ///       #region Public Methods
   /// 
   ///       public object Clone()
   ///       {
   ///          return MemberwiseClone();
   ///       }
   /// 
   ///       public override string ToString()
   ///       {
   ///          return String.Format("{0}, {1}", TypeName, Color);
   ///       }
   /// 
   ///       #endregion
   ///    }
   ///      
   ///    //======================================================================
   /// 
   ///    public class CarState : StateManager
   ///    {
   ///       #region Public Methods
   /// 
   ///       public override void SaveState(ICloneable car)
   ///       {
   ///          base.ObjectHandle = car.Clone() as Car;
   ///          base.Save();
   ///       }
   /// 
   ///       #endregion
   ///    }
   /// 
   ///    //======================================================================
   /// 
   ///    public class Person : ICloneable
   ///    {
   ///       #region Public Properties
   /// 
   ///       public String Firstname { get; set; }
   /// 
   ///       public String Lastname { get; set; }
   /// 
   ///       #endregion
   /// 
   ///       #region Public Methods
   /// 
   ///       public object Clone()
   ///       {
   ///          return MemberwiseClone();
   ///       }
   /// 
   ///       public override string ToString()
   ///       {
   ///          return String.Format("{0} {1}", Firstname, Lastname);
   ///       }
   /// 
   ///       #endregion
   ///    }
   /// 
   ///    //======================================================================
   /// 
   ///    public class PersonState : StateManager
   ///    {
   ///       #region Public Methods
   /// 
   ///       public override void SaveState(ICloneable person)
   ///       {
   ///          base.ObjectHandle = person.Clone() as Person;
   ///          base.Save();
   ///       }
   /// 
   ///       #endregion
   ///    }
   /// }
   /// 
   /// /* Output:
   /// Launch state    : George Jetson
   /// Second state    : Testvorname Testnachname
   /// After undo      : George Jetson
   /// After redo      : Testvorname Testnachname
   /// 
   /// 
   /// Every object type gets its own StateManager ->
   /// 
   /// Baxter Lomax should stand here: Baxter Lomax
   /// Aston Martin, silver should stand here: Aston Martin, silver
   /// CloneStack counter: 0/ UndoStack counter: 0
   /// CloneStack counter: 2/ UndoStack counter: 1
   ///  */
   /// ]]>
   /// </code>
   /// </example>  
   public abstract class StateManager : IDisposable
   {
      #region Events

      /// <summary>
      /// Is raised when the capacity of the list is reached.
      /// </summary>
      public static event Action CapacityExceeded;

      #endregion

      #region Static Fields

      private static object syncLock = new object();

      #endregion

      #region Fields

      private LinkedList<ICloneable> cloneList = new LinkedList<ICloneable>();
      private bool disposed = false;
      private LinkedList<ICloneable> undoList = new LinkedList<ICloneable>();

      /// <summary>
      /// Keeps the information whether unmanaged resources are used or not.
      /// </summary>
      private bool useImages;

      #endregion

      #region Public Properties

      /// <summary>
      /// Returns the capacity of CloneList (was set via SetCapacity method)
      /// </summary>      
      public int Capacity
      {
         get;
         private set;
      }

      /// <summary>
      /// Returns actual count value of CloneList.
      /// </summary>      
      public int? CloneListCount
      {
         get
         {
            if(this.cloneList != null)
               return this.cloneList.Count;
            else
               return null;
         }
      }

      /// <summary>
      /// Returns actual count value of UndoList.
      /// </summary>      
      public int? UndoListCount
      {
         get
         {
            if(this.undoList != null)
               return this.undoList.Count;
            else
               return null;
         }
      }

      #endregion

#if DEBUG
      internal LinkedList<ICloneable> CloneListMirror
      {
         get
         {
            return cloneList;
         }
      }

      internal LinkedList<ICloneable> UndoListMirror
      {
         get
         {
            return undoList;
         }
      }
#endif

      #region Protected Properties

      /// <summary>
      /// Sets the actual object handle or retrieves current value
      /// </summary>
      protected ICloneable ObjectHandle
      {
         get;
         set;
      }

      #endregion

      #region Public Methods

      /// <summary>
      /// Deletes all content of the lists
      /// without destroying the list instances itself.
      /// </summary>
      public void ClearLists()
      {
         this.cloneList.Clear();
         this.undoList.Clear();
      }

      /// <summary>
      /// Destroys all instances in StateManager
      /// and releases allocated memory.
      /// </summary>
      public void Dispose()
      {
         this.Dispose(true);
         GC.SuppressFinalize(this);
      }

      /// <summary>
      /// Redo the last operation.
      /// This method is threadsafe.
      /// </summary>
      /// <returns>The object in the state it was before the last undo.</returns>
      public ICloneable Redo()
      {
         lock(syncLock)
         {
            this.ObjectHandle = this.undoList.Last.Value;
            this.undoList.RemoveLast();
            this.cloneList.AddLast(this.ObjectHandle);
            return this.ObjectHandle;
         }
      }

      /// <summary>
      /// Inserts an instance into the CloneList.
      /// This method is threadsafe.
      /// </summary>
      /// <exception cref="NullReferenceException">Throws a NullReferenceException
      /// if an empty ObjectHandle is inserted.</exception>
      protected void Save()
      {
         lock(syncLock)
         {
            if(this.ObjectHandle != null)
            {
               AddObjectByCondition();
            }
            else
               throw new NullReferenceException("ObjectHandle is empty!");
         }
      }

      /// <summary>
      /// Transports a cloned instance to the StateManager in a derived class.
      /// </summary>
      /// <param name="clone">Cloned object</param>
      public abstract void SaveState(ICloneable clone);

      /// <summary>
      /// Sets the capacity of the CloneList.
      /// A value of 0 disables the limit.
      /// </summary>
      /// <param name="capacity">The capacity</param>
      public void SetCapacity(int capacity)
      {
         this.Capacity = capacity;
      }

      /// <summary>
      /// Undo the last operation.
      /// This method is threadsafe.
      /// </summary>
      /// <returns>The object in the state it was before the last save.</returns>
      public ICloneable Undo()
      {
         lock(syncLock)
         {
            this.ObjectHandle = this.cloneList.Last.Value;
            this.cloneList.RemoveLast();
            this.undoList.AddLast(this.ObjectHandle);
            this.ObjectHandle = this.cloneList.Last.Value;
            return this.ObjectHandle;
         }
      }

      #endregion

      #region Private Methods

      /// <summary>
      /// Inserts a new object at the end of the list.
      /// </summary>
      private void AddDirect()
      {
         this.cloneList.AddLast(this.ObjectHandle);
         if(ObjectHandle.GetType() == typeof(Bitmap))
            useImages = true;
      }

      /// <summary>
      /// Checks if the list has exceeded the capacity set by SetCapacity
      /// and executes the corresponding operations.
      /// </summary>
      private void AddObjectByCondition()
      {
         if(Capacity == 0 || Capacity > 0 && cloneList.Count < Capacity)
         {
            AddDirect();
         }
         else
            ClipAndAdd();
      }

      /// <summary>
      /// In the case that the list capacity is reached,
      /// the first and oldest member is removed from the list,
      /// and then the new item is added at the end.
      /// </summary>
      private void ClipAndAdd()
      {
         this.cloneList.RemoveFirst();
         AddDirect();
         OnCapacityExceeded();
      }

      private void Dispose(bool disposing)
      {
         if(!this.disposed)
         {
            if(disposing)
            {
               // check wether images are located on the list
               if(this.useImages)
               {
                  // clear images in the UndoList
                  while(this.UndoListCount > 0)
                  {
                     object obj = this.undoList.Last.Value;
                     this.undoList.RemoveLast();
                     if(obj.GetType() == typeof(Bitmap))
                     {
                        // cast object to image
                        Image img = obj as Image;
                        // destroy image
                        img.Dispose();
                     }
                  }
                  // clear images in der CloneList
                  while(this.CloneListCount > 0)
                  {
                     object obj = this.cloneList.Last.Value;
                     this.cloneList.RemoveLast();
                     if(obj.GetType() == typeof(Bitmap))
                     {
                        Image img = obj as Image;
                        img.Dispose();
                     }
                  }
               }
               this.cloneList = null;
               this.undoList = null;
               this.ObjectHandle = null;
               /* Without calling Collect() the GC may passes now and then,
                * but it should go fast. */
               GC.Collect();
            }
         }
         this.disposed = true;
      }

      private void OnCapacityExceeded()
      {
         if(CapacityExceeded != null)
            CapacityExceeded();
      }

      #endregion
   }
}
Tagged with undo, redo, state.

Comments

 

Log in, to comment!