読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

Undo,Redoの実装って何回かしかやってない気がする。ジェネリックなCommandパターンとMementoパターンの応用で大体いけそうな気がする。

プログラミング デザインパターン C#2.0 C#3.0

不足気味でしたので、たまにはC#分を補充しておきます。



 ↓  ↑



もう1年近く前になるんですね。以前、下記のエントリが注目を浴びていて、
わたしも楽しく読ませてもらいました。
 

Undo,Redoの実装って何十回もやってる気がする - あしあと日記
http://d.hatena.ne.jp/Youchan/20081110/1226282911
Undo,Redoの実装つづき - あしあと日記
http://d.hatena.ne.jp/Youchan/20081111/1226388917

久々にUndo,Redoを実装する機会があったので、せっかくなのでチラ裏に残しておきます。
いわゆるGoFデザインパターンでいうところのCommandパターンとMementoパターンの応用です。
そしてジェネリック対応することで、Undo,Redoの実装をある程度抽象化することができます。


今回はこのような感じにしてみました。
以下、C#によるジェネリックを利用して抽象化したUndo,Redo機能の実装サンプルです。


ICommandインターフェイス

Undo,Redoの機能を有するコマンドのインターフェイスを定義します。

namespace ClassLibrary1
{
    /// <summary>
    /// ICommandインターフェイス
    /// </summary>
    public interface ICommand
    {
        /// <summary>
        /// 呼び出し
        /// </summary>
        void Invoke();
        /// <summary>
        /// 元に戻す
        /// </summary>
        void Undo();
        /// <summary>
        /// やり直し
        /// </summary>
        void Redo();
    }
}


Memento抽象クラス

コマンド内に保持される、記念品(笑)を表す抽象クラスです。
思い出を保持したり、思い出を呼び覚ませてオブジェクトに反映したりします。
記憶する思い出データと、データ反映対象オブジェクトについてジェネリック型を指定して抽象化します。

namespace ClassLibrary1
{
    /// <summary>
    /// 記念品(笑)抽象クラス
    /// </summary>
    /// <typeparam name="T1">思い出データの型</typeparam>
    /// <typeparam name="T2">データ反映対象オブジェクトの型</typeparam>
    public abstract class Memento<T1, T2>
    {
        /// <summary>
        /// 思い出データを取得または設定します。
        /// </summary>
        public T1 MementoData { get; protected set; }

        /// <summary>
        /// データ反映対象オブジェクトを取得または設定します。
        /// </summary>
        protected T2 Target { get; set; }

        /// <summary>
        /// 思い出を反映させます。
        /// </summary>
        public abstract void SetMemento(T1 _mementoData);
    }
}


ICommandインターフェイスを実装した、思い出更新コマンドクラス

ICommandインターフェイスを実装し、
内部に保持した抽象的なMemento(思い出)について更新処理をするコマンドクラスを作成します。

namespace ClassLibrary1
{
    /// <summary>
    /// 思い出更新コマンド
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class MementoCommand<T1, T2> : ICommand
    {
        private Memento<T1, T2> _memento;
        private T1 _prev;
        private T1 _next;

        public MementoCommand(Memento<T1, T2> prev, Memento<T1, T2> next)
        {
            _memento = prev;
            _prev = prev.MementoData;
            _next = next.MementoData;
        }

        #region ICommand メンバ

        /// <summary>
        /// 呼び出し
        /// </summary>
        void ICommand.Invoke()
        {
            _prev = _memento.MementoData;
            _memento.SetMemento(_next);
        }

        /// <summary>
        /// 元に戻す
        /// </summary>
        void ICommand.Undo()
        {
            _memento.SetMemento(_prev);
        }

        /// <summary>
        /// やり直し
        /// </summary>
        void ICommand.Redo()
        {
            _memento.SetMemento(_next);
        }

        #endregion
    }
}


Undo,Redo機能などのコマンド処理を管理するCommandManagerクラス

