Click here to Skip to main content
15,886,095 members
Articles / Programming Languages / C#

Automated Undo/Redo library based on C# Generics in .NET framework

Rate me:
Please Sign up or sign in to vote.
4.94/5 (186 votes)
21 Mar 2012CPOL13 min read 304.8K   2.8K   476  
A reusable library that can equip any action in your application with the undo/redo feature.
// Siarhei Arkhipenka (c) 2006-2007. email: sbs-arhipenko@yandex.ru
using System;
using System.Collections.Generic;
using System.Text;

namespace UndoRedoFramework
{
	/// <summary>
	/// 
	/// </summary>
    public static class UndoRedoManager
    {
        private static List<Command> history = new List<Command>();
        private static int currentPosition = -1;
        private static Command currentCommand = null;

        internal static Command CurrentCommand
        {
            get { return currentCommand; }
        }
		/// <summary>
		/// Returns true if history has command that can be undone
		/// </summary>
        public static bool CanUndo
        {
            get { return currentPosition >= 0; }
        }
		/// <summary>
		/// Returns true if history has command that can be redone
		/// </summary>
        public static bool CanRedo
        {
            get { return currentPosition < history.Count - 1; }
        }
		/// <summary>
		/// Undo last command from history list
		/// </summary>
        public static void Undo()
        {
            AssertNoCommand();
            if (CanUndo)
            {
                Command command = history[currentPosition--];
                foreach (IUndoRedoMember member in command.Keys)
                {
                    member.OnUndo(command[member]);
                }
				OnCommandDone(CommandDoneType.Undo);
            }
        }
		/// <summary>
		/// Repeats command that was undone before
		/// </summary>
        public static void Redo()
        {
            AssertNoCommand();
            if (CanRedo)
            {
                Command command = history[++currentPosition];
                foreach (IUndoRedoMember member in command.Keys)
                {
                    member.OnRedo(command[member]);
                }
				OnCommandDone(CommandDoneType.Redo);
            }
        }
		/// <summary>
		/// Start command. Any data changes must be done within a command.
		/// </summary>
		/// <param name="commandCaption"></param>
		/// <returns></returns>
        public static IDisposable Start(string commandCaption)
        {
            AssertNoCommand();
            currentCommand = new Command(commandCaption);
			return currentCommand;
        }
		/// <summary>
		/// Commits current command and saves changes into history
		/// </summary>
        public static void Commit()
        {
            AssertCommand();
            foreach (IUndoRedoMember member in currentCommand.Keys)
                member.OnCommit(currentCommand[member]);

            // add command to history (all redo records will be removed)
			int count = history.Count - currentPosition - 1;
			history.RemoveRange(currentPosition + 1, count);

            history.Add(currentCommand);
            currentPosition++;
			TruncateHistory(); 

            currentCommand = null;
			OnCommandDone(CommandDoneType.Commit);
        }
		/// <summary>
		/// Rollback current command. It does not saves any changes done in current command.
		/// </summary>
        public static void Cancel()
        {
            AssertCommand();
            foreach (IUndoRedoMember member in currentCommand.Keys)
                member.OnUndo(currentCommand[member]);
            currentCommand = null;
        }
		/// <summary>
		/// Clears all history. It does not affect current data but history only. 
		/// It is usefull after any data initialization if you want forbid user to undo this initialization.
		/// </summary>
        public static void FlushHistory()
        {
            currentCommand = null;
            currentPosition = -1;
            history.Clear();
        }
		/// <summary>Checks there is no command started</summary>
        internal static void AssertNoCommand()
        {
            if (currentCommand != null)
                throw new InvalidOperationException("Previous command is not completed. Use UndoRedoManager.Commit() to complete current command.");
        }

		/// <summary>Checks that command had been started</summary>
        internal static void AssertCommand()
        {
            if (currentCommand == null)
                throw new InvalidOperationException("Command is not started. Use method UndoRedoManager.Start().");
        }

        public static bool IsCommandStarted
        {
            get { return currentCommand != null; }
        }

		/// <summary>Gets an enumeration of commands captions that can be undone.</summary>
		/// <remarks>The first command in the enumeration will be undone first</remarks>
		public static IEnumerable<string> UndoCommands
		{
			get
			{
				for (int i = currentPosition; i >= 0; i--)
					yield return history[i].Caption;
			}
		}
		/// <summary>Gets an enumeration of commands captions that can be redone.</summary>
		/// <remarks>The first command in the enumeration will be redone first</remarks>
		public static IEnumerable<string> RedoCommands
		{
			get
			{
				for (int i = currentPosition + 1; i < history.Count; i++)
					yield return history[i].Caption;
			}
		}

		public static event EventHandler<CommandDoneEventArgs> CommandDone;
		static void OnCommandDone(CommandDoneType type)
		{
			if (CommandDone != null)
				CommandDone(null, new CommandDoneEventArgs(type));
		}

        private static int maxHistorySize = 0;

        /// <summary>
        /// Gets/sets max commands stored in history. 
        /// Zero value (default) sets unlimited history size.
        /// </summary>
        public static int MaxHistorySize
        {
            get { return maxHistorySize; }
            set 
            {
				if (IsCommandStarted)
					throw new InvalidOperationException("Max size may not be set while command is run.");
                if (value < 0)
                    throw new ArgumentOutOfRangeException("Value may not be less than 0");
                maxHistorySize = value;
                TruncateHistory();
            }
        }

        private static void TruncateHistory()
        {			
            if (maxHistorySize > 0)
				if (history.Count > maxHistorySize)
				{
					int count = history.Count - maxHistorySize;
					history.RemoveRange(0, count);
					currentPosition -= count;
				}
        }
	
	}

	public enum CommandDoneType
	{
		Commit, Undo, Redo
	}

	public class CommandDoneEventArgs : EventArgs
	{ 
		public readonly CommandDoneType CommandDoneType;
		public CommandDoneEventArgs(CommandDoneType type)
		{
			CommandDoneType = type;
		}
	}

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior)
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions