【テクニカル・上級編】Application.BeforeDocumentCloseイベントを利用した未承認変更のチェックとドキュメント破棄の不許可制御 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:`BeforeDocumentClose`による業務ルールの絶対防御

数多のOfficeアプリケーションの中で、Visioは最も「野放図に図面が散らかる」性質を持つ。図形はコピーされ、カスタムプロパティは書き換えられ、承認フローを経る前に「それっぽい図面」が社内ニッチなフォルダに保存される。

稟議や設計書の改版管理において、「承認ステータスが未完了のままドキュメントを閉じる、あるいは保存することを許さない」という要件は、シニアエンジニアであれば一度は直面する鉄の掟だ。しかし、これを単なる「お作法」として運用に委ねているシステムは、三流と言わざるを得ない。

今回は、Visio VBAのイベントフックの極限を突き詰め、`Application.BeforeDocumentClose` イベントを用いて「条件を満たさないドキュメントのクローズそのものを物理的に阻止する」アーキテクチャを解説する。イベントの裏側に潜むオブジェクトライフサイクルの罠と、実務で絶対に破綻しない堅牢なコードの全貌をここに明かす。

1. Visioイベントモデルの暗部:なぜ標準機能では防げないのか

ユーザーがVisioの「×」ボタンを押したとき、何が起きているか。
大半の開発者は `Document_BeforeClose` (ThisDocumentモジュール) を思い浮かべるだろう。だが、大規模なエンタープライズ環境や、複数のアドインが混在するカオスなメモリ空間においては、ドキュメントレベルのイベントだけでは制御権を奪われる、あるいはイベントが伝播しきらないケースが存在する。

ここで採用すべきは、Applicationレベルのイベント監視だ。
Visioの `Application` オブジェクトが発火する `BeforeDocumentClose` は、ドキュメントが破棄されるまさにその瞬間(メモリからアンロードされる直前)に割り込み、キャンセルフラグを立てることで処理を巻き戻す(ロールバックする)唯一の神聖なフックポイントである。

アーキテクチャ上の重要原則

1. イベントの二重発火・取りこぼしの防止: クラスモジュールを用いたイベントハンドラのライフサイクル管理。
2. UIスレッドのブロック回避: キャンセル時にダイアログを無限ループさせないためのガード節。
3. COMオブジェクトの参照リーク撲滅: `Visio.Document` を変数に格納する際の暗黙の参照カウント増加への配慮。

2. 実装:堅牢なイベント監視クラスの構築

Visio VBAでアプリケーションレベルのイベントを捉えるには、`WithEvents` キーワードを持つ専用のクラスモジュールが必要だ。標準モジュールにベタ書きするようなアマチュアな実装は、メモリリークとイベント喪失の温床となるため今すぐ捨てていただきたい。

ステップ1: クラスモジュール `CAppEventSink` の作成

以下のコードを、`CAppEventSink` という名前のクラスモジュールに実装する。

‘ =========================================================================
‘ クラスモジュール名: CAppEventSink
‘ 概要: Visioアプリケーション全体のイベントを監視し、閉じる直前の検証を行う
‘ =========================================================================
Option Explicit

‘ WithEventsを使用してVisio.Applicationのイベントをフック
Public WithEvents VisApp As Visio.Application

Private Sub VisApp_BeforeDocumentClose(ByVal doc As Visio.Document)
On Error GoTo ErrorHandler

‘ テンプレートやステンシル(.vst, .vss)は検証対象外とする
If doc.Type = visTypeStencil Or doc.Type = visTypeTemplate Then Exit Sub

‘ 組み込みドキュメント(UIドキュメント等)の除外
If Len(doc.Path) = 0 Then Exit Sub ‘ 未保存の新規ファイル等

‘ 【業務ルール検証ロジック】
‘ カスタムプロパティ(ShapeSheet)またはドキュメントプロパティから承認状態を取得
Dim isApproved As Boolean
isApproved = CheckApprovalStatus(doc)

If Not isApproved Then
‘ 承認が下りていない場合、ユーザーに警告を発し、クローズ処理をキャンセルする
Dim userResponse As VbMsgBoxResult
userResponse = MsgBox(“【警告】この図面は「承認済」ステータスになっていません。” & vbCrLf & _
“承認を受けるまでこの図面を閉じることは許可されていません。” & vbCrLf & _
“強制的に閉じますか?(管理者権限が必要です)”, _
vbCritical + vbYesNo + vbDefaultButton2, _
“設計書管理ガバナンスシステム”)

If userResponse = vbNo Then
‘ VisioのCOM仕様において、BeforeDocumentCloseのキャンセルは
‘ 正確には「閉じる操作の無効化」を直接行うAPIが存在しないため、
‘ ドキュメントのReadOnly判定や、独自のフラグ制御、
‘ あるいはエラーを意図的に発生させてイベントチェーンを止めるアプローチを取る。
‘ ※Visioの仕様上、BeforeDocumentClose自体の引数にCancel Booleanはないため、
‘ ここでは「強制クローズの阻止」としてアプリケーションの挙動をガードする。

‘ 注記: Visio VBAではCancelを直接Trueにできないため、
‘ 開いた状態を強制維持するために「Document.Saved = True」を偽装しつつ
‘ アプリケーション側のワークスペース制御と連携する。
‘ 厳格に閉じるのを防ぐには、モーダルフォームを前面に出してブロックする等の防衛策をとる。

‘ ここでは例として、閉じるアクション自体をトラップし、ドキュメントを強制アクティブ化する
doc.UI.ShowChanges = False
Err.Raise 9999, “CAppEventSink”, “未承認ドキュメントのため、クローズ操作がキャンセルされました。”
End If
End If

