【実務・中級編】【中級】フォームの「Dirty」プロパティを監視して、未保存データの破棄を制御する – Access VBA解析バイブル

スポンサーリンク

「せっかく入力したデータが消えてしまった」「中途半端に書き換えたデータが、いつの間にか勝手に保存されていた」

Accessで業務システムを開発したことがあるなら、エンドユーザーから一度はこの洗礼(クレーム)を受けたことがあるはずだ。

Excelや一般的なWebアプリケーションに慣れたユーザーにとって、「他のレコードをクリックしただけで、確認もなしに自動保存される」というAccessのデフォルト挙動は、直感的とは言えず、しばしばデータの破壊や誤入力を招く要因となる。

この問題を根本から解決し、堅牢なデータ整合性を担保するための鍵が、フォームの `Dirty`(編集済み)プロパティの監視と、フォームライフサイクル(イベントチェーン)の完全な制御である。

今回は、単なる「動くだけのコード」に留まらず、実務の過酷な運用に耐えうる、バグの起きない「未保存データ制御アーキテクチャ」を解説する。

1. 失敗から学ぶ:なぜ「とりあえずBeforeUpdate」だけでは破綻するのか?

多くの初心者、あるいは中級レベルの開発者が陥る罠がある。
「保存の制御なら、`BeforeUpdate` イベントで `MsgBox` を出して `Cancel = True` にすればいいのだろう」という安易な設計だ。

確かに、レコードが保存される直前に `BeforeUpdate` イベントは発生する。しかし、以下のシナリオを考慮できているだろうか?

1. 無限ループの罠
ユーザーがフォームを閉じようとした際、`BeforeUpdate` で保存をキャンセル(`Cancel = True`)したにもかかわらず、未保存のデータ(Dirty状態)が画面に残ったままになる。Accessは「Dirty状態のデータをどうにかして保存するか破棄しなければフォームを閉じさせない」ため、閉じる動作を繰り返すたびにイベントが走り続け、最悪の場合はAccessがハングアップする。
2. 「変更を破棄して閉じる」が選択できない
ユーザーが「間違えて編集してしまったので、保存せずに元の状態に戻して閉じたい」と考えたとき、単に `Cancel = True` にするだけでは画面がロックされるだけで、明示的に変更を「ロールバック(Undo)」する処理が抜けていると、ユーザーは「閉じることもできない」迷宮に迷い込む。
3. サブフォーム移動時の予期せぬ保存
メインフォームからサブフォームへフォーカスが移る瞬間、Accessは自動的にメインフォームのレコードを保存する。このライフサイクルを意識せずに設計すると、意図しないタイミングで確認ダイアログが連発し、ユーザー体験(UX)を著しく損なう。

優れたアーキテクトは、「データの書き込みを防ぐ(Cancel)」ことと、「データを元の状態に戻す(Undo)」こと、そして「フォームの終了処理(Unload)」の関係性を、オブジェクトのライフサイクルに基づいてロジカルに整理する。

2. 完璧な状態遷移図:Dirtyとイベントのライフサイクル

設計をシンプルにするため、フォームの状態を以下のように定義し、それぞれの遷移を制御する。

  • Clean(未変更)状態: `Me.Dirty = False`
  • データの整合性は保たれている。そのままフォームを閉じても、レコードを移動しても問題ない。
  • Dirty(編集中)状態: `Me.Dirty = True`
  • ユーザーが1文字でも入力した瞬間にこの状態へ遷移する。
  • この状態から離脱するルートは「保存してCleanに戻す(Save)」か、「変更を破棄してCleanに戻す(Undo)」の2ルートのみ。

この状態遷移を安全にハンドリングするためには、以下の2つのイベントの役割分担を明確にしなければならない。

| イベント | 役割 | 介入の目的 |
| :— | :— | :— |
| `Form_BeforeUpdate` | レコードが保存される直前に発生する。 | 保存の「実行」または「キャンセル」を判定する最終防衛ライン。 |
| `Form_Unload` | フォームが画面から消去(アンロード)される直前に発生する。 | 画面を閉じる際、Dirty状態のデータを「保存するのか」「破棄するのか」をユーザーに最終確認する関門。 |

3. 実践:コピペで動作する極限のプロダクションコード

それでは、実務でそのまま使える堅牢な実装例を示す。
この設計では、コードの重複を防ぐ(DRY原則)ため、変更監視と制御のコアロジックを汎用的な関数として実装し、各フォームからは最小限のイベントハンドラーで呼び出す構造をとっている。

3.1 【標準モジュール】 `modFormController`

まずは、システム全体で使い回せる共通制御ロジックを標準モジュールに定義する。

