첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
728x90
반응형
728x170

TestSolution.zip
0.03MB

[TestLibrary 프로젝트]

▶ IAction.cs

namespace TestLibrary
{
    /// <summary>
    /// 액션 인터페이스
    /// </summary>
    public interface IAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property

        #region 이전 액션과 병합 허용 여부 - AllowToMergeWithPrevious

        /// <summary>
        /// 이전 액션과 병합 허용 여부
        /// </summary>
        bool AllowToMergeWithPrevious { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method

        #region 실행 가능 여부 구하기 - CanExecute()

        /// <summary>
        /// 실행 가능 여부 구하기
        /// </summary>
        /// <returns>실행 가능 여부</returns>
        bool CanExecute();

        #endregion
        #region 실행하기 - Execute()

        /// <summary>
        /// 실행하기
        /// </summary>
        void Execute();

        #endregion
        #region 실행 취소 가능 여부 구하기 - CanUnexecute()

        /// <summary>
        /// 실행 취소 가능 여부 구하기
        /// </summary>
        /// <returns>실행 취소 가능 여부</returns>
        bool CanUnexecute();

        #endregion
        #region 실행 취소하기 - Unexecute()

        /// <summary>
        /// 실행 취소하기
        /// </summary>
        void Unexecute();

        #endregion
        #region 병합 시도하기 - TryToMerge(sourceAction)

        /// <summary>
        /// 병합 시도하기
        /// </summary>
        /// <param name="sourceAction">소스 액션</param>
        /// <returns>처리 결과</returns>
        bool TryToMerge(IAction sourceAction);

        #endregion
    }
}

 

728x90

 

▶ AbstractAction.cs

namespace TestLibrary
{
    /// <summary>
    /// 추상 액션
    /// </summary>
    public abstract class AbstractAction : IAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행 카운트 - ExecuteCount