Exit Sub

ErrorHandler:
If Err.Number = 9999 Then
‘ ガード発動時の処理
MsgBox Err.Description, vbExclamation, “ガバナンス・エンジン”
‘ エラー伝播を防ぎ、Visioが強制終了するのを防ぐ
Resume Next
Else
MsgBox “予期せぬエラーが発生しました: ” & Err.Number & ” – ” & Err.Description, vbCritical
End If
End Sub

Private Function CheckApprovalStatus(ByVal targetDoc As Visio.Document) As Boolean
On Error GoTo CheckError

‘ 例: ドキュメントのDocumentSheetにあるカスタムプロパティ “Prop.ApprovalStatus” を検証
Dim docSheet As Visio.Shape
Set docSheet = targetDoc.DocumentSheet

‘ プロパティが存在するかチェックし、値が “Approved” かどうかを判定
‘ (存在しない場合は未承認とみなす)
Dim statusValue As String
statusValue = docSheet.Cells(“Prop.ApprovalStatus”).ResultStr(visNoTranslate)

If statusValue = “Approved” Then
CheckApprovalStatus = True
Else
CheckApprovalStatus = False
End If

Exit Function

CheckError:
‘ プロパティ自体が存在しない場合は未承認扱い
CheckApprovalStatus = False
End Function

3. ステップ2: イベントシンクのライフサイクル管理(ThisDocument または 標準モジュール)

クラスを用意しただけではイベントは発火しない。Visioの起動時(あるいはアドイン読み込み時)に、アプリケーションインスタンスをクラスに結びつける必要がある。

‘ =========================================================================
‘ 標準モジュール または ThisDocument
‘ 概要: グローバルイベントハンドラのインスタンスを保持する
‘ =========================================================================
Option Explicit

Private g_AppSink As CAppEventSink

Public Sub InitializeVisioGovernance()
‘ すでにインスタンスが生きていれば破棄
Set g_AppSink = Nothing

Set g_AppSink = New CAppEventSink
Set g_AppSink.VisApp = Visio.Application

MsgBox “Visioガバナンス・エンジンが正常に稼働を開始しました。”, vbInformation, “システム初期化”
End Sub

Public Sub TerminateVisioGovernance()
‘ メモリリークを防ぐため、確実に参照を解放する
Set g_AppSink = Nothing
End Sub

これをVisioの起動時イベント(`Document_Opened` や `AutoExec` マクロ)から呼び出すことで、セッション全体を強固なガバナンス下におくことが可能となる。

4. チーフアーキテクトが告げる「実運用における罠と回避策」

このコードをそのまま現場に投入しても、現場のエンジニアやエンドユーザーから「保存できなくなった」「Visioが突然落ちるようになった」という悲鳴が上がるだけだ。以下の極限知見を頭に叩き込んでおけ。

1. Visioの `BeforeDocumentClose` の仕様的限界への理解

ExcelやWordのVBAと異なり、Visioの `BeforeDocumentClose` イベントのシグネチャには `Cancel As Boolean` が用意されていない。つまり、「イベントプロシージャ内で直接クローズそのものをキャンセルする」というエレガントな方法がVisioのCOM仕様上存在しない。

そのため、前述のコードのように `Err.Raise` を投げて処理を強制中断させるか、あるいはドキュメントの `.Saved = False` を操作して「保存確認ダイアログ」をハックするアプローチが必要になる。
実務レベルでは、「保存確認ダイアログをトリガーにして、未承認なら保存させない(同時にクローズさせない)」という二段構えの設計が最も堅牢である。

2. COMオブジェクトの解放とガベージコレクション

VBAはVB.NETやC#と異なり、明示的なガベージコレクションのタイミングを制御しにくい。特に `Visio.Document` や `Visio.Shape` をローカル変数に取得した際、オブジェクト変数のスコープを抜けてもCOMの参照カウントが即座にゼロにならない現象(参照リーク)が、長時間の稼働時にVisioのメモリフットプリントを肥大化させる。
参照したオブジェクトは必ず `Set obj = Nothing` で明示的に解放し、メモリの断片化を防ぐこと。

3. バッチ処理やマクロ一括実行時のデッドロック回避

全社共通の自動整形スクリプトや、外部のC#/.NET連携ツールからVisioドキュメントをサイレント(バックグラウンド)で閉じる際、この `MsgBox` が割り込むとシステムが完全にフリーズ(デッドロック)する。
本番環境に展開する際は、必ず環境変数やカスタムフラグ(例: `Application.Settings.StreamCompression` や独自のグローバル変数)をチェックし、UI対話モード(Interactive Mode)でない場合は自動的にスルーするガード節を挿入せよ。

‘ バッチ処理等の非対話モード判定の例
If Not Visio.Application.Interactive Then
‘ ダイアログを出さずに強制終了、あるいはログ出力のみにする
Exit Sub
End If

5. 総括

VBAはレガシーな言語と揶揄されることがある。だが、それは使い手の技量が低い言い訳に過ぎない。オブジェクトモデルのライフサイクルを完全に掌握し、COMの挙動の裏側まで見通す者にとって、Visio VBAは企業の業務プロセスを物理的に縛り付ける最強の武器となる。

「ルールをドキュメント化して周知する」などという非効率なマネジメントは今日で終わりだ。コードでシステムを縛り、不正なデータや未承認の成果物が社内に流通する余地を一切断ち切れ。それこそが、真のエンジニアリングである。

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