Commandパターンの舵取り役である、CommandManagerクラスを作成します。
処理対象はあくまでICommandインターフェイスであり、MementoCommandは意識されていないことに注意してください。
何回分のMementoを保持できるかをコンストラクタで指定できるようにしてみました。

using System.Collections.Generic;

namespace ClassLibrary1
{
    /// <summary>
    /// CommandManager
    /// </summary>
    public sealed class CommandManager
    {
        private int _maxStack = int.MaxValue;
        private Stack<ICommand> _undoStack;
        private Stack<ICommand> _redoStack;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public CommandManager()
        {
            _undoStack = new Stack<ICommand>();
            _redoStack = new Stack<ICommand>();
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="max">最大保存数</param>
        public CommandManager(int maxStack) : this()
        {
            _maxStack = maxStack;
        }

        /// <summary>
        /// 呼び出し
        /// </summary>
        /// <param name="command">コマンド</param>
        public bool Invoke(ICommand command)
        {
            if (_undoStack.Count >= _maxStack) return false;
            command.Invoke();
            _redoStack.Clear();
            _undoStack.Push(command);
            return true;
        }

        /// <summary>
        /// 元に戻す
        /// </summary>
        public void Undo()
        {
            if (_undoStack.Count == 0) return;
            var command = _undoStack.Pop();
            command.Undo();
            _redoStack.Push(command);
        }

        /// <summary>
        /// やり直し
        /// </summary>
        public void Redo()
        {
            if (_redoStack.Count == 0) return;
            var command = _redoStack.Pop();
            command.Redo();
            _undoStack.Push(command);
        }

         /// <summary>
        /// リフレッシュ
        /// </summary>
        public void Refresh()
        {
            _undoStack.Clear();
            _redoStack.Clear();
        }
    }
}


Undo,Redo機能についてお試し

Form1.cs (Form1.Designer.csは省略)

using System;
using System.Windows.Forms;
using ClassLibrary1;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private Memento<string, TextBox> _memento;
        private CommandManager _cmdManager = new CommandManager(6);
        public Form1()
        {
            InitializeComponent();
            _memento = new TextBoxMemento(this.textBox1.Text, this.textBox1);
        }

        private void btnSave_Click(object sender, EventArgs e)
        {
            var current = new TextBoxMemento(this.textBox1.Text,this.textBox1);
            var cmd = new MementoCommand<string,TextBox>(_memento,current);
            if (!_cmdManager.Invoke(cmd))
            {
                MessageBox.Show("状態の最大保存数を超えました。");
                return;
            }
            _memento = current;
        }

        private void btnRefresh_Click(object sender, EventArgs e)
        {
            _cmdManager.Refresh();
        }

        private void btnUndo_Click(object sender, EventArgs e)
        {
            _cmdManager.Undo();
        }

        private void btnRedo_Click(object sender, EventArgs e)
        {
            _cmdManager.Redo();
        }
    }
}


TextBoxについてMementoな具象クラスを作ります。
当たり前ですがタイプセーフです。

TextBoxMemento.cs

using System.Windows.Forms;
using ClassLibrary1;

namespace WindowsFormsApplication1
{
    /// <summary>
    /// TextBoxの思い出具象クラス
    /// </summary>
    public sealed class TextBoxMemento : Memento<string, TextBox>
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="mementoData">思い出データ</param>
        /// <param name="txtBox">データ反映対象オブジェクト</param>
        public TextBoxMemento(string mementoData, TextBox target)
        {
            base.MementoData = mementoData;
            base.Target = target;
        }

        /// <summary>
        /// ターゲットに思い出を反映させます。
        /// </summary>
        public override void SetMemento(string mementoData)
        {
            base.MementoData = mementoData;
            base.Target.Text = mementoData;
        }
    }
}


Program.cs

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

というような感じですが、いかがでしょうか。こういうのを1つ用意しておけば、
一般的なUndo,Redo機能であればまかなえそうです。だいたいイケそうな気がする〜。