| 公開 | 2009-04-04 |
| 更新 | 2009-04-04 |
何らかの「編集」を行うツールには、直前の操作を取り消したり戻したりする Undo/Redo の機能が欲しいものです。(Undo 機能があることに慣れ過ぎて、紙に手書きで文字や絵を書いているときにも Ctrl+Z が欲しくなります!)
非常に便利な Undo/Redo 機構ですが、プログラムとしては、一体、どのように実装するのでしょうか。.NET Framework の標準機能自体には、Undo/Redo を実現するための汎用的な仕組み、といったものは提供されていません。自前で Undo/Redo システムを作り込む必要があります。
Undo/Redo 機構とは、ソフトウェアの要求にもよりますが、基本的には「操作のスタック」であると捉えることができます。
編集の対象となる「項目」に、何らかの「操作」を実行すると、結果として項目の状態が変化します。このとき、変化した状態を元に戻す「逆操作」があるのなら、その実行は Undo となります。(Redo は、Undo で元に戻した状態から、再び前の操作を正方向で実行することを意味する。)
たとえば、Content オブジェクト(「項目」)の Body プロパティ(String 型)の値を変更する操作を考えます。この逆操作とは、Body の値を変更前の値に設定しなおすことです。
ユーザーが実行した過去の操作を記録する「操作の履歴」を用意します。ユーザーが「操作」を実行する度に、その操作を履歴に追加します。
通常、Undo したいのは直前に実行した操作です。また、Undo を連続すると、過去の操作を逆順に一つずつ元に戻すことができます。――これはつまりスタックです。Undo とは、操作履歴スタックの一番上にある操作を取り出して、その逆操作を実行することです。

また、Redo を実現するためには、Undo 用のスタックの隣に、Redo 用のスタックを置きましょう。Undo スタックから取り出して Undo した操作は、Redo スタックの一番上に載せます。逆も同様です。
“操作をスタックに積み上げる”ためには、「操作」はオブジェクトの形をしていることが望ましいです。スタックは、そのまま System.Collections.Generic.Stack<T> クラスを使えます。
という訳で、まずは IUndoableOperation インターフェースを定義します。「Undo できる操作」です。
public interface IUndoableOperation
{
#region Methods
void Execute();
void Undo();
#endregion
}
要求によって最適な形は異なるとは思いますが、ここでは、操作と逆操作を一つのオブジェクトにまとめています。最初の実行と Redo のときは Execute メソッドを、Undo のときは Undo メソッドを呼び出します。IUndoableOperation の実装者は、それぞれの「操作」について、これらのメソッドを実装します。
操作履歴の管理もクラスとしてまとめておきましょう。UndoableOperationHistory オブジェクトは、実行された IUndoableOperation オブジェクトを受け取って、内部に保持する Undo スタックに追加します。
public class UndoableOperationHistory
{
#region Fields
private Stack<IUndoableOperation> undoStack;
private Stack<IUndoableOperation> redoStack;
#endregion
#region Methods
public void Append(IUndoableOperation operation)
{
if (operation == null)
{
throw new ArgumentNullException("operation");
}
// 操作履歴を更新する。
this.redoStack.Clear();
// 操作を履歴に追加する。
this.undoStack.Push(operation);
}
public void Undo()
{
if (this.undoStack.Count > 0)
{
// 操作を取り出す。
var operation = this.undoStack.Pop();
// 操作を実行する。
operation.Undo();
// 操作履歴を更新する。
this.redoStack.Push(operation);
}
}
public void Redo()
{
if (this.redoStack.Count > 0)
{
// 操作を取り出す。
var operation = this.redoStack.Pop();
// 操作を実行する。
operation.Execute();
// 操作履歴を更新する。
this.undoStack.Push(operation);
}
}
#endregion
}
操作の実行と逆操作の実行を IUndoableOperation で定義したので、操作履歴自体に Undo/Redo メソッドを設けることができます。
なお、操作を履歴に追加する際には、現在の Redo スタックを破棄しています。Undo/Redo の整合性を維持するために、このような実装になります。
基本的な履歴管理はできるようになりました。具体的な操作の実装と、その他の詳細な機能を詰めてゆきます。
IUndoableOperation の Execute および Undo メソッドには、当然、編集の対象となる「項目」や、操作のパラメーター、変更前の値、などといった情報が別途必要になります。それらは IUndoableOperation を実装するクラスにプロパティとして設けることになるでしょう。
たとえば、ContentSetBodyOperation は、Content プロパティ(編集操作の対象)、OldBody プロパティ(変更前の値)、NewBody プロパティ(変更後の値)を持ちます。
操作の Undo/Redo は、通常は一対一で対応するものです。同じ IUndoableOperation について、Execute あるいは Undo メソッドを連続して実行することは禁止します。(予期しないメソッドの呼び出しには System.InvalidOperationException を送出する。)IUndoableOperation には IsExecuted プロパティを設けます。
画面には Undo/Redo ボタンを配置します。操作履歴に何も入っていないときは、これらのボタンは無効になります。また、「次に何の操作を Undo できるのか」は、ツールチップテキストなどでユーザーに提示することが望ましいでしょう。
IUndoableOperation には「操作の名前」を示す Name プロパティを設けます。ボタンの状態を更新するときに、この値を参照します。
UndoableOperationHistory には、CanUndo および CanRedo プロパティを設けることができます。その他、次の Undo/Redo の操作を取り出すための NextUndoOperation、NextRedoOperation プロパティが必要になるでしょう。
ユーザーが画面上で行う操作は、実際には、いくつかの小さな操作の集まりであるとも見なせます。たとえば、Content オブジェクトの Body プロパティ(項目の本文)を編集する際には、同時に LastModificationTime プロパティ(最終更新日時)も更新します。
「本文を編集する」操作について、その実装は Body と LastModificationTime を一緒に扱うのでしょうか。それとも、Body の変更と LastModificationTime の変更は、それぞれクラスを分割すべきでしょうか。
クラスを分割する場合、その二つの IUndoableOperation は、画面上では一つの操作であるように見せなければなりません。Undo ボタンを押して、LastModificationTime だけしか戻らない(Body を戻すには、もう一度 Undo する必要がある)というのでは、ちょっと困ります。
複数の子操作をまとめて扱う仕組みとして、MacroOperation オブジェクトを考えます。それ自体も IUndoableOperation の実装の一つで、内部に IUndoableOperation の集合(子操作の集合)を持ちます。Append メソッドで追加された子操作を、Execute メソッドでは昇順に、Undo メソッドでは降順に、それぞれ実行します。この例では、SetBodyOperation と SetLastModificationTimeOperation を MacroOperation に包んで実行、履歴に登録します。
ここで考えた IUndoableOperation 関連のクラス群は、ライブラリーにまとめています。名前空間は SunnyGrove.Entities としました。