制御不能なスケジュールを排せ:Project VBAにおける「制約違反リアルタイム監視エンジン」の設計思想
MS Projectを用いた大規模プロジェクト管理において、ユーザーの「善意の誤操作」は、プロジェクト全体のクリティカルパスを破壊する致命的なノイズとなる。
特に、タスクの制約条件(Constraint)の変更は厄介だ。誰かが「Must Start On(開始日固定)」を安易に設定した瞬間、スケジュールの動的な再計算機能は死に、ガントチャートは単なる「絵」へと成り下がる。
多くの開発者は、この問題に対して「ボタンを押した時にチェックする」という事後確認的なアプローチを取る。だが、それはプロの設計ではない。我々が目指すべきは、ユーザーの操作をリアルタイムでインターセプトし、組織の標準ルールに反する変更を即座に無効化する「ガードレール」の実装である。
本稿では、`Project.TaskConstraintChange` イベントを使い、再入可能性(Re-entrancy)とパフォーマンスの罠を回避しながら、堅牢な監視エンジンを構築するための極限の知見を伝授する。
1. 陥りやすい罠:なぜ単純なイベントハンドリングでは「破綻」するのか
初心者が書くコードは、常に一つの致命的な欠陥を抱えている。それは「イベントの無限ループ(再入可能性によるスタックオーバーフロー)」だ。
負の連鎖のメカニズム
1. ユーザーがタスクの制約を「Must Start On」に変更する。
2. `TaskConstraintChange` イベントが発生する。
3. イベントハンドラ内で「ルール違反だ」と判断し、プログラムが制約を「As Soon As Possible」に書き戻す。
4. この「書き戻し操作」自体が、再び `TaskConstraintChange` イベントを発生させる。
5. ステップ2に戻り、無限ループが発生。MS Projectはフリーズし、最悪の場合はクラッシュする。
さらに、大規模なプロジェクト(数千タスク規模)では、イベントの発生頻度が極めて高いため、監視ロジックが重いとユーザー体験(UX)は著しく損なわれる。
2. アーキテクチャ設計:ガードレール・パターンの採用
堅牢な監視エンジンを作るには、以下の3つの設計原則を遵守しなければならない。
1. Re-entrancy Guard (再入防止フラグ): 現在の処理が「検知」なのか「修正(ロールバック)」なのかを厳密に区別する。
2. Separation of Concerns (責務の分離): 「制約の判定ロジック」と「イベントの制御ロジック」を分離し、保守性を高める。
3. Fail-safe Error Handling: エラーが発生しても、必ず「再入防止フラグ」を解除してシステムを正常状態に戻す。
3. プロダクション実装:ConstraintGuard クラス
以下に、実務レベルでそのまま利用可能な、クラスモジュールによる実装例を示す。標準モジュールに直接書くのではなく、必ずクラスモジュールとしてカプセル化せよ。それがオブジェクトのライフサイクルを制御する唯一の道だ。
クラスモジュール: `clsConstraintGuard`
' ===========================================================================
' Class Name: clsConstraintGuard
' Purpose: MS Projectのタスク制約変更をリアルタイムで監視し、
' 組織ルールに反する設定を自動的にロールバックする。
' ===========================================================================
Option Explicit
Private WithEvents App As MSProject.Application
Private m_isProcessing As Boolean ' 再入防止用フラグ
' 初期化メソッド
' @param targetApp: 監視対象のMS Project Applicationオブジェクト
Public Sub Initialize(ByRef targetApp As MSProject.Application)
Set App = targetApp
m_isProcessing = False
End Sub
' 解除メソッド
Public Sub Terminate()
Set App = Nothing
m_isProcessing = False
End Sub
' ---------------------------------------------------------------------------
' TaskConstraintChange イベントハンドラ
' ---------------------------------------------------------------------------
Private Sub App_TaskConstraintChange(ByVal Task As MSProject.Task)
' 1. 再入防止チェック: プログラム自身による変更操作中の場合は即座に抜ける
If m_isProcessing Then Exit Sub
On Error GoTo ErrorHandler
' 2. 状態のロック
m_isProcessing = True
' 3. ビジネスルールに基づいたバリデーション
' ここでは例として「Must Start On (2)」や「Must Finish On (3)」を禁止する
' 組織の標準ルールに合わせてここを拡張すること
If IsConstraintViolated(Task) Then
Debug.Print "Violation Detected: Task '" & Task.Name & "' changed to invalid constraint: " & Task.Constraint
' 4. ロールバック実行
' ASAP (0) または ALAP (1) などの適切な制約へ強制変更
RollbackConstraint Task
' ユーザーへの通知(必要に応じて)
' MsgBox "制約条件の変更は組織ルールにより禁止されています。", vbCritical
End If
CleanUp:
' 5. 状態の解除(必ず実行される必要がある)
m_isProcessing = False
Exit Sub
ErrorHandler:
' エラー発生時もフラグを戻さないと、二度とイベントが動かなくなる
MsgBox "監視エンジンでエラーが発生しました: " & Err.Description, vbCritical
Resume CleanUp
End Sub
' ---------------------------------------------------------------------------
' Private Function: IsConstraintViolated
' 判定ロジックの分離(保守性の向上)
' ---------------------------------------------------------------------------
Private Function IsConstraintViolated(ByVal t As MSProject.Task) As Boolean
' MSProject.TaskConstraint の定数を利用
' 2: Must Start On, 3: Must Finish On, 4: Start No Earlier Than 等を禁止対象とする例
Select Case t.Constraint
Case pjTaskMustStartOn, pjTaskMustFinishOn
IsConstraintViolated = True
Case Else
IsConstraintViolated = False
End Select
End Function
' ---------------------------------------------------------------------------
' Private Sub: RollbackConstraint
' ロールバック処理の集約
' ---------------------------------------------------------------------------
Private Sub RollbackConstraint(ByVal t As MSProject.Task)
' ASAP (pjTaskAsSoonAsPossible) に強制的に戻す
' ここでプロパティを変更すると、再度 App_TaskConstraintChange が呼ばれるが、
' m_isProcessing が True なので無限ループは回避される。
t.Constraint = pjTaskAsSoonAsPossible
End Sub
標準モジュール: `modMain`
' ===========================================================================
' Module: modMain
' Purpose: 監視エンジンの起動とライフサイクル管理
' ===========================================================================
Option Explicit
Private m_guard As clsConstraintGuard
' エンジンの起動
Public Sub StartMonitoring()
Set m_guard = New clsConstraintGuard
m_guard.Initialize Application
Debug.Print "Constraint Guard Engine Started."
End Sub
' エンジンの停止
Public Sub StopMonitoring()
If Not m_guard Is Nothing Then
m_guard.Terminate
Set m_guard = Nothing
Debug.Print "Constraint Guard Engine Stopped."
End If
End Sub
4. プロフェッショナルのための高度な考慮事項
この実装をさらに「製品レベル」に引き上げるには、以下の3点に留意せよ。
① データベース/外部連携の注意点
もし「禁止されている制約リスト」を外部のSQL ServerやExcelファイルから取得する場合、イベントハンドラの中で直接I/O(ファイル読み書き)を行ってはならない。
イベント内でのネットワーク待機やディスクI/Oは、MS ProjectのUIスレッドを長時間ブロックし、ユーザーに「フリーズした」という印象を与える。
- 対策: 起動時にキャッシュ(Dictionaryオブジェクト等)へルールをロードしておくか、非同期的に更新する設計にせよ。
② パフォーマンスの重み
`TaskConstraintChange` は、ユーザーがドラッグ&ドロップで日付を動かしている最中にも、連続して発生する可能性がある。
- 対策: 判定ロジック(`IsConstraintViolated`)は極限まで軽量化すること。複雑な条件分岐や、タスクの親子関係を遡るような再帰的な計算をイベント内で行うのは厳禁だ。
③ ログの永続化
「誰が、いつ、どのタスクの制約を違反させたか」という情報は、プロジェクト管理のガバナンスにおいて極めて重要である。
- 対策: ロールバックが発生した際、単に画面にメッセージを出すだけでなく、エラーログとしてテキストファイルやデータベースへ記録する仕組みを組み込め。ただし、これも前述の通り、I/Oのタイミングには細心の注意を払え。
結論
Project VBAにおけるイベント制御は、単なる「きっかけ」の作成ではない。それは、アプリケーションの挙動を規定する「憲法」を実装する作業である。
再入防止フラグによる制御、責務の分離、そしてエラー発生時における状態の整合性維持。これらを完璧にこなして初めて、あなたのツールは「便利なマクロ」から「信頼に足る業務基盤」へと昇華する。
設計に妥協は許されない。コードの背後にあるオブジェクトの動きを常に意識せよ。
