C# で Undo/Redo

C++ Undo 入門 にあるコードを移植しただけ

使用例

HistoryTest.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace csharp_undo_sample {
	static class HistoryTest {
		class CHistoryEvent_string_push_back : HistoryEvent {
			StringBuilder	m_string;
			char	m_c;
	
			void	push_back( char	c ){
				m_string.Append( c );
			}
			void	pop_back(){
				if( m_string.Length > 0 ){
					m_string.Remove( m_string.Length - 1, 1 );
				}
			}
			void	undo(){
				var	string_prev = m_string.ToString();
				pop_back();
				System.Diagnostics.Trace.WriteLine( string.Format( "undo: \"{0}\"\"{1}\"", string_prev, m_string.ToString()));
			}
			void	redo(){
				var	string_prev = m_string.ToString();
				push_back( m_c );
				System.Diagnostics.Trace.WriteLine( string.Format( "redo: \"{0}\"\"{1}\"", string_prev.ToString(), m_string.ToString()));
			}
			void	undo_begin(){
				System.Diagnostics.Trace.WriteLine( "undo:{" );
			}
			void	undo_end(){
				System.Diagnostics.Trace.WriteLine( "undo:}" );
			}
			void	redo_begin(){
				System.Diagnostics.Trace.WriteLine( "redo:{" );
			}
			void	redo_end(){
				System.Diagnostics.Trace.WriteLine( "undo:}" );
			}
			protected override bool	OnUndo( uint	dwId ){ undo(); return	true; }
			protected override bool	OnRedo( uint	dwId ){ redo(); return	true; }
			protected override bool	OnUndoBegin( uint	dwId ){ undo_begin(); return	true; }
			protected override bool	OnUndoEnd( uint	dwId ){ undo_end(); return	true; }
			protected override bool	OnRedoBegin( uint	dwId ){ redo_begin(); return	true; }
			protected override bool	OnRedoEnd( uint	dwId ){ redo_end(); return	true; }
			protected override void	OnDispose(){}
			
			public CHistoryEvent_string_push_back( StringBuilder	s, char	c ):base(0){
				m_string = s;
				m_c = c;
			}
		}
		static void	string_push_back( History	history, StringBuilder	s, char	c ){
			history.AddEvent( new CHistoryEvent_string_push_back( s, c ));
		}
		static void	output_periods( History	history ){
			var	undo_periods = history.GetUndoPeriods();
			var	redo_periods = history.GetRedoPeriods();
			System.Diagnostics.Trace.WriteLine( "{" );
			foreach( var	period_id in undo_periods ){
				var	period_name = history.GetPeriodName( period_id );
				System.Diagnostics.Trace.WriteLine( string.Format( "    {0}", period_name ));
			}
			System.Diagnostics.Trace.WriteLine( "---" );
			foreach( var	period_id in redo_periods ){
				var	period_name = history.GetPeriodName( period_id );
				System.Diagnostics.Trace.WriteLine( string.Format( "    {0}", period_name ));
			}
			System.Diagnostics.Trace.WriteLine( "}" );
		}
		public static int	test(){
			var	s = new StringBuilder();
			var	history = new History();
			history.BeginPeriod( "[0] ABC" );
			string_push_back( history, s, 'A' );
			string_push_back( history, s, 'B' );
			string_push_back( history, s, 'C' );
			history.EndPeriod();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			history.BeginPeriod( "[1] 123" );
			string_push_back( history, s, '1' );
			string_push_back( history, s, '2' );
			string_push_back( history, s, '3' );
			history.EndPeriod();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			history.BeginPeriod( "[2] D" );
			string_push_back( history, s, 'D' );
			history.EndPeriod();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			output_periods( history );
			history.Undo();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			output_periods( history );
			history.Undo();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			output_periods( history );
			history.Undo();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			output_periods( history );
			history.BeginPeriod( "[3] E" );
			string_push_back( history, s, 'E' );
			history.EndPeriod();
			System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString()));
			output_periods( history );
			return	0;
		}
	}
}
出力
redo: "" → "A"
redo: "A" → "AB"
redo: "AB" → "ABC"
s = "ABC"
redo: "ABC" → "ABC1"
redo: "ABC1" → "ABC12"
redo: "ABC12" → "ABC123"
s = "ABC123"
redo: "ABC123" → "ABC123D"
s = "ABC123D"
{
    [2] D
    [1] 123
    [0] ABC
---
}
undo:{
undo: "ABC123D" → "ABC123"
undo:}
s = "ABC123"
{
    [1] 123
    [0] ABC
---
    [2] D
}
undo:{
undo: "ABC123" → "ABC12"
undo: "ABC12" → "ABC1"
undo: "ABC1" → "ABC"
undo:}
s = "ABC"
{
    [0] ABC
---
    [1] 123
    [2] D
}
undo:{
undo: "ABC" → "AB"
undo: "AB" → "A"
undo: "A" → ""
undo:}
s = ""
{
---
    [0] ABC
    [1] 123
    [2] D
}
redo: "" → "E"
s = "E"
{
    [3] E
---
}