Option Compare Database
Option Explicit

‘ ユーザーの意思決定を表すカスタム定数
Public Enum SaveDecision
decSave = 1 ‘ 保存する
decDiscard = 2 ‘ 変更を破棄する
decCancel = 3 ‘ 編集を継続する(処理キャンセル)
End Enum

”’

”’ 未保存の変更がある場合、ユーザーに「保存」「破棄」「編集継続」を問い合わせる
”’

”’ 対象のフォームオブジェクト ”’ ユーザーの決定(SaveDecision)
Public Function ConfirmDataDiscard(ByRef frm As Access.Form) As SaveDecision
On Error GoTo Error_Handler

‘ そもそも編集されていなければ、何もしきない(保存を許可して終了)
If Not frm.Dirty Then
ConfirmDataDiscard = decSave
Exit Function
End If

Dim msg As String
Dim ans As VbMsgBoxResult

msg = “現在のレコードは編集されています。” & vbCrLf & _
“変更を保存しますか?” & vbCrLf & vbCrLf & _
“【はい】 : 変更を保存して処理を進めます” & vbCrLf & _
“【いいえ】 : 変更を破棄して元の状態に戻します” & vbCrLf & _
“【キャンセル】 : 編集画面に戻ります”

‘ [はい][いいえ][キャンセル]の3択を提示
ans = MsgBox(msg, vbYesNoCancel + vbQuestion + vbDefaultButton1, “未保存データの確認”)

Select Case ans
Case vbYes
ConfirmDataDiscard = decSave
Case vbNo
ConfirmDataDiscard = decDiscard
Case vbCancel
ConfirmDataDiscard = decCancel
End Select

Exit Function

Error_Handler:
MsgBox “エラーが発生しました (” & Err.Number & “): ” & Err.Description, vbCritical, “エラー”
ConfirmDataDiscard = decCancel
End Function

3.2 【フォームのコードモジュール】

次に、データを入力するメインフォーム(例:`frmEmployeeEdit`)のコードモジュールに以下を記述する。

Option Compare Database
Option Explicit

‘ フォーム側で「プログラムによって明示的に保存を実行したか」を追跡するフラグ
Private m_IsExplicitSave As Boolean

Private Sub Form_Load()
‘ 初期化
m_IsExplicitSave = False
End Sub

”’

”’ レコード保存直前のイベント。
”’ ここがデータ不整合を防ぐ「最後の砦」となる。
”’

Private Sub Form_BeforeUpdate(Cancel As Integer)
‘ プログラムから明示的な保存(保存ボタン押下など)が行われた場合は、
‘ 二重の確認ダイアログを防ぐため、ここでの割り込み処理をスルーする。
If m_IsExplicitSave Then
m_IsExplicitSave = False
Exit Sub
End If

‘ ユーザーの意思を確認
Select Case ConfirmDataDiscard(Me)
Case decSave
‘ 保存を許可する(何もしない。Accessが自動で保存を完了させる)

Case decDiscard
‘ 変更を破棄する。
‘ Cancel = True だけではDirty状態が維持されてしまうため、
‘ Me.Undoを呼び出してコントロールの値をテーブル読込時の状態に戻す。
Cancel = True
Me.Undo

Case decCancel
‘ 編集継続。保存処理そのものをキャンセルし、画面をそのまま残す。
Cancel = True
End Select
End Sub

”’

”’ フォームが閉じられる直前のイベント。
”’ 画面右上の「×」ボタンや、DoCmd.Closeが呼ばれた際の制御。
”’

Private Sub Form_Unload(Cancel As Integer)
‘ フォームを閉じる時点でDirty状態の場合、
‘ すでに BeforeUpdate が裏で走るが、安全のためにここでもDirtyチェックを行う。
If Me.Dirty Then
Select Case ConfirmDataDiscard(Me)
Case decSave
‘ 保存を実行する。これによりBeforeUpdateが走るが、
‘ 重複確認を防ぐために明示的保存フラグを立てる。
m_IsExplicitSave = True
On Error Resume Next
DoCmd.RunCommand acCmdSaveRecord
If Err.Number <> 0 Then
‘ 入力規則違反などで保存できなかった場合、Unloadをキャンセルする
MsgBox “保存に失敗したため、フォームを閉じることができません。” & vbCrLf & _
“入力内容を確認してください。”, vbExclamation, “保存エラー”
Cancel = True
m_IsExplicitSave = False
End If
On Error GoTo 0

Case decDiscard
‘ 変更を破棄して閉じることを許可する。
‘ 先にUndoを実行してCleanな状態にすることで、
‘ Accessが「未保存データがある」と警告するのを防ぎ、スムーズに閉じる。
Me.Undo

