Feedback

C# - Multiples Undo/Redo

Veröffentlicht von am 28.11.2009
(3 Bewertungen)
Bietet Undo/Redo Funktionen für beliebige Zustände bzw. Inhalte. Ein 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("Startzustand : " + p);

p.Firstname = "Testvorname";
p.Lastname = "Testnachname";
pState.SaveState(p);
Console.WriteLine("Zweiter Zustand : " + p);

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

p = pState.Redo() as Person;
Console.WriteLine("Nach 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 = "Austin Martin";
c.Color = "silver";
cState.SaveState(c);
c.TypeName = "Maserati";
c.Color = "gold";
cState.SaveState(c);

Console.WriteLine("Jeder Objekt-Typ bekommt seinen eigenen StateManager ->\n");
p = pState.Undo() as Person;
Console.WriteLine("Hier müßte Baxter Lomax stehen: " + p);

c = cState.Undo() as Car;
Console.WriteLine("Hier müßte Austin Martin, silver stehen: " + c);

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

pState.Dispose();
// Der Stack von cState ist vom Dispose nicht betroffen:
Console.WriteLine("CloneStack-Zähler: {0}/ UndoStack-Zähler: {1}",
cState.CloneListCount, cState.UndoListCount);

// Verhindert das selbsttätige Schließen des Konsolenfensters.
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:
Startzustand : George Jetson
Zweiter Zustand : Testvorname Testnachname
Nach undo : George Jetson
Nach redo : Testvorname Testnachname


Jeder Objekt-Typ bekommt seinen eigenen StateManager ->

Hier müßte Baxter Lomax stehen: Baxter Lomax
Hier müßte Austin Martin, silver stehen: Austin Martin, silver
CloneStack-Zähler: 0/ UndoStack-Zähler: 0
CloneStack-Zähler: 2/ UndoStack-Zähler: 1
*/


Der StateManager ist Bestandteil meines DotNetExpansions Framework:
http://cyrons.beanstalkapp.com/general/browse/DotNetExpansions/tags/(neueste Release Nummer)/

Das Paket beinhaltet neben dem Framework auch eine ausführliche Hilfedatei im chm-Format.

Weitere Informationen zum DotNetExpansions Framework gibt es hier:
http://dotnet-forum.de/blogs/rainerhilmer/archive/2009/09/28/dotnet-expansions-framework.aspx
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Security.Permissions;

[assembly: PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
namespace DotNetExpansions
{
   /// <summary>
   /// Bietet in einer abgeleiteten Klasse Funktionen für Undo und Redo.
   /// </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("Startzustand    : " + p);
   /// 
   ///          p.Firstname = "Testvorname";
   ///          p.Lastname = "Testnachname";
   ///          pState.SaveState(p);
   ///          Console.WriteLine("Zweiter Zustand : " + p);
   /// 
   ///          p = pState.Undo() as Person;
   ///          Console.WriteLine("Nach undo       : " + p);
   /// 
   ///          p = pState.Redo() as Person;
   ///          Console.WriteLine("Nach 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 = "Austin Martin";
   ///          c.Color = "silver";
   ///          cState.SaveState(c);
   ///          c.TypeName = "Maserati";
   ///          c.Color = "gold";
   ///          cState.SaveState(c);
   /// 
   ///          Console.WriteLine("Jeder Objekt-Typ bekommt seinen eigenen StateManager ->\n");
   ///          p = pState.Undo() as Person;
   ///          Console.WriteLine("Hier müßte Baxter Lomax stehen: " + p);
   /// 
   ///          c = cState.Undo() as Car;
   ///          Console.WriteLine("Hier müßte Austin Martin, silver stehen: " + c);
   /// 
   ///          pState.ClearLists();
   ///          Console.WriteLine("CloneStack-Zähler: {0}/ UndoStack-Zähler: {1}",
   ///                            pState.CloneListCount, pState.UndoListCount);
   /// 
   ///          pState.Dispose();
   ///          // Der Stack von cState ist vom Dispose nicht betroffen:
   ///          Console.WriteLine("CloneStack-Zähler: {0}/ UndoStack-Zähler: {1}",
   ///                            cState.CloneListCount, cState.UndoListCount);
   /// 
   ///          // Verhindert das selbsttätige Schließen des Konsolenfensters.
   ///          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:
   /// Startzustand    : George Jetson
   /// Zweiter Zustand : Testvorname Testnachname
   /// Nach undo       : George Jetson
   /// Nach redo       : Testvorname Testnachname
   /// 
   /// 
   /// Jeder Objekt-Typ bekommt seinen eigenen StateManager ->
   /// 
   /// Hier müßte Baxter Lomax stehen: Baxter Lomax
   /// Hier müßte Austin Martin, silver stehen: Austin Martin, silver
   /// CloneStack-Zähler: 0/ UndoStack-Zähler: 0
   /// CloneStack-Zähler: 2/ UndoStack-Zähler: 1
   /// 
   /// Press any key to terminate the program.
   /// 
   ///  */
   /// ]]>
   /// </code>
   /// </example>  
   public abstract class StateManager : IDisposable
   {
      #region Events

      /// <summary>
      /// Tritt ein wenn die Kapazität der Liste erreicht ist.
      /// </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>
      /// Hält die Information ob unmanaged Ressourcen verwendet werden oder nicht.
      /// </summary>
      private bool useImages;

      #endregion

      #region Public Properties

      /// <summary>
      /// Gibt die über die SetCapacity-Methode gesetzte Kapazität der CloneListe zurück.
      /// </summary>      
      public int Capacity
      {
         get;
         private set;
      }

      /// <summary>
      /// Gibt den aktuellen Zählerstand der CloneListe zurück.
      /// </summary>      
      public int? CloneListCount
      {
         get
         {
            if(this.cloneList != null)
               return this.cloneList.Count;
            else
               return null;
         }
      }

      /// <summary>
      /// Gibt den aktuellen Zählerstand der UndoListe zurück.
      /// </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>
      /// Setzt das aktuelle Objekt-Handle oder ruft dieses ab.
      /// </summary>
      protected ICloneable ObjectHandle
      {
         get;
         set;
      }

      #endregion

      #region Public Methods

      /// <summary>
      /// Löscht alle Inhalte der Listen,
      /// ohne die List-Instanzen selber zu zerstören.
      /// </summary>
      public void ClearLists()
      {
         this.cloneList.Clear();
         this.undoList.Clear();
      }

      /// <summary>
      /// Zerstört alle Instanzen im StateManager
      /// und gibt den von ihnen verwendeten Speicher frei.
      /// </summary>
      public void Dispose()
      {
         this.Dispose(true);
         GC.SuppressFinalize(this);
      }

      /// <summary>
      /// Wiederholt die letzte Operation.
      /// Diese Methode ist Threadsicher.
      /// </summary>
      /// <returns>Das Objekt in dem Zustand,
      /// in dem es sich vor dem letzten Undo befand.</returns>
      public ICloneable Redo()
      {
         lock(syncLock)
         {
            this.ObjectHandle = this.undoList.Last.Value;
            this.undoList.RemoveLast();
            this.cloneList.AddLast(this.ObjectHandle);
            return this.ObjectHandle;
         }
      }

      /// <summary>
      /// Fügt eine Instanz in die CloneListe ein.
      /// Diese Methode ist Threadsicher.
      /// </summary>
      /// <exception cref="NullReferenceException">Wirft eine NullReferenceException
      /// wenn versucht wird, ein leeres ObjectHandle an die CloneListe zu hängen.</exception>
      protected void Save()
      {
         lock(syncLock)
         {
            if(this.ObjectHandle != null)
            {
               AddObjectByCondition();
            }
            else
               throw new NullReferenceException("ObjektHandle ist leer!");
         }
      }

      /// <summary>
      /// Transportiert in einer abgeleiteten Klasse
      /// eine geklonte Instanz an den StateManager weiter.
      /// </summary>
      /// <param name="clone">Das geklonte Objekt</param>
      public abstract void SaveState(ICloneable clone);

      /// <summary>
      /// Setzt die Kapazität der CloneListe.
      /// Ein Wert von 0 deaktiviert die Kapazitätsbegrenzung.
      /// </summary>
      /// <param name="capacity">Die Kapazität</param>
      public void SetCapacity(int capacity)
      {
         this.Capacity = capacity;
      }

      /// <summary>
      /// Macht eine Operation rückgängig.
      /// Diese Methode ist Threadsicher.
      /// </summary>
      /// <returns>Das Objekt in dem Zustand,
      /// in dem es sich vor dem letzten Save befand.</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>
      /// Schiebt ein neues Objekt an das Ende der Liste.
      /// </summary>
      private void AddDirect()
      {
         this.cloneList.AddLast(this.ObjectHandle);
         if(ObjectHandle.GetType() == typeof(Bitmap))
            useImages = true;
      }

      /// <summary>
      /// Prüft, ob die Liste die mit SetCapacity angegebene Kapazität überschritten hat,
      /// und führt dem entsprechende Operationen aus.
      /// </summary>
      private void AddObjectByCondition()
      {
         if(Capacity == 0 || Capacity > 0 && cloneList.Count < Capacity)
         {
            AddDirect();
         }
         else
            ClipAndAdd();
      }

      /// <summary>
      /// Im Falle daß die Listenkapazität erreicht ist,
      /// wird das erste und damit älteste Element aus der Liste entfernt,
      /// und dann das neue Element am Ende angefügt.
      /// </summary>
      private void ClipAndAdd()
      {
         this.cloneList.RemoveFirst();
         AddDirect();
         OnCapacityExceeded();
      }

      private void Dispose(bool disposing)
      {
         if(!this.disposed)
         {
            if(disposing)
            {
               // prüfen ob Images auf der Liste liegen
               if(this.useImages)
               {
                  // Images in der UndoListe zerstören
                  while(this.UndoListCount > 0)
                  {
                     object obj = this.undoList.Last.Value;
                     this.undoList.RemoveLast();
                     if(obj.GetType() == typeof(Bitmap))
                     {
                        // Object in Image casten
                        Image img = obj as Image;
                        // Image zerstören
                        img.Dispose();
                     }
                  }
                  // images in der CloneListe zerstören
                  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;
               /* Ohne Collect() kommt der GC vielleicht irgendwann mal vorbei,
                * aber es soll ja schnell gehen. */
               GC.Collect();
            }
         }
         this.disposed = true;
      }

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

      #endregion
   }
}
Abgelegt unter undo, redo, state.

4 Kommentare zum Snippet

Konstantin Gross schrieb am 31.01.2010:
Hey, herzlichen Glückwunsch zum ersten Platz =)
Rainer Hilmer schrieb am 31.01.2010:
Danke Konstantin. :-)
Gerade nachdem ich dein ISO-Snippet sah, hatte ich mir eigentlich keine Chancen mehr ausgemalt. Als ich gestern die Mail erhielt, sind mir fast die Augen aus dem Kopf gefallen.
Es freut mich daß die Jury einen Sonderpreis für dich eingerichtet hat.
Sperneder Patrick schrieb am 20.10.2011:
wirklich gut.
danieljena schrieb am 13.10.2016:
Wenn ich das so sehe, denke ich: Man kann damit jegliche Arten von Objekten in unterschiedlichen Statien speichern, oder?
 

Logge dich ein, um hier zu kommentieren!