実装

HistoryMessage.cs
using System;

namespace csharp_undo_sample {
	enum HistoryMessage {
		HISTORY_DETACHED,
		HISTORY_ATTACHED,
		HISTORY_UNDO,
		HISTORY_REDO,
		HISTORY_UNDO_BEGIN,
		HISTORY_UNDO_END,
		HISTORY_REDO_BEGIN,
		HISTORY_REDO_END,
		HISTORY_ENABLED
	}
}
HistoryEvent.cs
using System;

namespace csharp_undo_sample {
	interface IHistoryEvent : IDisposable {
		bool	Invoke( uint	dwId, HistoryMessage	uMsg );
	}
	class HistoryEvent : Disposable, IHistoryEvent {
		public event EventHandler	Undo = delegate{};
		public event EventHandler	Redo = delegate{};
		public long	RequestId = 0;

		public HistoryEvent( long	requestId ){
			RequestId = requestId;
		}
		
		protected virtual bool	OnDetach( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnAttach( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnUndo( uint	dwId ){
#if DEBUG
			System.Diagnostics.Trace.WriteLine( string.Format( "{0}_Undo", GetType().Name ));
#endif // DEBUG
			Undo( this, EventArgs.Empty );
			return	true;
		}
		protected virtual bool	OnRedo( uint	dwId ){
#if DEBUG
			System.Diagnostics.Trace.WriteLine( string.Format( "{0}_Redo", GetType().Name ));
#endif // DEBUG
			Redo( this, EventArgs.Empty );
			return	true;
		}
		protected virtual bool	OnUndoBegin( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnUndoEnd( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnRedoBegin( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnRedoEnd( uint	dwId ){
			return	true;
		}
		protected virtual bool	OnEnabled( uint	dwId ){
			return	true;
		}
		public bool	Invoke( uint	dwId, HistoryMessage	uMsg ){
			var	result = true;
			if( uMsg == HistoryMessage.HISTORY_DETACHED ){
				result = OnDetach( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_ATTACHED ){
				result = OnAttach( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_UNDO ){
				result = OnUndo( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_REDO ){
				result = OnRedo( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_UNDO_BEGIN ){
				result = OnUndoBegin( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_UNDO_END ){
				result = OnUndoEnd( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_REDO_BEGIN ){
				result = OnRedoBegin( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_REDO_END ){
				result = OnRedoEnd( dwId );
			}
			else if( uMsg == HistoryMessage.HISTORY_ENABLED ){
				result = OnEnabled( dwId );
			}
			return	result;
		}
	}
}
History.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace csharp_undo_sample {
	class History {
		public const uint	INVALID_EVENT_ID = uint.MaxValue;
		public const uint	INVALID_PERIOD_ID = uint.MaxValue;
		public const uint	MAXIMUM_PERIOD_LEVEL = uint.MaxValue;

		List<uint>	m_redo = new List<uint>();
		List<uint>	m_undo = new List<uint>();
		Dictionary<uint,HistoryItem>	m_items = new Dictionary<uint,HistoryItem>();
		Dictionary<uint,HistoryPeriod>	m_periods = new Dictionary<uint,HistoryPeriod>();
		uint	m_period_id = INVALID_PERIOD_ID;
		uint	m_period_level = 0;
		uint	m_next_event = INVALID_EVENT_ID;
		uint	m_saved_event = INVALID_EVENT_ID;
		uint	m_current_event = INVALID_EVENT_ID;
		
		public bool	Modified {
			get {
				return	( m_current_event != m_saved_event );
			}
		}
		public void	Save(){
			m_saved_event = m_current_event;
		}

		void	ClearRedo(){
			foreach( var	event_id in m_redo ){
				var	item = m_items[event_id];
				m_periods.Remove( item.period_id());
				m_items.Remove( event_id );
				item.Detach();
			}
			m_redo.Clear();
		}
		public uint	AddEvent( IHistoryEvent	pHistoryEvent ){
			uint	result = INVALID_EVENT_ID;
			var	item = new HistoryItem( m_period_id, m_period_level, ++m_next_event, pHistoryEvent );
			m_items[item.id()] = item;
			m_periods[m_period_id].Add( item.id());
			m_undo.Add( item.id());
			m_current_event = m_next_event;
			ClearRedo();
			if( item.Redo()){
				result = m_next_event;
			}
			return	result;
		}
		public void	Clear(){
			foreach( var	item in m_items ){
				item.Value.Detach();
			}
			m_redo.Clear();
			m_undo.Clear();
			m_periods.Clear();
			m_items.Clear();
			m_period_id = INVALID_PERIOD_ID;
			m_period_level = 0;
			m_next_event = INVALID_EVENT_ID;
			m_saved_event = INVALID_EVENT_ID;
			m_current_event = INVALID_EVENT_ID;
		}
		public void	DeleteEvent( uint	event_id ){
			if( m_items.ContainsKey( event_id )){
				var	item = m_items[event_id];
				m_items.Remove( event_id );
				m_periods[item.period_id()].Remove( event_id );
				item.Detach();
			}
		}
		public IHistoryEvent	GetEvent( uint	event_id ){
			IHistoryEvent	result = null;
			if( m_items.ContainsKey( event_id )){
				result = m_items[event_id].GetEvent();
			}
			return	result;
		}
		public void	BeginPeriod( string	period_name ){
			if( m_period_level > 0 ){
				if( m_period_level + 1 >= MAXIMUM_PERIOD_LEVEL ){
					throw new Exception("period level overflow");
				}
				++m_period_level;
			}
			else{
				if( m_period_id + 1 == INVALID_PERIOD_ID ){
					throw new Exception("period id overflow");
				}
				++m_period_level;
				++m_period_id;
				m_periods.Add( m_period_id, new HistoryPeriod( m_period_id, period_name ));
			}
		}
		public void	EndPeriod(){
			if( m_period_level == 0 ){
				throw new Exception("period level underflow");
			}
			--m_period_level;
		}
		public void	CancelPeriod(){
			if( m_period_level == 0 ){
				throw new Exception("period level underflow");
			}
			while( m_undo.Count > 0 ){
				var	n = m_undo.Count-1;
				var	event_id = m_undo[n];
				var	item = m_items[event_id];
				if(( item.period_id() != m_period_id )||( item.period_level() != m_period_level )){
					break;
				}
				item.Undo();
				item.Detach();
				m_undo.RemoveAt( n );
			}
			m_periods.Remove( m_period_id );
			--m_period_level;
		}
		public void	Undo(){
			if( m_undo.Count > 0 ){
				var	temp = new List<uint>();
				do{
					var	n = m_undo.Count - 1;
					var	event_id = m_undo[n];
					temp.Add( event_id );
					m_undo.RemoveAt( n );
				}while(( m_undo.Count > 0 )&&( m_items[m_undo[m_undo.Count-1]].period_id() == m_items[temp[temp.Count-1]].period_id()));
				m_redo.AddRange( temp );
				m_items[temp[0]].UndoBegin();
				foreach( var	event_id in temp ){
					m_items[event_id].Undo();
				}
				m_items[temp[temp.Count-1]].UndoEnd();
				--m_current_event;
			}
		}
		public void	Redo(){
			if( m_redo.Count > 0 ){
				var	temp = new List<uint>();
				do{
					var	n = m_redo.Count - 1;
					var	event_id = m_redo[n];
					temp.Add( event_id );
					m_redo.RemoveAt( n );
				}while(( m_redo.Count > 0 )&&( m_items[m_redo[m_redo.Count-1]].period_id() == m_items[temp[temp.Count-1]].period_id()));
				m_undo.AddRange( temp );
				m_items[temp[0]].RedoBegin();
				foreach( var	event_id in temp ){
					m_items[event_id].Redo();
				}
				m_items[temp[temp.Count-1]].RedoEnd();
				++m_current_event;
			}
		}
		public bool	IsUndoAvailable(){
			return	( m_undo.Count > 0 );
		}
		public bool	IsRedoAvailable(){
			return	( m_redo.Count > 0 );
		}
		public uint	GetUndoPeriodCount(){
			return	(uint)GetUndoPeriods().Count;
		}
		public uint	GetRedoPeriodCount(){
			return	(uint)GetRedoPeriods().Count;
		}
		public List<uint>	GetUndoPeriods(){
			var	result = new List<uint>();
			var	last_period_id = INVALID_PERIOD_ID;
			for( var	n = 0; n < m_undo.Count; ++n ){
				var	m = m_undo.Count - n - 1;
				var	event_id = m_undo[m];
				var	item = m_items[event_id];
				if(( last_period_id == INVALID_PERIOD_ID )||( last_period_id != item.period_id())){
					result.Add( item.period_id());
					last_period_id = item.period_id();
				}
			}
			return	result;
		}
		public List<uint>	GetRedoPeriods(){
			var	result = new List<uint>();
			var	last_period_id = INVALID_PERIOD_ID;
			for( var	n = 0; n < m_redo.Count; ++n ){
				var	m = m_redo.Count - n - 1;
				var	event_id = m_redo[m];
				var	item = m_items[event_id];
				if(( last_period_id == INVALID_PERIOD_ID )||( last_period_id != item.period_id())){
					result.Add( item.period_id());
					last_period_id = item.period_id();
				}
			}
			return	result;
		}
		public List<uint>	GetPeriods(){
			var	result = new List<uint>();
			foreach( var	kv in m_periods ){
				result.Add( kv.Key );
			}
			return	result;
		}
		public string	GetPeriodName( uint	period_id ){
			string	result = null;
			if( m_periods.ContainsKey( period_id )){
				result = m_periods[period_id].name();
			}
			return	result;
		}
		public bool	IsPeriodEnabled( uint	period_id ){
			bool	result = false;
			if( m_periods.ContainsKey( period_id )){
				var	period = m_periods[period_id];
				for( var	n = 0; n < period.Count; ++n ){
					var	event_id = period[n];
					var	item = m_items[event_id];
					var	pHistoryEvent = item.GetEvent();
					if( pHistoryEvent != null ){
						result = pHistoryEvent.Invoke( event_id, HistoryMessage.HISTORY_ENABLED );
						if( !result ){
							break;
						}
					}
				}
			}
			return	result;
		}
	}
}
HistoryItem.cs
using System;

namespace csharp_undo_sample {
	class HistoryItem : Disposable {
		uint	m_period_id = History.INVALID_PERIOD_ID;
		uint	m_period_level = 0;
		uint	m_id = History.INVALID_EVENT_ID;
		IHistoryEvent	m_pHistoryEvent = null;
		
		bool	SendMessage( HistoryMessage	uMsg ){
			bool	result = true;
			if( m_pHistoryEvent != null ){
				result = m_pHistoryEvent.Invoke( m_id, uMsg );
			}
			return	result;
		}
		protected override void	OnDispose(){
			Detach();
			base.OnDispose();
		}
		public void	Detach(){
			if( m_pHistoryEvent != null ){
				SendMessage( HistoryMessage.HISTORY_DETACHED );
				m_pHistoryEvent = null;
			}
			m_period_id = History.INVALID_PERIOD_ID;
			m_period_level = 0;
			m_id = History.INVALID_EVENT_ID;
		}
		public HistoryItem( uint	period_id, uint	period_level, uint	id, IHistoryEvent	pHistoryEvent ){
			m_period_id = period_id;
			m_period_level = period_level;
			m_id = id;
			m_pHistoryEvent = pHistoryEvent;
			SendMessage( HistoryMessage.HISTORY_ATTACHED );
		}
		public IHistoryEvent	GetEvent(){
			return	m_pHistoryEvent;
		}
		public bool	UndoBegin(){
			return	SendMessage( HistoryMessage.HISTORY_UNDO_BEGIN );
		}
		public bool	UndoEnd(){
			return	SendMessage( HistoryMessage.HISTORY_UNDO_END );
		}
		public bool	Undo(){
			return	SendMessage( HistoryMessage.HISTORY_UNDO );
		}
		public bool	RedoBegin(){
			return	SendMessage( HistoryMessage.HISTORY_REDO_BEGIN );
		}
		public bool	RedoEnd(){
			return	SendMessage( HistoryMessage.HISTORY_REDO_END );
		}
		public bool	Redo(){
			return	SendMessage( HistoryMessage.HISTORY_REDO );
		}
		public bool	IsEnabled(){
			return	SendMessage( HistoryMessage.HISTORY_ENABLED );
		}
		public uint	period_id(){
			return	m_period_id;
		}
		public uint	period_level(){
			return	m_period_level;
		}
		public uint	id(){
			return	m_id;
		}
	}
}
HistoryPeriod.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace csharp_undo_sample {
	class HistoryPeriod : List<uint> {
		uint	m_id = History.INVALID_PERIOD_ID;
		string	m_name = null;

		public uint	id(){
			return	m_id;
		}
		public string	name(){
			return	m_name;
		}
		public HistoryPeriod( uint	id, string	name ):base(){
			m_id = id;
			m_name = name;
		}
	}
}
Disposable.cs
using System;

namespace csharp_undo_sample {
	// 
	public class Disposable : IDisposable {
		bool	m_disposed = false;
		public bool	IsDisposed {
			get {
				return	m_disposed;
			}
		}
		public event EventHandler	Disposed = delegate{};
		
		~Disposable(){
			Dispose( false );
		}
		// 概要:
		//     System.Windows.Forms.ApplicationContext によって使用されているすべてのリソースを解放します。
		public void Dispose(){
			Dispose( true );
			GC.SuppressFinalize( this );
		}
		//
		// 概要:
		//     System.Windows.Forms.ApplicationContext によって使用されているアンマネージ リソースを解放し、オプションでマネージ
		//     リソースも解放します。
		//
		// パラメーター:
		//   disposing:
		//     マネージ リソースとアンマネージ リソースの両方を解放する場合は true。アンマネージ リソースだけを解放する場合は false。
		protected virtual void	Dispose( bool	disposing ){
			if( !m_disposed ){
				m_disposed = true;
				if( disposing ){
					OnDisposing();
				}
				Disposed( this, EventArgs.Empty );
				OnDispose();
			}
		}
		protected virtual void	OnDisposing(){
			// Disposeメソッドから呼ばれた。Managedリソースだけを解放せよ
		}
		protected virtual void	OnDispose(){
			// ファイナライザ(デストラクタ)または Disposeメソッドから呼ばれた。UnmanagedリソースとManagedリソースの両方を解放せよ
		}
	}
}