↩️

タスク削除して戻せない地獄 → Undo機能を後付けした話

に公開

はじめに

ある日こうなった。

タスク削除 → 間違えた → 戻せない

Ctrl+Zで戻したい。

当たり前の機能なのに、後付けするとそこそこ重い。


やりたいこと

  • Ctrl+Zで直前の操作を取り消す
  • 最大20回
  • 全操作対応

👉 「全部にUndoが効く」が前提


設計:UndoAction

何を記録するか。

public class UndoAction
{
    public string Type { get; set; }
    public string Description { get; set; }
    public TaskItem Before { get; set; }
    public long? TaskId { get; set; }
    public List<TaskItem> ClearedTasks { get; set; }
}

👉 本質はこれ

操作前の状態を持つ

Afterはいらない。DBにある。


設計:UndoService

public class UndoService
{
    private readonly Stack<UndoAction> _stack = new();
    private const int MaxUndo = 20;

    public void Push(UndoAction action)
    {
        _stack.Push(action);

        if (_stack.Count > MaxUndo)
        {
            var temp = _stack.ToArray();
            _stack.Clear();

            for (int i = 0; i < MaxUndo; i++)
            {
                _stack.Push(temp[MaxUndo - 1 - i]);
            }
        }
    }

    public UndoAction Pop()
    {
        if (_stack.Count == 0) return null;
        return _stack.Pop();
    }
}

👉 Stack一択


Cloneが命

public TaskItem Clone()
{
    return new TaskItem
    {
        Id = this.Id,
        Title = this.Title,
        Category = this.Category,
        DueDate = this.DueDate,
        IsCompleted = this.IsCompleted,
        SortOrder = this.SortOrder,
        RepeatRule = this.RepeatRule,
        Note = this.Note,
        Tag = this.Tag,
        ViewId = this.ViewId,
    };
}

👉 これがすべて

忘れると壊れる


記録(Push)

_undo.Push(new UndoAction
{
    Type = "Delete",
    Before = task.Clone(),
    TaskId = task.Id
});

👉 操作前に必ずPush


復元(Undo)

switch (action.Type)
{
    case "Add":
        _db.Delete(action.TaskId.Value);
        break;

    case "Delete":
        _db.ReInsert(action.Before);
        break;

    case "Edit":
        _db.Update(action.Before);
        break;

    case "Clear":
        foreach (var t in action.ClearedTasks)
            _db.ReInsert(t);
        break;
}

👉 逆操作をするだけ


ReInsertの注意

INSERT INTO Tasks (Id, Title, ...)
VALUES ($id, $title, ...);

👉 ID指定必須

しないと別タスクになる


Ctrl+Z

if (e.Key == Key.Z && Keyboard.Modifiers == ModifierKeys.Control)
{
    var msg = _vm.Undo();
    RenderDisplay();
}

なぜ難しいか

理由はこれ。

全操作を逆再現する必要がある

単機能じゃない。


設計の重さ

Add / Delete / Edit / Sort / Clear

全部に

事前保存 + 逆処理

が必要。

👉 後付けだと漏れる


Redoを入れなかった理由

  • コストが倍
  • 利用頻度低

👉 後から足せる


教訓

  • Cloneを忘れるな
  • Beforeだけでいい
  • Stackで管理
  • 最初から仕込め

まとめ

Undoはシンプル。

操作前を保存 → 戻す

でもカバー範囲が広い。

👉 設計の問題


ダウンロード

https://github.com/moritan777/tsk-releases/releases/latest


作者:mitsukida
お問い合わせ:mmitsuki0806@gmail.com


🔗 次の記事

https://zenn.dev/mitsukida/articles/9e44f27c268b7c

Discussion