首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 單對象狀態模式實現 Undo/Redo:原理、建模步驟與 WPF 繪圖案例

     編輯:關於C#

Undo/Redo 核心思想

應用程式每次操作後狀態發生變化。要支援撤銷,必須能回到前一狀態;要支援重做,必須能前進到下一狀態。因此需維護兩個堆疊:

  • Undo Stack:保存歷史狀態,Pop 回退;
  • Redo Stack:保存被撤銷的狀態,Pop 前進。

關鍵在於「如何表示狀態變化」。本文介紹 單對象表示法:用一個類 ChangeRepresentationObject 包含所有操作類型所需的屬性,執行具體操作時只填充相關子集。

通用建模六步法

  1. 識別操作與容器:決定哪些操作支援 Undo/Redo(如插入、刪除、移動、縮放),作用於哪個容器(如 Canvas)與哪些物件(如 Shape)。
  2. 識別需保存屬性:移動需保存 Margin;縮放需 Width/Height/Margin;插入/刪除需物件引用。
  3. 定義狀態對象與動作枚舉:

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; // 物件引用
}
  1. 實現 UndoRedo 管理類:雙堆疊 + InsertObjectForUndoRedo/Undo/Redo 方法。
  2. 實現具體邏輯:根據 ActionType 反向操作(刪除↔插入、移動/縮放還原 Margin/Size)。
  3. 業務代碼集成:每次操作前調用 InsertObjectForUndoRedo,工具列按鈕綁定 Undo/Redo。

WPF 繪圖案例完整實現

1. UndoRedo 核心類


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 對稱,略
    }
}

2. 業務操作中調用


// 移動物件
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 設計,違反單一職責與介面隔離原則

對比:命令模式 vs 備忘錄模式

  • 命令模式:每個操作封裝為 ICommand (Execute/Undo),符合開閉原則,新增操作只需新類,但類數量膨脹。
  • 備忘錄模式:保存物件完整快照 (GetState/SetState),通用性強,但序列化大對象效能差,適合文本編輯器等全狀態回溯。
  • 單對象法:介於兩者之間,適合「操作類型穩定、屬性有限、團隊規模小」的專案。

工程化建議

  1. 限制堆疊大小:防止記憶體洩漏,如 _undoStack.Count > 100 時移除底部舊項。
  2. 合併微操作:拖曳移動產生大量 MouseMove 事件,應在 MouseUp 時才記錄一次 Move 狀態,或用「合併命令」包裝。
  3. 異步保存:若狀態對象巨大,考慮背景線程序列化到磁碟,UI 線程只保留引用。
  4. 單元測試:為每個 ActionType 編寫 Undo/Redo 往返測試,確保狀態還原精確。

總結

單對象狀態法以「一個類承載所有變化」換取極低的實現門檻,適合原型驗證、中小型繪圖/編輯工具。隨著操作類型增長,建議遷移至命令模式或備忘錄模式,獲得更好的擴展性與關注點分離。核心要訣:「先識別操作與屬性,再設計狀態對象,最後實現雙堆疊調度」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved