應用程式每次操作後狀態發生變化。要支援撤銷,必須能回到前一狀態;要支援重做,必須能前進到下一狀態。因此需維護兩個堆疊:
關鍵在於「如何表示狀態變化」。本文介紹 單對象表示法:用一個類 ChangeRepresentationObject 包含所有操作類型所需的屬性,執行具體操作時只填充相關子集。
public enum ActionType { Delete = 0, Move = 1, Resize = 2, Insert = 3 }
public class ChangeRepresentationObject {
public ActionType Action;
public Point Margin;
public double Width;
public double Height;
public FrameworkElement UiElement; // 物件引用
}
InsertObjectForUndoRedo/Undo/Redo 方法。ActionType 反向操作(刪除↔插入、移動/縮放還原 Margin/Size)。InsertObjectForUndoRedo,工具列按鈕綁定 Undo/Redo。
public interface IUndoRedo {
void Undo(int level = 1);
void Redo(int level = 1);
void InsertObjectForUndoRedo(ChangeRepresentationObject data);
}
public class UndoRedoManager : IUndoRedo {
private readonly Stack<ChangeRepresentationObject> _undoStack = new();
private readonly Stack<ChangeRepresentationObject> _redoStack = new();
public Canvas Container { get; set; } // 目標容器
public void InsertObjectForUndoRedo(ChangeRepresentationObject data) {
_undoStack.Push(data);
_redoStack.Clear();
}
public void Undo(int level = 1) {
for (int i = 0; i < level && _undoStack.Count > 0; i++) {
var obj = _undoStack.Pop();
_redoStack.Push(obj);
ExecuteUndo(obj);
}
}
public void Redo(int level = 1) {
for (int i = 0; i < level && _redoStack.Count > 0; i++) {
var obj = _redoStack.Pop();
_undoStack.Push(obj);
ExecuteRedo(obj);
}
}
private void ExecuteUndo(ChangeRepresentationObject obj) {
switch (obj.Action) {
case ActionType.Delete:
Container.Children.Add(obj.UiElement);
break;
case ActionType.Insert:
Container.Children.Remove(obj.UiElement);
break;
case ActionType.Move:
var prevMargin = new Point(obj.UiElement.Margin.Left, obj.UiElement.Margin.Top);
// 為 Redo 保存當前狀態
_redoStack.Push(new ChangeRepresentationObject {
Action = ActionType.Move, UiElement = obj.UiElement, Margin = prevMargin
});
obj.UiElement.Margin = new Thickness(obj.Margin.X, obj.Margin.Y, 0, 0);
break;
case ActionType.Resize:
var prevMarginR = new Point(obj.UiElement.Margin.Left, obj.UiElement.Margin.Top);
_redoStack.Push(new ChangeRepresentationObject {
Action = ActionType.Resize,
UiElement = obj.UiElement,
Margin = prevMarginR,
Width = obj.UiElement.Width,
Height = obj.UiElement.Height
});
obj.UiElement.Margin = new Thickness(obj.Margin.X, obj.Margin.Y, 0, 0);
obj.UiElement.Width = obj.Width;
obj.UiElement.Height = obj.Height;
break;
}
}
private void ExecuteRedo(ChangeRepresentationObject obj) {
// 邏輯與 Undo 對稱,略
}
}
// 移動物件
private void OnMouseMove(object sender, MouseEventArgs e) {
if (_selectedElement == null) return;
var oldMargin = _selectedElement.Margin;
var newMargin = CalculateNewMargin(e.GetPosition(Container));
// 記錄舊狀態
_undoRedo.InsertObjectForUndoRedo(new ChangeRepresentationObject {
Action = ActionType.Move,
UiElement = _selectedElement,
Margin = oldMargin
});
_selectedElement.Margin = newMargin;
}
// 刪除物件
private void DeleteSelected() {
if (_selectedElement == null) return;
_undoRedo.InsertObjectForUndoRedo(new ChangeRepresentationObject {
Action = ActionType.Delete,
UiElement = _selectedElement
});
Container.Children.Remove(_selectedElement);
}
| 優點 | 缺點 |
|---|---|
| 實現簡單,無需設計模式知識即可上手 | 可維護性差:新增操作類型需修改枚舉、狀態類、Undo/Redo 邏輯三處 |
| 適合操作類型固定、數量少的場景 | 記憶體浪費:單一對象包含所有屬性,執行特定操作時大部分屬性閒置 |
| 狀態對象可序列化,易於持久化/傳輸 | 非純粹 OO 設計,違反單一職責與介面隔離原則 |
ICommand (Execute/Undo),符合開閉原則,新增操作只需新類,但類數量膨脹。GetState/SetState),通用性強,但序列化大對象效能差,適合文本編輯器等全狀態回溯。_undoStack.Count > 100 時移除底部舊項。單對象狀態法以「一個類承載所有變化」換取極低的實現門檻,適合原型驗證、中小型繪圖/編輯工具。隨著操作類型增長,建議遷移至命令模式或備忘錄模式,獲得更好的擴展性與關注點分離。核心要訣:「先識別操作與屬性,再設計狀態對象,最後實現雙堆疊調度」。