Windows FormsにおけるUndo/Redoの極致:Commandパターンによる状態管理の深淵
多くのエンジニアが、`TextBox`の`Undo`プロパティに甘んじている間、我々のようなアーキテクトは「アプリケーションの状態そのもの」を制御下に置く必要がある。CADのような精密な操作が求められる業務システムにおいて、単なるテキストボックスの履歴機能など、おもちゃに過ぎない。
今日は、Windows FormsにおけるUndo/Redoを、Commandパターンを用いて如何にして「堅牢」かつ「メモリ効率良く」実装するか、その知見を共有する。
—
1. なぜ「Commandパターン」なのか
状態のロールバックを実装する際、多くの初心者が陥る罠がある。それは「状態のスナップショット(全保持)」だ。フォームの全プロパティをシリアライズしてスタックに積むような実装は、メモリを食いつぶし、ガベージコレクション(GC)の暴走を招く。
Commandパターンは、「操作そのもの」をオブジェクトとしてカプセル化する。
- Execute: アクションを実行する。
- Unexecute: アクションを逆転させる(これがUndoの正体だ)。
このアプローチにより、メモリ消費は「実行された操作の数」に比例するのみとなる。
—
2. 実装の設計:Commandインターフェースの定義
まずは、すべての操作を統括するインターフェースを定義する。
Public Interface ICommand
Sub Execute()
Sub Unexecute()
End Interface
次に、このコマンドを管理する`CommandManager`だ。ここは`Stack(Of ICommand)`を用いて、最新の操作を常に追跡する。
Public Class CommandManager
Private undoStack As New Stack(Of ICommand)()
Private redoStack As New Stack(Of ICommand)()
Public Sub ExecuteCommand(cmd As ICommand)
cmd.Execute()
undoStack.Push(cmd)
redoStack.Clear() ‘ 新しい操作が発生した時点で、Redoの歴史は断絶する
End Sub
Public Sub Undo()
If undoStack.Count = 0 Then Exit Sub
Dim cmd = undoStack.Pop()
cmd.Unexecute()
redoStack.Push(cmd)
End Sub
Public Sub Redo()
If redoStack.Count = 0 Then Exit Sub
Dim cmd = redoStack.Pop()
cmd.Execute()
undoStack.Push(cmd)
End Sub
End Class
—
3. シニアエンジニアが意識すべき「メモリの汚染」と「Windows API」
ここで注意すべきは、`Unexecute`時の処理だ。特にWinFormsで複雑なUIコンポーネントを操作する場合、イベントが連鎖的に発火し、意図せぬリフレッシュが発生して画面がチラつくことがある。
これを防ぐには、`Win32 API`の`SendMessage`を使用して、再描画を一時停止させるのが定石だ。
‘ 描画の凍結・解凍
Public Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function
Private Const WM_SETREDRAW As Integer = &HB
Public Sub SuspendPainting(target As Control)
SendMessage(target.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
End Sub
Public Sub ResumePainting(target As Control)
SendMessage(target.Handle, WM_SETREDRAW, New IntPtr(1), IntPtr.Zero)
target.Refresh()
End Sub
コマンド実行中にこのAPIを挟むことで、バックグラウンドでの複雑なプロパティ変更を、ユーザーの目には「一瞬の変更」として映すことができる。
—
4. 複雑な入力項目に対応する具象コマンドの例
例えば、「数値入力フィールドの値を変更する」操作を考えてみる。
Public Class ChangeValueCommand
Implements ICommand
Private _control As TextBox
Private _oldValue As String
Private _newValue As String
Public Sub New(ctrl As TextBox, newValue As String)
_control = ctrl
_newValue = newValue
_oldValue = ctrl.Text
End Sub
Public Sub Execute() Implements ICommand.Execute
_control.Text = _newValue
End Sub
Public Sub Unexecute() Implements ICommand.Unexecute
_control.Text = _oldValue
End Sub
End Class
このように設計すれば、フォーム側は `_commandManager.ExecuteCommand(New ChangeValueCommand(txtPrice, “1000”))` を呼ぶだけで済む。UIのロジックと、ビジネスデータの整合性管理を完全に分離できるのだ。
—
5. 伝説的アーキテクトからの助言:リークを防げ
最後に、忘れられがちな「オブジェクトのライフサイクル」について。
VB.NETのガベージコレクタは優秀だが、`Command`オブジェクトが長い間`Stack`に留まり続けると、それが参照しているコントロール(`TextBox`等)のメモリ解放が阻害される可能性がある。
もしUndoの履歴が数千件に及ぶようなシステムを作るなら、履歴スタックに「保持するコマンドの最大数」を設け、`IDisposable`を実装して明示的に参照を切り離す設計を強く推奨する。
‘ 履歴が溢れたら古いコマンドを破棄する工夫
If undoStack.Count > 100 Then
‘ ここで古いオブジェクトの参照を null にし、明示的にGCへ解放を促す
‘ 巨大なデータ保持型コマンドの場合は特に重要だ
End If
結びに代えて
Undo/Redoの実装は、単なる機能追加ではない。それは「アプリケーションの時間を操る」という設計上の特権的権利だ。
安易な設計に逃げず、Commandパターンという洗練された武器を使いこなせ。そうすれば、レガシーなWinFormsのコードベースであっても、CADソフトに匹敵する極めて堅牢な操作体験を構築できるはずだ。
技術は常に、詳細な実装への執着の中にのみ、その真価を宿す。健闘を祈る。