        /// <summary>
        /// 실행 카운트
        /// </summary>
        protected int ExecuteCount { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region (IAction) 이전 노드와 병합 허용 여부 - AllowToMergeWithPrevious

        /// <summary>
        /// 이전 노드와 병합 허용 여부
        /// </summary>
        public bool AllowToMergeWithPrevious { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region (IAction) 실행 가능 여부 구하기 - CanExecute()

        /// <summary>
        /// 실행 가능 여부 구하기
        /// </summary>
        /// <returns>실행 가능 여부</returns>
        public virtual bool CanExecute()
        {
            return ExecuteCount == 0;
        }

        #endregion
        #region (IAction) 실행하기 - Execute()

        /// <summary>
        /// 실행하기
        /// </summary>
        public virtual void Execute()
        {
            if(!CanExecute())
            {
                return;
            }

            ExecuteCore();

            ExecuteCount++;
        }

        #endregion
        #region (IAction) 실행 취소 가능 여부 구하기 - CanUnexecute()

        /// <summary>
        /// 실행 취소 가능 여부 구하기
        /// </summary>
        /// <returns>실행 취소 가능 여부</returns>
        public virtual bool CanUnexecute()
        {
            return !CanExecute();
        }

        #endregion
        #region (IAction) 실행 취소하기 - Unexecute()

        /// <summary>
        /// 실행 취소하기
        /// </summary>
        public virtual void Unexecute()
        {
            if(!CanUnexecute())
            {
                return;
            }

            UnexecuteCore();

            ExecuteCount--;
        }

        #endregion
        #region (IAction) 병합 시도하기 - TryToMerge(sourceAction)

        /// <summary>
        /// 병합 시도하기
        /// </summary>
        /// <param name="sourceAction">소스 액션</param>
        /// <returns>처리 결과</returns>
        public virtual bool TryToMerge(IAction sourceAction)
        {
            return false;
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected abstract void ExecuteCore();

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected abstract void UnexecuteCore();

        #endregion
    }
}

 

300x250

 

▶ AddItemAction.cs

using System;

namespace TestLibrary
{
    /// <summary>
    /// 항목 추가 액션
    /// </summary>
    /// <typeparam name="T">항목 타입</typeparam>
    public class AddItemAction<T> : AbstractAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 추가 액션 - AddAction

        /// <summary>
        /// 추가 액션
        /// </summary>
        public Action<T> AddAction { get; set; }

        #endregion
        #region 제거 액션 - RemoveAction

        /// <summary>
        /// 제거 액션
        /// </summary>
        public Action<T> RemoveAction { get; set; }

        #endregion
        #region 항목 - Item

        /// <summary>
        /// 항목
        /// </summary>
        public T Item { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - AddItemAction(addAction, removeAction, item)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="addAction">추가 액션</param>
        /// <param name="removeAction">제거 액션</param>
        /// <param name="item">항목</param>
        public AddItemAction(Action<T> addAction, Action<T> removeAction, T item)
        {
            AddAction    = addAction;
            RemoveAction = removeAction;
            Item         = item;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected override void ExecuteCore()
        {
            AddAction(Item);
        }

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected override void UnexecuteCore()
        {
            RemoveAction(Item);
        }

        #endregion
    }
}

 

▶ CallMethodAction.cs

using System;

namespace TestLibrary
{
    /// <summary>
    /// 메소드 호출 액션
    /// </summary>
    public class CallMethodAction : AbstractAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 실행 액션 - ExecuteAction

        /// <summary>
        /// 실행 액션
        /// </summary>
        public Action ExecuteAction { get; set; }

        #endregion
        #region 실행 취소 액션 - UnexecuteAction

        /// <summary>
        /// 실행 취소 액션
        /// </summary>
        public Action UnexecuteAction { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - CallMethodAction(executeAction, unexecuteAction)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="executeAction">실행 액션</param>
        /// <param name="unexecuteAction">실행 취소 액션</param>
        public CallMethodAction(Action executeAction, Action unexecuteAction)
        {
            ExecuteAction   = executeAction;
            UnexecuteAction = unexecuteAction;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected override void ExecuteCore()
        {
            if(ExecuteAction != null)
            {
                ExecuteAction();
            }
        }

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected override void UnexecuteCore()
        {
            if(UnexecuteAction != null)
            {
                UnexecuteAction();
            }
        }

        #endregion
    }
}

 

▶ SetPropertyAction.cs

using System.Reflection;

namespace TestLibrary
{
    /// <summary>
    /// 속성 설정 액션
    /// </summary>
    public class SetPropertyAction : AbstractAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 타겟 객체 - TargetObject

        /// <summary>
        /// 타겟 객체
        /// </summary>
        public object TargetObject { get; set; }

        #endregion
        #region 속성 정보 - PropertyInfo

        /// <summary>
        /// 속성 정보
        /// </summary>
        public PropertyInfo PropertyInfo { get; set; }

        #endregion
        #region 값 - Value

        /// <summary>
        /// 값
        /// </summary>
        public object Value { get; set; }

        #endregion
        #region 이전 값 - PreviousValue

        /// <summary>
        /// 이전 값
        /// </summary>
        public object PreviousValue { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - SetPropertyAction(targetObejct, propertyName, value)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="targetObejct">부모 객체</param>
        /// <param name="propertyName">속성명</param>
        /// <param name="value">값</param>
        public SetPropertyAction(object targetObejct, string propertyName, object value)
        {
            TargetObject = targetObejct;
            PropertyInfo = targetObejct.GetType().GetTypeInfo().GetDeclaredProperty(propertyName);
            Value        = value;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 병합 시도하기 - TryToMerge(sourceAction)

        /// <summary>
        /// 병합 시도하기
        /// </summary>
        /// <param name="sourceAction">소스 액션</param>
        /// <returns>처리 결과</returns>
        public override bool TryToMerge(IAction sourceAction)
        {
            SetPropertyAction setPropertyAction = sourceAction as SetPropertyAction;

            if(setPropertyAction != null && setPropertyAction.TargetObject == TargetObject && setPropertyAction.PropertyInfo == PropertyInfo)
            {
                Value = setPropertyAction.Value;

                PropertyInfo.SetValue(TargetObject, Value, null);

                return true;
            }

            return false;
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected override void ExecuteCore()
        {
            PreviousValue = PropertyInfo.GetValue(TargetObject, null);

            PropertyInfo.SetValue(TargetObject, Value, null);
        }

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected override void UnexecuteCore()
        {
            PropertyInfo.SetValue(TargetObject, PreviousValue, null);
        }

        #endregion
    }
}

 

▶ Transaction.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace TestLibrary
{
    /// <summary>
    /// 트랜잭션
    /// </summary>
    public sealed class Transaction : IAction, IDisposable
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 액션 관리자
        /// </summary>
        private readonly ActionManager actionManager;

        /// <summary>
        /// 액션 리스트
        /// </summary>
        private readonly List<IAction> actionList;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region (IAction) 이전 액션과 병합 허용 여부 - AllowToMergeWithPrevious

        /// <summary>
        /// 이전 액션과 병합 허용 여부
        /// </summary>
        public bool AllowToMergeWithPrevious { get; set; }

        #endregion
        #region 지연 여부 - IsDelayed

        /// <summary>
        /// 지연 여부
        /// </summary>
        public bool IsDelayed { get; set; }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 중단 여부 - Aborted

        /// <summary>
        /// 중단 여부
        /// </summary>
        private bool Aborted { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 생성자 - Transaction(actionManager, isDelayed)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="actionManager">액션 관리자</param>
        /// <param name="isDelayed">지연 여부</param>
        Transaction(ActionManager actionManager, bool isDelayed)
        {
            this.actionManager = actionManager;

            this.actionList = new List<IAction>();

            this.actionManager.OpenTransaction(this);

            IsDelayed = isDelayed;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 생성하기 - Create(actionManager, isDelayed)

        /// <summary>
        /// 생성하기
        /// </summary>
        /// <param name="actionManager">액션 관리자</param>
        /// <param name="isDelayed">지연 여부</param>
        /// <returns>트랜잭션</returns>
        public static Transaction Create(ActionManager actionManager, bool isDelayed)
        {
            if(actionManager == null)
            {
                throw new ArgumentNullException("actionManager");
            }

            return new Transaction(actionManager, isDelayed);
        }

        #endregion
        #region 생성하기 - Create(actionManager)

        /// <summary>
        /// 생성하기
        /// </summary>
        /// <param name="actionManager">액션 관리자</param>
        /// <returns>트랜잭션</returns>
        public static Transaction Create(ActionManager actionManager)
        {
            return Create(actionManager, true);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Public

        #region (IAction) 실행 가능 여부 구하기 - CanExecute()

        /// <summary>
        /// 실행 가능 여부 구하기
        /// </summary>
        /// <returns>실행 가능 여부</returns>
        public bool CanExecute()
        {
            foreach (var action in this.actionList)
            {
                if (!action.CanExecute())
                {
                    return false;
                }
            }
            return true;
        }

        #endregion
        #region (IAction) 실행하기 - Execute()

        /// <summary>
        /// 실행하기
        /// </summary>
        public void Execute()
        {
            if(!IsDelayed)
            {
                IsDelayed = true;

                return;
            }

            foreach(IAction action in this.actionList)
            {
                action.Execute();
            }
        }

        #endregion
        #region (IAction) 실행 취소 가능 여부 구하기 - CanUnexecute()

        /// <summary>
        /// 실행 취소 가능 여부 구하기
        /// </summary>
        /// <returns>실행 취소 가능 여부</returns>
        public bool CanUnexecute()
        {
            foreach(IAction action in Enumerable.Reverse(this.actionList))
            {
                if(!action.CanUnexecute())
                {
                    return false;
                }
            }

            return true;
        }

        #endregion
        #region (IAction) 실행 취소하기 - Unexecute()

        /// <summary>
        /// 실행 취소하기
        /// </summary>
        public void Unexecute()
        {
            foreach(IAction action in Enumerable.Reverse(this.actionList))
            {
                action.Unexecute();
            }
        }

        #endregion
        #region (IAction) 병합 시도하기 - TryToMerge(sourceAction)

        /// <summary>
        /// 병합 시도하기
        /// </summary>
        /// <param name="sourceAction">소스 액션</param>
        /// <returns>처리 결과</returns>
        public bool TryToMerge(IAction sourceAction)
        {
            return false;
        }

        #endregion

        #region 커밋하기 - Commit()

        /// <summary>
        /// 커밋하기
        /// </summary>
        public void Commit()
        {
            this.actionManager.CommitTransaction();
        }

        #endregion
        #region 롤백하기 - Rollback()

        /// <summary>
        /// 롤백하기
        /// </summary>
        public void Rollback()
        {
            this.actionManager.RollBackTransaction();

            Aborted = true;
        }

        #endregion

        #region 리소스 해제하기 - Dispose()

        /// <summary>
        /// 리소스 해제하기
        /// </summary>
        public void Dispose()
        {
            if(!Aborted)
            {
                Commit();
            }
        }

        #endregion

        #region 추가하기 - Add(action)

        /// <summary>
        /// 추가하기
        /// </summary>
        /// <param name="action">액션</param>
        public void Add(IAction action)
        {
            if(action == null)
            {
                throw new ArgumentNullException("action");
            }

            this.actionList.Add(action);
        }

        #endregion
        #region 액션 보유 여부 구하기 - HasActions()

        /// <summary>
        /// 액션 보유 여부 구하기
        /// </summary>
        /// <returns>액션 보유 여부</returns>
        public bool HasActions()
        {
            return this.actionList.Count != 0;
        }

        #endregion
        #region 제거하기 - Remove(action)

        /// <summary>
        /// 제거하기
        /// </summary>
        /// <param name="action">액션</param>
        public void Remove(IAction action)
        {
            if(action == null)
            {
                throw new ArgumentNullException("action");
            }

            this.actionList.Remove(action);
        }

        #endregion
    }
}

 

▶ DefaultHistoryNode.cs

namespace TestLibrary
{
    /// <summary>
    /// 디폴트 히스토리 노드
    /// </summary>
    internal class DefaultHistoryNode
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 이전 액션 - PreviousAction

        /// <summary>
        /// 이전 액션
        /// </summary>
        public IAction PreviousAction { get; set; }

        #endregion
        #region 다음 액션 - NextAction

        /// <summary>
        /// 다음 액션
        /// </summary>
        public IAction NextAction { get; set; }

        #endregion
        #region 이전 노드 - PreviousNode

        /// <summary>
        /// 이전 노드
        /// </summary>
        public DefaultHistoryNode PreviousNode { get; set; }

        #endregion
        #region 다음 노드 - NextNode

        /// <summary>
        /// 다음 노드
        /// </summary>
        public DefaultHistoryNode NextNode { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DefaultHistoryNode()

        /// <summary>
        /// 생성자
        /// </summary>
        public DefaultHistoryNode()
        {
        }

        #endregion
        #region 생성자 - DefaultHistoryNode(lastExistingAction, lastExistingNode)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="lastExistingAction">마지막 기존 액션</param>
        /// <param name="lastExistingNode">마지막 기존 노드</param>
        public DefaultHistoryNode(IAction lastExistingAction, DefaultHistoryNode lastExistingNode)
        {
            PreviousAction = lastExistingAction;
            PreviousNode   = lastExistingNode;
        }

        #endregion
    }
}

 

▶ IActionHistory.cs

using System;
using System.Collections.Generic;

namespace TestLibrary
{
    /// <summary>
    /// 액션 히스토리 인터페이스
    /// </summary>
    internal interface IActionHistory : IEnumerable<IAction>
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event

        #region 컬렉션 변경시 이벤트 - CollectionChanged

        /// <summary>
        /// 컬렉션 변경시 이벤트
        /// </summary>
        event EventHandler CollectionChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property

        #region 뒤로 이동 가능 여부 - CanMoveBack

        /// <summary>
        /// 뒤로 이동 가능 여부
        /// </summary>
        bool CanMoveBack { get; }

        #endregion
        #region 앞으로 이동 가능 여부 - CanMoveForward 

        /// <summary>
        /// 앞으로 이동 가능 여부
        /// </summary>
        bool CanMoveForward { get; }

        #endregion
        #region 길이 - Length

        /// <summary>
        /// 길이
        /// </summary>
        int Length { get; }

        #endregion
        #region 현재 노드 - CurrentNode

        /// <summary>
        /// 현재 노드
        /// </summary>
        DefaultHistoryNode CurrentNode { get; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method

        #region 액션 추가하기 - AppendAction(action)

        /// <summary>
        /// 액션 추가하기
        /// </summary>
        /// <param name="action">액션</param>
        /// <returns>처리 결과</returns>
        bool AppendAction(IAction action);

        #endregion
        #region 지우기 - Clear()

        /// <summary>
        /// 지우기
        /// </summary>
        void Clear();

        #endregion
        #region 뒤로 이동하기 - MoveBack()

        /// <summary>
        /// 뒤로 이동하기
        /// </summary>
        void MoveBack();

        #endregion
        #region 앞으로 이동하기 - MoveForward()

        /// <summary>
        /// 앞으로 이동하기
        /// </summary>
        void MoveForward();

        #endregion
        #region 취소 가능한 액션 열거 가능형 구하기 - GetUndoableActionEnumerable()

        /// <summary>
        /// 취소 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>취소 가능한 액션 열거 가능형</returns>
        IEnumerable<IAction> GetUndoableActionEnumerable();

        #endregion
        #region 재실행 가능한 액션 열거 가능형 구하기 - GetRedoableActionEnumerable()

        /// <summary>
        /// 재실행 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>재실행 가능한 액션 열거 가능형</returns>
        IEnumerable<IAction> GetRedoableActionEnumerable();

        #endregion
    }
}

 

▶ DefaultHistory.cs

using System;
using System.Collections;
using System.Collections.Generic;

namespace TestLibrary
{
    /// <summary>
    /// 디폴트 히스토리
    /// </summary>
    internal class DefaultHistory : IActionHistory
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region (IActionHistory) 컬렉션 변경시 이벤트 - CollectionChanged

        /// <summary>
        /// 컬렉션 변경시 이벤트
        /// </summary>
        public event EventHandler CollectionChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 현재 노드
        /// </summary>
        private DefaultHistoryNode currentNode = new DefaultHistoryNode();

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 헤드 노드 - HeadNode

        /// <summary>
        /// 헤드 노드
        /// </summary>
        public DefaultHistoryNode HeadNode { get; set; }

        #endregion
        #region 마지막 액션 - LastAction

        /// <summary>
        /// 마지막 액션
        /// </summary>
        public IAction LastAction { get; set; }

        #endregion

        #region (IActionHistory) 뒤로 이동 가능 여부 - CanMoveBack

        /// <summary>
        /// 뒤로 이동 가능 여부
        /// </summary>
        public bool CanMoveBack
        {
            get
            {
                return CurrentNode.PreviousAction != null && CurrentNode.PreviousNode != null;
            }
        }

        #endregion
        #region (IActionHistory) 앞으로 이동 가능 여부 - CanMoveForward

        /// <summary>
        /// 앞으로 이동 가능 여부
        /// </summary>
        public bool CanMoveForward
        {
            get
            {
                return CurrentNode.NextAction != null && CurrentNode.NextNode != null;
            }
        }

        #endregion
        #region (IActionHistory) 길이 - Length

        /// <summary>
        /// 길이
        /// </summary>
        public int Length { get; set; }

        #endregion
        #region (IActionHistory) 현재 노드 - CurrentNode

        /// <summary>
        /// 현재 노드
        /// </summary>
        public DefaultHistoryNode CurrentNode
        {
            get
            {
                return this.currentNode;
            }
            set
            {
                if(value != null)
                {
                    this.currentNode = value;
                }
                else
                {
                    throw new ArgumentNullException("CurrentNode");
                }
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DefaultHistory()

        /// <summary>
        /// 생성자
        /// </summary>
        public DefaultHistory()
        {
            Initialize();
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 열거자 구하기 - GetEnumerator()

        /// <summary>
        /// 열거자 구하기
        /// </summary>
        /// <returns>열거자</returns>
        public IEnumerator<IAction> GetEnumerator()
        {
            return GetUndoableActionEnumerable().GetEnumerator();
        }

        #endregion
        #region 열거자 구하기 - IEnumerable.GetEnumerator()

        /// <summary>
        /// 열거자 구하기
        /// </summary>
        /// <returns>열거자</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region (IActionHistory) 액션 추가하기 - AppendAction(action)

        /// <summary>
        /// 액션 추가하기
        /// </summary>
        /// <param name="action">액션</param>
        /// <returns>처리 결과</returns>
        public bool AppendAction(IAction action)
        {
            if(CurrentNode.PreviousAction != null && CurrentNode.PreviousAction.TryToMerge(action))
            {
                FireCollectionChangedEvent();

                return false;
            }

            CurrentNode.NextAction = action;
            CurrentNode.NextNode   = new DefaultHistoryNode(action, CurrentNode);

            return true;
        }

        #endregion
        #region (IActionHistory) 지우기 - Clear()

        /// <summary>
        /// 지우기
        /// </summary>
        public void Clear()
        {
            Initialize();

            FireCollectionChangedEvent();
        }

        #endregion
        #region (IActionHistory) 뒤로 이동하기 - MoveBack()

        /// <summary>
        /// 뒤로 이동하기
        /// </summary>
        public void MoveBack()
        {
            if(!CanMoveBack)
            {
                throw new InvalidOperationException
                (
                    "History.MoveBack() cannot execute because CanMoveBack returned false " +
                    "(the current node is the last node in the undo buffer."
                );
            }

            CurrentNode.PreviousAction.Unexecute();

            CurrentNode = CurrentNode.PreviousNode;

            Length -= 1;

            FireCollectionChangedEvent();
        }

        #endregion
        #region (IActionHistory) 앞으로 이동하기 - MoveForward()

        /// <summary>
        /// 앞으로 이동하기
        /// </summary>
        public void MoveForward()
        {
            if(!CanMoveForward)
            {
                throw new InvalidOperationException
                (
                    "History.MoveForward() cannot execute because CanMoveForward returned false " +
                    "(the current node is the last node in the undo buffer."
                );
            }

            CurrentNode.NextAction.Execute();

            CurrentNode = CurrentNode.NextNode;

            Length += 1;

            FireCollectionChangedEvent();
        }

        #endregion
        #region (IActionHistory) 취소 가능한 액션 열거 가능형 구하기 - GetUndoableActionEnumerable()

        /// <summary>
        /// 취소 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>취소 가능한 액션 열거 가능형</returns>
        public IEnumerable<IAction> GetUndoableActionEnumerable()
        {
            DefaultHistoryNode currentNode = HeadNode;

            while(currentNode != null && currentNode != CurrentNode && currentNode.NextAction != null)
            {
                yield return currentNode.NextAction;

                currentNode = currentNode.NextNode;
            }
        }

        #endregion
        #region (IActionHistory) 재실행 가능한 액션 열거 가능형 구하기 - GetRedoableActionEnumerable()

        /// <summary>
        /// 재실행 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>재실행 가능한 액션 열거 가능형</returns>
        public IEnumerable<IAction> GetRedoableActionEnumerable()
        {
            DefaultHistoryNode currentNode = CurrentNode;

            while(currentNode != null && currentNode.NextAction != null)
            {
                yield return currentNode.NextAction;

                currentNode = currentNode.NextNode;
            }
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 컬렉션 변경시 이벤트 발생시키기 - FireCollectionChangedEvent()

        /// <summary>
        /// 컬렉션 변경시 이벤트 발생시키기
        /// </summary>
        protected void FireCollectionChangedEvent()
        {
            CollectionChanged?.Invoke(this, new EventArgs());
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 초기화하기 - Initialize()

        /// <summary>
        /// 초기화하기
        /// </summary>
        private void Initialize()
        {
            CurrentNode = new DefaultHistoryNode();
            HeadNode    = CurrentNode;
        }

        #endregion
    }
}

 

▶ ActionManager.cs

using System;
using System.Collections.Generic;

namespace TestLibrary
{
    /// <summary>
    /// 액션 관리자
    /// </summary>
    public class ActionManager
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 컬렉션 변경시 이벤트 - CollectionChanged

        /// <summary>
        /// 컬렉션 변경시 이벤트
        /// </summary>
        public event EventHandler CollectionChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 트랜잭션 스택
        /// </summary>
        private Stack<Transaction> transactionStack = new Stack<Transaction>();

        /// <summary>
        /// 액션 히스토리
        /// </summary>
        private IActionHistory history;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 트랜잭션 스택 - TransactionStack

        /// <summary>
        /// 트랜잭션 스택
        /// </summary>
        public Stack<Transaction> TransactionStack
        {
            get
            {
                return this.transactionStack;
            }
        }

        #endregion
        #region 기록 트랜잭션 - RecordingTransaction

        /// <summary>
        /// 기록 트랜잭션
        /// </summary>
        public Transaction RecordingTransaction
        {
            get
            {
                if(TransactionStack.Count > 0)
                {
                    return TransactionStack.Peek();
                }

                return null;
            }
        }

        #endregion

        #region 현재 액션 - CurrentAction

        /// <summary>
        /// 현재 액션
        /// </summary>
        public IAction CurrentAction { get; internal set; }

        #endregion
        #region 액션 실행 여부 - IsActionExecuting

        /// <summary>
        /// 액션 실행 여부
        /// </summary>
        public bool IsActionExecuting
        {
            get
            {
                return CurrentAction != null;
            }
        }

        #endregion
        #region 기록없이 즉시 실행 여부 - ExecuteImmediatelyWithoutRecording

        /// <summary>
        /// 기록없이 즉시 실행 여부
        /// </summary>
        public bool ExecuteImmediatelyWithoutRecording { get; set; }

        #endregion

        #region 실행 취소 가능 여부 - CanUndo

        /// <summary>
        /// 실행 취소 가능 여부
        /// </summary>
        public bool CanUndo
        {
            get
            {
                return History.CanMoveBack;
            }
        }

        #endregion
        #region 재실행 가능 여부 - CanRedo

        /// <summary>
        /// 재실행 가능 여부
        /// </summary>
        public bool CanRedo
        {
            get
            {
                return History.CanMoveForward;
            }
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Internal

        #region 히스토리 - History

        /// <summary>
        /// 히스토리
        /// </summary>
        internal IActionHistory History
        {
            get
            {
                return this.history;
            }
            set
            {
                if(this.history != null)
                {
                    this.history.CollectionChanged -= history_CollectionChanged;
                }

                this.history = value;

                if(this.history != null)
                {
                    this.history.CollectionChanged += history_CollectionChanged;
                }
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - ActionManager()

        /// <summary>
        /// 생성자
        /// </summary>
        public ActionManager()
        {
            History = new DefaultHistory();
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 액션 기록하기 - RecordAction(action)

        /// <summary>
        /// 액션 기록하기
        /// </summary>
        /// <param name="action">액션</param>
        public void RecordAction(IAction action)
        {
            if(action == null)
            {
                throw new ArgumentNullException("ActionManager.RecordAction : the action argument is null");
            }

            CheckNotRunningBeforeRecording(action);

            if(ExecuteImmediatelyWithoutRecording && action.CanExecute())
            {
                action.Execute();

                return;
            }

            Transaction currentTransaction = RecordingTransaction;

            if(currentTransaction != null)
            {
                currentTransaction.Add(action);

                if(!currentTransaction.IsDelayed)
                {
                    action.Execute();
                }
            }
            else
            {
                RunActionDirectly(action);
            }
        }

        #endregion

        #region 트랜잭션 생성하기 - CreateTransaction()

        /// <summary>
        /// 트랜잭션 생성하기
        /// </summary>
        /// <returns>트랜잭션</returns>
        public Transaction CreateTransaction()
        {
            return Transaction.Create(this);
        }

        #endregion
        #region 트랜잭션 생성하기 - CreateTransaction(delayed)

        /// <summary>
        /// 트랜잭션 생성하기
        /// </summary>
        /// <param name="delayed">지연 여부</param>
        /// <returns>트랜잭션</returns>
        public Transaction CreateTransaction(bool delayed)
        {
            return Transaction.Create(this, delayed);
        }

        #endregion
        #region 트랜잭션 열기 - OpenTransaction(transaction)

        /// <summary>
        /// 트랜잭션 열기
        /// </summary>
        /// <param name="transaction">트랜잭션</param>
        public void OpenTransaction(Transaction transaction)
        {
            TransactionStack.Push(transaction);
        }

        #endregion
        #region 트랜잭션 커밋하기 - CommitTransaction()

        /// <summary>
        /// 트랜잭션 커밋하기
        /// </summary>
        public void CommitTransaction()
        {
            if(TransactionStack.Count == 0)
            {
                throw new InvalidOperationException
                (
                    "ActionManager.CommitTransaction was called"                               +
                    " when there is no open transaction (TransactionStack is empty)."          +
                    " Please examine the stack trace of this exception to find code"           +
                    " which called CommitTransaction one time too many."                       +
                    " Normally you don't call OpenTransaction and CommitTransaction directly," +
                    " but use using(var t = Transaction.Create(Root)) instead."
                );
            }

            Transaction transaction = TransactionStack.Pop();

            if(transaction.HasActions())
            {
                RecordAction(transaction);
            }
        }

        #endregion
        #region 트랜잭션 롤백하기 - RollBackTransaction()

        /// <summary>
        /// 트랜잭션 롤백하기
        /// </summary>
        public void RollBackTransaction()
        {
            if(TransactionStack.Count != 0)
            {
                Transaction transaction = TransactionStack.Peek();

                if(transaction != null)
                {
                    transaction.Unexecute();
                }

                TransactionStack.Clear();
            }
        }

        #endregion

        #region 실행 취소하기 - Undo()

        /// <summary>
        /// 실행 취소하기
        /// </summary>
        public void Undo()
        {
            if(!CanUndo)
            {
                return;
            }

            if(IsActionExecuting)
            {
                throw new InvalidOperationException
                (
                    string.Format
                    (
                        "ActionManager is currently busy"                                    +
                        " executing a transaction ({0}). This transaction has called Undo()" +
                        " which is not allowed until the transaction ends."                  +
                        " Please examine the stack trace of this exception to see"           +
                        " what part of your code called Undo.",
                        CurrentAction
                    )
                );
            }
 
            CurrentAction = History.CurrentNode.PreviousAction;

            History.MoveBack();

            CurrentAction = null;
        }

        #endregion
        #region 다시 실행하기 - Redo()

        /// <summary>
        /// 다시 실행하기
        /// </summary>
        public void Redo()
        {
            if(!CanRedo)
            {
                return;
            }

            if(IsActionExecuting)
            {
                throw new InvalidOperationException
                (
                    string.Format
                    (
                        "ActionManager is currently busy"                                    +
                        " executing a transaction ({0}). This transaction has called Redo()" +
                        " which is not allowed until the transaction ends."                  +
                        " Please examine the stack trace of this exception to see"           +
                        " what part of your code called Redo.",
                        CurrentAction
                    )
                );
            }

            CurrentAction = History.CurrentNode.NextAction;

            History.MoveForward();

            CurrentAction = null;
        }

        #endregion

        #region 지우기 - Clear()

        /// <summary>
        /// 지우기
        /// </summary>
        public void Clear()
        {
            History.Clear();

            CurrentAction = null;
        }

        #endregion

        #region 취소 가능한 액션 열거 가능형 구하기 - GetUndoableActionEnumerable()

        /// <summary>
        /// 취소 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>취소 가능한 액션 열거 가능형</returns>
        public IEnumerable<IAction> GetUndoableActionEnumerable()
        {
            return History.GetUndoableActionEnumerable();
        }

        #endregion
        #region 재실행 가능한 액션 열거 가능형 구하기 - GetRedoableActionEnumerable()

        /// <summary>
        /// 재실행 가능한 액션 열거 가능형 구하기
        /// </summary>
        /// <returns>재실행 가능한 액션 열거 가능형</returns>
        public IEnumerable<IAction> GetRedoableActionEnumerable()
        {
            return History.GetRedoableActionEnumerable();
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 히스토리 컬렉션 변경시 처리하기 - history_CollectionChanged(sender, e)

        /// <summary>
        /// 히스토리 컬렉션 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        protected void history_CollectionChanged(object sender, EventArgs e)
        {
            CollectionChanged?.Invoke(this, e);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 기록전 미실행 체크하기 - CheckNotRunningBeforeRecording(candidateAction)

        /// <summary>
        /// 기록전 미실행 체크하기
        /// </summary>
        /// <param name="candidateAction">후보 액션</param>
        private void CheckNotRunningBeforeRecording(IAction candidateAction)
        {
            if(CurrentAction != null)
            {
                string candidateActionName = candidateAction != null ? candidateAction.ToString() : "";

                throw new InvalidOperationException
                (
                    string.Format
                    (
                        "ActionManager.RecordActionDirectly : the ActionManager is currently running "          +
                        "or undoing an action ({0}), and this action (while being executed) attempted "         +
                        "to recursively record another action ({1}), which is not allowed. "                    +
                        "You can examine the stack trace of this exception to see what the "                    +
                        "executing action did wrong and change this action not to influence the "               +
                        "Undo stack during its execution. Checking if ActionManager.ActionIsExecuting == true " +
                        "before launching another transaction might help to avoid the problem. Thanks and sorry for the inconvenience.",
                        CurrentAction.ToString(),
                        candidateActionName
                    )
                );
            }
        }

        #endregion
        #region 직접 액션 실행하기 - RunActionDirectly(action)

        /// <summary>
        /// 직접 액션 실행하기
        /// </summary>
        /// <param name="action">액션</param>
        private void RunActionDirectly(IAction action)
        {
            CheckNotRunningBeforeRecording(action);

            CurrentAction = action;

            try
            {
                if(History.AppendAction(action))
                {
                    History.MoveForward();
                }
            }
            finally
            {
                CurrentAction = null;
            }
        }

        #endregion
    }
}

 

▶ ActionManagerExtensions.cs

using System.Collections.Generic;

namespace TestLibrary
{
    /// <summary>
    /// 액션 관리자 확장
    /// </summary>
    public static class ActionManagerExtension
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 실행하기 - Execute(actionManager, action)

        /// <summary>
        /// 실행하기
        /// </summary>
        /// <param name="actionManager">액션 관리자</param>
        /// <param name="action">액션</param>
        public static void Execute(this ActionManager actionManager, IAction action)
        {
            if(actionManager == null)
            {
                action.Execute();
            }
            else
            {
                actionManager.RecordAction(action);
            }
        }

        #endregion
        #region 속성 설정하기 - SetProperty(actionManager, targetObject, propertyName, value)

        /// <summary>
        /// 속성 설정하기
        /// </summary>
        /// <param name="actionManager">액션 관리자</param>
        /// <param name="targetObject">타겟 객체</param>
        /// <param name="propertyName">속성명</param>
        /// <param name="value">값</param>
        public static void SetProperty(this ActionManager actionManager, object targetObject, string propertyName, object value)
        {
            SetPropertyAction action = new SetPropertyAction(targetObject, propertyName, value);

            Execute(actionManager, action);
        }

        #endregion
        #region 항목 추가하기 - AddItem<TItem>(actionManager, collection, item)

        /// <summary>
        /// 항목 추가하기
        /// </summary>
        /// <typeparam name="TItem">항목 타입</typeparam>
        /// <param name="actionManager">액션 관리자</param>
        /// <param name="collection">컬렉션</param>
        /// <param name="item">항목</param>
        public static void AddItem<TItem>(this ActionManager actionManager, ICollection<TItem> collection, TItem item)
        {
            AddItemAction<TItem> action = new AddItemAction<TItem>(collection.Add, t => collection.Remove(t), item);

            actionManager.Execute(action);
        }

        #endregion
    }
}

 

[TestConsole 프로젝트]

▶ SetConsoleColorAction.cs

using System;

using TestLibrary;

namespace TestConsole
{
    /// <summary>
    /// 콘솔 색상 설정 액션
    /// </summary>
    public class SetConsoleColorAction : AbstractAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 색상
        /// </summary>
        private ConsoleColor color;

        /// <summary>
        /// 이전 색상
        /// </summary>
        private ConsoleColor previousColor;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - SetConsoleColorAction(color)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="color">색상</param>
        public SetConsoleColorAction(ConsoleColor color)
        {
            this.color = color;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected override void ExecuteCore()
        {
            this.previousColor = Console.ForegroundColor;

            Console.ForegroundColor = color;
        }

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected override void UnexecuteCore()
        {
            Console.ForegroundColor = this.previousColor;
        }

        #endregion
    }
}

 

▶ Program.cs

using System;

using TestLibrary;

namespace TestConsole
{
    /// <summary>
    /// 프로그램
    /// </summary>
    class Program
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 액션 관리자
        /// </summary>
        private static ActionManager _actionManager = new ActionManager();

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 프로그램 시작하기 - Main()

        /// <summary>
        /// 프로그램 시작하기
        /// </summary>
        private static void Main()
        {
            Console.WriteLine("원래 색상");

            SetConsoleColor(ConsoleColor.Green);

            Console.WriteLine("신규 색상");

            _actionManager.Undo();

            Console.WriteLine("다시 이전 색상");

            using(Transaction.Create(_actionManager))
            {
                SetConsoleColor(ConsoleColor.Red);

                Console.WriteLine("LAZY 평가 때문에 여전히 Red로 변경되지 않았습니다.");

                SetConsoleColor(ConsoleColor.Blue);
            }

            Console.WriteLine("한 번에 두 가지 색상을 변경함");

            _actionManager.Undo();

            Console.WriteLine("원래 색상으로 돌아감");

            _actionManager.Redo();

            Console.WriteLine("다시 청색으로");
        }

        #endregion
        #region 콘솔 색상 설정하기 - SetConsoleColor(color)

        /// <summary>
        /// 콘솔 색상 설정하기
        /// </summary>
        /// <param name="color">색상</param>
        private static void SetConsoleColor(ConsoleColor color)
        {
            SetConsoleColorAction action = new SetConsoleColorAction(color);

            _actionManager.RecordAction(action);
        }

        #endregion
    }
}

 

[TestWinForm 프로젝트]

▶ Person.cs

using System;

namespace TestWinForm
{
    /// <summary>
    /// 사람
    /// </summary>
    public class Person
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 성명 변경시 이벤트 - NameChanged

        /// <summary>
        /// 성명 변경시 이벤트
        /// </summary>
        public event Action NameChanged;

        #endregion
        #region 나이 변경시 이벤트 - AgeChanged

        /// <summary>
        /// 나이 변경시 이벤트
        /// </summary>
        public event Action AgeChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 성명
        /// </summary>
        private string name;

        /// <summary>
        /// 나이
        /// </summary>
        private int age;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 성명 - Name

        /// <summary>
        /// 성명
        /// </summary>
        public string Name
        {
            get
            {
                return this.name;
            }
            set
            {
                this.name = value;

                if(NameChanged != null)
                {
                    NameChanged();
                }
            }
        }

        #endregion
        #region 나이 - Age

        /// <summary>
        /// 나이
        /// </summary>
        public int Age
        {
            get
            {
                return this.age;
            }
            set
            {
                this.age = value;

                if(AgeChanged != null)
                {
                    AgeChanged();
                }
            }
        }

        #endregion
    }
}

 

▶ MultiLevelUndoTextBoxChangeAction.cs

using System;
using System.Windows.Forms;

using TestLibrary;

namespace TestWinForm
{
    /// <summary>
    /// 멀티 레벨 실행 취소 텍스트 박스 변경 액션
    /// </summary>
    public class MultiLevelUndoTextBoxChangeAction : AbstractAction
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 텍스트 박스
        /// </summary>
        private readonly TextBox textBox;

        /// <summary>
        /// 이전 텍스트
        /// </summary>
        private readonly string previousText;

        /// <summary>
        /// 이전 선택 시작
        /// </summary>
        private readonly int previousSelectionStart;

        /// <summary>
        /// 이전 선택 길이
        /// </summary>
        private readonly int previousSelectionLength;

        /// <summary>
        /// 신규 텍스트
        /// </summary>
        private string newText;

        /// <summary>
        /// 신규 선택 시작
        /// </summary>
        private int newSelectionStart;

        /// <summary>
        /// 신규 선택 길이
        /// </summary>
        private int newSelectionLength;
        
        /// <summary>
        /// 타임 스탬프
        /// </summary>
        private readonly DateTime timeStamp;

        /// <summary>
        /// 첫번째 여부
        /// </summary>
        private bool firstTime = true;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MultiLevelUndoTextBoxChangeAction(textBox, previousText, previousSelectionStart, previousSelectionLength)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="textBox">텍스트 박스</param>
        /// <param name="previousText">이전 텍스트</param>
        /// <param name="previousSelectionStart">이전 선택 시작</param>
        /// <param name="previousSelectionLength">이전 선택 길이</param>
        public MultiLevelUndoTextBoxChangeAction(TextBox textBox, string previousText, int previousSelectionStart, int previousSelectionLength)
        {
            this.textBox                 = textBox;
            this.previousText            = previousText;
            this.previousSelectionStart  = previousSelectionStart;
            this.previousSelectionLength = previousSelectionLength;
            this.newText                 = this.textBox.Text;
            this.newSelectionStart       = this.textBox.SelectionStart;
            this.newSelectionLength      = this.textBox.SelectionLength;
            this.timeStamp               = DateTime.Now;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 병합 시도하기 - TryToMerge(sourceAction)

        /// <summary>
        /// 병합 시도하기
        /// </summary>
        /// <param name="sourceAction">소스 액션</param>
        /// <returns>처리 결과</returns>
        public override bool TryToMerge(IAction sourceAction)
        {
            MultiLevelUndoTextBoxChangeAction action = sourceAction as MultiLevelUndoTextBoxChangeAction;

            if(action == null)
            {
                return false;
            }

            TimeSpan timeSpan = action.timeStamp.Subtract(this.timeStamp);

            if(timeSpan.Seconds > 3)
            {
                return false;
            }

            this.newText            = action.newText;
            this.newSelectionStart  = action.newSelectionStart;
            this.newSelectionLength = action.newSelectionLength;

            SetText(this.newText, this.newSelectionStart, this.newSelectionLength);

            return true;
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 실행하기 (코어) - ExecuteCore()

        /// <summary>
        /// 실행하기 (코어)
        /// </summary>
        protected override void ExecuteCore()
        {
            if(this.firstTime)
            {
                this.firstTime = false;

                return;
            }

            SetText(this.newText, this.newSelectionStart, this.newSelectionLength);
        }

        #endregion
        #region 실행 취소하기 (코어) - UnexecuteCore()

        /// <summary>
        /// 실행 취소하기 (코어)
        /// </summary>
        protected override void UnexecuteCore()
        {
            SetText(this.previousText, this.previousSelectionStart, this.previousSelectionLength);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 텍스트 설정하기 - SetText(text, selectionStart, selectionLength)

        /// <summary>
        /// 텍스트 설정하기
        /// </summary>
        /// <param name="text">텍스트</param>
        /// <param name="selectionStart">선택 시작</param>
        /// <param name="selectionLength">선택 길이</param>
        private void SetText(string text, int selectionStart, int selectionLength)
        {
            this.textBox.Text            = text;
            this.textBox.SelectionStart  = selectionStart;
            this.textBox.SelectionLength = selectionLength;

            this.textBox.Focus();
        }

        #endregion
    }
}

 

▶ MainForm.cs

using System;
using System.Windows.Forms;

using TestLibrary;

namespace TestWinForm
{
    /// <summary>
    /// 메인 폼
    /// </summary>
    public partial class MainForm : Form
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        #region 샘플용

        /// <summary>
        /// 액션 관리자
        /// </summary>
        private ActionManager actionManager = new ActionManager();

        /// <summary>
        /// 사람
        /// </summary>
        private Person persion = new Person();

        /// <summary>
        /// 코드에 의해서 UI 업데이트 여부
        /// </summary>
        private bool uiUpdatedByCode = false;

        #endregion
        #region 멀티 레벨 실행 취소 텍스트 박스용

        /// <summary>
        /// 멀티 레벨 실행 취소 텍스트 박스 액션 관리자
        /// </summary>
        private ActionManager multiLevelUndoTextBoxActionManager = new ActionManager();

        /// <summary>
        /// 이전 텍스트
        /// </summary>
        private string previousText = string.Empty;

        /// <summary>
        /// 이전 선택 시작
        /// </summary>
        private int previousSelectionStart = 0;

        /// <summary>
        /// 이전 선택 길이
        /// </summary>
        private int previuosSelectionLength = 0;

        /// <summary>
        /// 잠금 여부
        /// </summary>
        private bool locked = false;

        #endregion

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MainForm()

        /// <summary>
        /// 생성자
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            this.persion.NameChanged += persion_NameChanged;
            this.persion.AgeChanged  += persion_AgeChanged;

            this.redoButton.Click        += redoButton_Click;
            this.undoButton.Click        += undoButton_Click;
            this.setAllButton.Click      += setAllButton_Click;
            this.nameTextBox.TextChanged += nameTextBox_TextChanged;
            this.ageTextBox.TextChanged  += ageTextBox_TextChanged;

            this.multiLevelUndoTextBox.TextChanged     += multiLevelUndoTextBox_TextChanged;
            this.multiLevelUndoTextBoxUndoButton.Click += multiLevelUndoTextBoxUndoButton_Click;
            this.multiLevelUndoTextBoxRedoButton.Click += multiLevelUndoTextBoxRedoButton_Click;

            SetButtonEnabled();
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Private
        //////////////////////////////////////////////////////////////////////////////// Event

        #region 사람 성명 변경시 처리하기 - persion_NameChanged()

        /// <summary>
        /// 사람 성명 변경시 처리하기
        /// </summary>
        private void persion_NameChanged()
        {
            this.uiUpdatedByCode = true;

            this.nameTextBox.Text = this.persion.Name;

            this.uiUpdatedByCode = false;
        }

        #endregion
        #region 사람 나이 변경시 처리하기 - persion_AgeChanged()

        /// <summary>
        /// 사람 나이 변경시 처리하기
        /// </summary>
        private void persion_AgeChanged()
        {
            this.uiUpdatedByCode = true;

            this.ageTextBox.Text = this.persion.Age.ToString();

            this.uiUpdatedByCode = false;
        }

        #endregion

        #region 실행 취소 버튼 클릭시 처리하기 - undoButton_Click(sender, e)

        /// <summary>
        /// 실행 취소 버튼 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void undoButton_Click(object sender, EventArgs e)
        {
            this.actionManager.Undo();

            SetButtonEnabled();
        }

        #endregion
        #region 재실행 버튼 클릭시 처리하기 - redoButton_Click(sender, e)

        /// <summary>
        /// 재실행 버튼 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void redoButton_Click(object sender, EventArgs e)
        {
            this.actionManager.Redo();

            SetButtonEnabled();
        }

        #endregion
        #region 모두 설정 버튼 클릭시 처리하기 - setAllButton_Click(sender, e)

        /// <summary>
        /// 모두 설정 버튼 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void setAllButton_Click(object sender, EventArgs e)
        {
            using(Transaction.Create(this.actionManager))
            {
                SetProperty("Name", "Joe");
                SetProperty("Age" , 30   );
            }

            SetButtonEnabled();
        }

        #endregion
        #region 성명 텍스트 박스 텍스트 변경시 처리하기 - nameTextBox_TextChanged(sender, e)

        /// <summary>
        /// 성명 텍스트 박스 텍스트 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void nameTextBox_TextChanged(object sender, EventArgs e)
        {
            SetProperty("Name", this.nameTextBox.Text);
        }

        #endregion
        #region 나이 텍스트 박스 텍스트 변경시 처리하기 - ageTextBox_TextChanged(sender, e)

        /// <summary>
        /// 나이 텍스트 박스 텍스트 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void ageTextBox_TextChanged(object sender, EventArgs e)
        {
            int age = 0;

            if(int.TryParse(this.ageTextBox.Text, out age))
            {
                SetProperty("Age", age);
            }
        }

        #endregion

        #region 멀티 레벨 실행 취소 텍스트 박스 텍스트 변경시 처리하기 - multiLevelUndoTextBox_TextChanged(sender, e)

        /// <summary>
        /// 멀티 레벨 실행 취소 텍스트 박스 텍스트 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void multiLevelUndoTextBox_TextChanged(object sender, EventArgs e)
        {
            if(this.locked)
            {
                return;
            }

            this.locked = true;

            MultiLevelUndoTextBoxChangeAction action = new MultiLevelUndoTextBoxChangeAction
            (
                this.multiLevelUndoTextBox,
                this.previousText,
                this.previousSelectionStart,
                this.previuosSelectionLength
            );

            this.multiLevelUndoTextBoxActionManager.RecordAction(action);

            this.previousText            = this.multiLevelUndoTextBox.Text;
            this.previousSelectionStart  = this.multiLevelUndoTextBox.SelectionStart;
            this.previuosSelectionLength = this.multiLevelUndoTextBox.SelectionLength;

            SetMultiLevelUndoButtonEnabled();

            this.locked = false;
        }

        #endregion
        #region 멀티 레벨 실행 취소 텍스트 박스 실행 취소 버튼 클릭시 처리하기 - multiLevelUndoTextBoxUndoButton_Click(sender, e)

        /// <summary>
        /// 멀티 레벨 실행 취소 텍스트 박스 실행 취소 버튼 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void multiLevelUndoTextBoxUndoButton_Click(object sender, EventArgs e)
        {
            if(this.locked)
            {
                return;
            }

            this.locked = true;

            this.multiLevelUndoTextBoxActionManager.Undo();

            SetMultiLevelUndoButtonEnabled();

            this.previousText = this.multiLevelUndoTextBox.Text;

            this.locked = false;
        }

        #endregion
        #region 멀티 레벨 실행 취소 텍스트 박스 재실행 버튼 클릭시 처리하기 - multiLevelUndoTextBoxRedoButton_Click(sender, e)

        /// <summary>
        /// 멀티 레벨 실행 취소 텍스트 박스 재실행 버튼 클릭시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void multiLevelUndoTextBoxRedoButton_Click(object sender, EventArgs e)
        {
            if(this.locked)
            {
                return;
            }

            this.locked = true;

            this.multiLevelUndoTextBoxActionManager.Redo();

            SetMultiLevelUndoButtonEnabled();

            this.previousText = this.multiLevelUndoTextBox.Text;

            this.locked = false;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Function

        #region 버튼 이용 가능 여부 설정하기 - SetButtonEnabled()

        /// <summary>
        /// 버튼 이용 가능 여부 설정하기
        /// </summary>
        private void SetButtonEnabled()
        {
            this.undoButton.Enabled = this.actionManager.CanUndo;
            this.redoButton.Enabled = this.actionManager.CanRedo;
        }

        #endregion
        #region 속성 설정하기 - SetProperty(propertyName, propertyValue)

        /// <summary>
        /// 속성 설정하기
        /// </summary>
        /// <param name="propertyName">속성명</param>
        /// <param name="propertyValue">속성 값</param>
        private void SetProperty(string propertyName, object propertyValue)
        {
            if(this.uiUpdatedByCode)
            {
                return;
            }

            SetPropertyAction action = new SetPropertyAction(this.persion, propertyName, propertyValue);

            this.actionManager.RecordAction(action);

            SetButtonEnabled();
        }

        #endregion

        #region 멀티 레벨 실행 취소 버튼 이용 가능 여부 설정하기 - SetMultiLevelUndoButtonEnabled()

        /// <summary>
        /// 멀티 레벨 실행 취소 버튼 이용 가능 여부 설정하기
        /// </summary>
        private void SetMultiLevelUndoButtonEnabled()
        {
            this.multiLevelUndoTextBoxRedoButton.Enabled = this.multiLevelUndoTextBoxActionManager.CanRedo;
            this.multiLevelUndoTextBoxUndoButton.Enabled = this.multiLevelUndoTextBoxActionManager.CanUndo;
        }

        #endregion
    }
}
728x90
반응형
그리드형(광고전용)
Posted by icodebroker

댓글을 달아 주세요