【実務・中級編】Windows FormsにおけるUndo/Redo(元に戻す・やり直し)機能の実装:Commandパターンの適用で複雑な入力フォームの操作履歴を完璧に管理する – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

泥沼の「状態管理」から脱却せよ:Commandパターンで実装する堅牢なUndo/Redo設計

業務アプリケーションのUI開発において、多くのエンジニアが陥る罠がある。それは、「テキストボックスの値を直接書き換えて、履歴を配列で管理する」という短絡的な実装だ。

データ量が増え、UIコンポーネントが複雑化するにつれ、この手法は「状態の不整合」と「バグの温床」へと変貌する。CADや複雑な入力を伴う業務ツールで求められるのは、単なる履歴保持ではない。「実行可能なアクションそのものをオブジェクト化する」という、Commandパターンの真髄である。

本稿では、VB.NETを用いた、拡張可能かつ堅牢なUndo/Redoアーキテクチャの極意を伝授する。

—

1. なぜ「状態」ではなく「アクション」を保存するのか

多くの開発者が犯す過ちは、`List(Of String)`のような単純なコレクションに「値」を保存することだ。しかし、これでは「どのコンポーネントが」「何をどう変えたのか」という文脈(コンテキスト)が失われる。

Commandパターンでは、以下の2つを定義する。
1. ICommandインターフェース: `Execute()`(実行)と `Undo()`(差し戻し)を約束する。
2. CommandManager: スタックを使って履歴を管理し、実行の可否を制御する。

この設計であれば、テキストボックスの入力だけでなく、データベースの更新やファイル操作まで、同じインターフェースで統合管理できる。これが「プロフェッショナルな設計」と「素人のスパゲッティコード」の境界線だ。

—

2. 実装の核心:ICommand インターフェースと具体的なコマンド

まずは、すべてのアクションの基礎となるインターフェースを定義する。

”’

”’ 全てのアクションが実装すべき契約
”’

Public Interface ICommand
Sub Execute()
Sub Undo()
End Interface

次に、具体的な「テキストボックスの変更」を行うコマンドを作成する。ここでのポイントは、変更前の値(OldValue)と変更後の値(NewValue)を保持することだ。

Public Class TextBoxChangeCommand
Implements ICommand

Private ReadOnly _target As TextBox
Private ReadOnly _oldValue As String
Private ReadOnly _newValue As String

Public Sub New(target As TextBox, oldValue As String, newValue As String)
_target = target
_oldValue = oldValue
_newValue = newValue
End Sub

Public Sub Execute() Implements ICommand.Execute
_target.Text = _newValue
End Sub

Public Sub Undo() Implements ICommand.Undo
_target.Text = _oldValue
End Sub
End Class

—

3. CommandManager:履歴の統括司令塔

履歴の管理は、単なる `List` ではなく、`Stack` を使うのが定石だ。さらに重要なのは、「やり直し(Redo)」用のスタックも別に持つことである。

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 Return
Dim cmd = _undoStack.Pop()
cmd.Undo()
_redoStack.Push(cmd)
End Sub

Public Sub Redo()
If _redoStack.Count = 0 Then Return
Dim cmd = _redoStack.Pop()
cmd.Execute()
_undoStack.Push(cmd)
End Sub
End Class

—

4. プロダクション環境への適用:設計上の注意点

この設計を現場で運用する際、以下の3点に注意せよ。

  • イベントの無限ループ回避:

`Undo()` を実行すると `TextChanged` イベントが再度発生し、それが新たな「変更」として履歴に追加されるという悲劇が起きる。`Undo` 実行時はフラグを立てて、イベントハンドラ内での履歴追加処理をスキップするように設計せよ。

  • データベースとの同期:

ファイルやDBへの書き込みを伴う場合、`Execute` の中で `Transaction` を切ること。失敗した場合はロールバックし、コマンドの実行自体を履歴に残さない(あるいは失敗として処理する)必要がある。

  • メモリリークの管理:

数千の操作を保持するとメモリを圧迫する。履歴の最大数を設定し、古いものから破棄する仕組み(循環バッファ的アプローチ)を必ず組み込むこと。

—

結論:コードは「状態」から「振る舞い」へ

もしあなたが今、「ボタンを押したときに変数を書き換える」というコードを書いているなら、それはまだ初心者だ。

真の業務自動化・アプリ開発者は、「何が起きたか(What happened)」をオブジェクト化し、それを「どう巻き戻すか(How to rollback)」を設計の初期段階で定義する。このCommandパターンを習得すれば、複雑な入力フォームも、もはや畏怖の対象ではない。

設計の力で、コードをより堅牢に、そして美しく保て。それが、我々エンジニアがプロとして生き残るための唯一の道だ。

タイトルとURLをコピーしました