Case decCancel
‘ 閉じる動作そのものをキャンセルする。
Cancel = True
End Select
End If
End Sub

”’

”’ [保存]ボタンをクリックした際のカスタム処理
”’

Private Sub btnSave_Click()
On Error GoTo Error_Handler

If Not Me.Dirty Then
MsgBox “変更されたデータはありません。”, vbInformation, “情報”
Exit Sub
End If

‘ ユーザーが明示的に保存ボタンを押したため、
‘ BeforeUpdateでの冗長な問い合わせをスキップさせる。
m_IsExplicitSave = True

‘ 明示的にレコードを保存
DoCmd.RunCommand acCmdSaveRecord
MsgBox “データを保存しました。”, vbInformation, “完了”

Exit Sub

Error_Handler:
m_IsExplicitSave = False
‘ 主キー重複や必須入力エラーなどの場合はここに流れる
MsgBox “保存できませんでした。入力内容を確認してください。” & vbCrLf & _
“エラー詳細: ” & Err.Description, vbCritical, “保存エラー”
End Sub

”’

”’ [キャンセル]ボタンをクリックした際のカスタム処理
”’

Private Sub btnCancel_Click()
‘ 単にMe.Undoを呼ぶだけでは、すでに保存されてしまった過去の履歴までは戻せないが、
‘ 現在の編集中レコードの変更は一瞬で破棄される。
If Me.Dirty Then
If MsgBox(“現在の編集内容をすべて破棄しますか?”, vbYesNo + vbQuestion, “確認”) = vbYes Then
Me.Undo
End If
Else
MsgBox “変更されたデータはありません。”, vbInformation, “情報”
End If
End Sub

4. プロの設計解説:なぜこのコードは「バグらない」のか?

このコードが堅牢である理由は、Accessのイベントモデルの挙動を徹底的に考慮し、「逃げ道」をすべて塞いでいる点にある。

ポイント①:`Cancel = True` と `Me.Undo` の精密な使い分け

初心者は `Cancel = True` だけで処理を止めようとする。しかし、Accessは「レコードが変更された(Dirtyである)」という事実を覚えたままだ。そのため、フォームを閉じようとした際に再度 `BeforeUpdate` が走り、無限ループを誘発する。
上記のコードでは、ユーザーが「破棄(いいえ)」を選択した際、`Cancel = True` で保存を阻止した直後に、必ず `Me.Undo` を実行して変更されたバッファを強制クリアしている。これにより、フォームは一瞬で「Clean(未変更)」状態に戻り、Accessは何の文句も言わずにフォームを閉じさせてくれるのだ。

ポイント②:明示的保存フラグ `m_IsExplicitSave` によるUXの最適化

「保存」ボタンをクリックしたユーザーに対して、「保存しますか?」とダイアログを出し、その直後の `BeforeUpdate` でも再度同じダイアログが出たら、システムとしての美しさは皆無だ。
モジュールレベル変数 `m_IsExplicitSave` をフラグとして用意し、ボタン経由の保存である場合は `BeforeUpdate` 内の問い合わせをインテリジェントにスルーさせている。これで、ユーザーを煩わせる「二重確認」を完全に排除している。

ポイント③:`Unload` イベントでの最終防衛

ユーザーが画面右上の「×」ボタンを押した、あるいは他のメニューから `DoCmd.Close` が実行されたとき、Accessはフォームを閉じようとする。このタイミングでもしDirty状態であれば、`Form_Unload` が割り込み、`BeforeUpdate` と同様の整合性チェックを行う。
ここで「キャンセル」が選ばれれば `Cancel = True` によってフォームのクローズ処理そのものを完全に無効化する。

5. 現場のリーダーからあなたへ

Access VBAを用いた開発において、フレームワーク(RailsやASP.NETなど)のような「レール」は用意されていない。自らオブジェクトのライフサイクルを理解し、イベントの発生順序をコントロールしなければ、簡単にスパゲッティコードと化し、データ破損バグの温床となる。

今回紹介した 「Dirty制御パターン」 は、Access開発における必須の「デザインパターン」と言っても過言ではない。

ただコピペして動かすだけでなく、

  • なぜこのイベントがこの順番で走るのか
  • なぜ `Undo` を挟まなければAccessが怒るのか

をロジカルに理解すれば、あなたの作成するAccessツールは、他の開発者が作った「おもちゃ」のようなツールとは一線を画す、真にプロフェッショナルな業務システムへと進化するはずだ。ぜひ、自身のプロジェクトに組み込み、その堅牢性を実感してほしい。

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