【プロ向け】計算エンジンをバイパスした「日付強制書き込み」の副作用と、それを補完する再計算トリガーの設計
MS Projectを用いたスケジューリング自動化ツールを開発していると、必ずと言っていいほど直面する「壁」がある。それは、「プログラムから日付を書き込んだ瞬間、プロジェクト全体の動的な連動性が死ぬ」という現象だ。
多くの開発者は、`Task.Start = MyDate` と記述すれば、そのタスクの日付が更新されると考えている。しかし、これは極めて危険な誤解だ。MS Projectの内部エンジンは、プロパティへの直接代入を「ユーザーによる制約(Constraint)の強制介入」と解釈する。
本稿では、計算エンジンをバイパスする書き込みが引き起こす副作用の正体を暴き、プロフェッショナルなツールに求められる「堅牢な再計算制御」の設計思想を伝授する。
1. 致命的な副作用:なぜ「日付代入」はスケジュールを破壊するのか
MS Projectの真髄は、プリセッサー(先行作業)とサクセッサー(後続作業)の依存関係に基づいた、ASAP(最短)やALAP(最遅)といった動的な計算エンジンにある。
制約条件(ConstraintType)の意図しない書き換え
`Task.Start` プロパティに直接値を代入した瞬間、MS Projectの内部では以下の挙動が発生する。
1. ConstraintTypeの自動変更: `pjTaskConstraintAsSoonAsPossible`(制約なし)だったタスクが、代入された日付に基づき `pjTaskConstraintStartNoEarlierThan`(開始前制約)や `pjTaskConstraintMustStartOn`(開始指定)へと、自動的に書き換えられる。
2. 依存関係の無効化: 制約が強まることで、先行作業が遅延しても、そのタスクは「固定された日付」を守ろうとする。結果として、本来なら後ろに流れるべきスケジュールが、論理的な矛盾を抱えたまま「静止」してしまう。
これが、ツール開発者が陥る「データは入っているのに、プロジェクトが動かない(連動しない)」というバグの正体だ。
2. 堅牢な設計思想:Write-Reset-Calculate パターン
プロフェッショナルなエンジニアは、単に値を書き込むことはしない。書き込み、制約をリセットし、エンジンを再起動するという「Write-Reset-Calculate」のサイクルを設計に組み込む。
設計の3原則
1. 計算モードの制御: ループ内で1件ごとに再計算を走らせるのは、パフォーマンスの観点から愚策である。計算を「手動モード」に切り替え、バッチ処理の最後に一括で計算させる。
2. 制約の明示的解除: 日付を強制的に書き込んだ後は、そのタスクの `ConstraintType` を明示的に `pjTaskConstraintAsSoonAsPossible` に戻し、エンジンの支配下に戻す。
3. 再計算トリガーの局所化: プロジェクト全体の再計算(`Project.Calculate`)を行うタイミングを、データの整合性が担保された直後に制御する。
3. 実践:プロダクションレベルの制御コード
以下に、外部データ(DBやExcel等)から取得した日付を、プロジェクトの整合性を保ったまま安全に反映させるための、保守性の高いVBA実装例を示す。
Option Explicit
''' <summary>
''' タスクの開始日を安全に更新する。
''' 単なる代入ではなく、制約条件の制御と計算エンジンの再起動をパッケージ化している。
''' </summary>
''' <param name="targetTask">更新対象のTaskオブジェクト</param>
''' <param name="newStart">設定したい新しい開始日</param>
Public Sub SafeUpdateTaskStart(ByRef targetTask As MSProject.Task, ByVal newStart As Date)
On Error GoTo ErrorHandler
' 1. 既存の制約を確認(デバッグ・ログ出力用)
' Debug.Print "Current Constraint: " & targetTask.ConstraintType
' 2. 日付の書き込み
' ここで ConstraintType が自動変更されるが、後続の処理で補完する
targetTask.Start = newStart
' 3. 制約の強制解除 (Critical Step)
' ASAP(制約なし)に戻すことで、依存関係による自動計算を再び有効化する
targetTask.ConstraintType = pjTaskConstraintAsSoonAsPossible
Exit Sub
ErrorHandler:
' プロダクション環境では、ここで独自のログ出力クラスやエラーハンドリングを呼び出す
Err.Raise Err.Number, "SafeUpdateTaskStart", "Task ID: " & targetTask.ID & " の更新に失敗しました。"
End Sub
''' <summary>
''' バッチ更新プロセスを実行する。
''' パフォーマンスを最適化するため、計算モードを制御する。
''' </summary>
Public Sub BatchUpdateSchedule(ByRef taskDataList As Collection)
Dim app As MSProject.Application
Set app = MSProject.Application
Dim t As MSProject.Task
Dim item As Variant
On Error GoTo Finalizer
' --- Phase 1: Setup ---
' 計算モードを手動に設定し、ループ中の不要な再計算(オーバーヘッド)を徹底排除する
app.Calculation = pjCalculationManual
' --- Phase 2: Batch Process ---
' taskDataList は {TaskID, NewStart} の配列を持つ Collection を想定
For Each item In taskDataList
' タスクの検索(本来はDictionary等で高速化すべき)
Set t = app.ActiveProject.Tasks(item(0))
If Not t Is Nothing Then
' 安全な更新プロシージャの呼び出し
SafeUpdateTaskStart t, item(1)
End If
Next item
' --- Phase 3: Re-calculation Trigger ---
' すべての書き込みと制約のリセットが終わった後、
' 唯一、計算エンジンを「一撃」で起動させる
app.Calculate
MsgBox "スケジュールの更新が完了しました。", vbInformation
Finalizer:
' --- Phase 4: Teardown ---
' 計算モードを自動に戻し、ユーザーの操作感を損なわないようにする
app.Calculation = pjCalculationAutomatic
If Err.Number <> 0 Then
MsgBox "エラーが発生しました: " & Err.Description, vbCritical
End If
End Sub
4. 外部システム連携における注意点:データの「冪等性」
このツールをデータベース(SQL ServerやAccess)や外部ファイルと連携させる場合、もう一つ考慮すべきは「冪等性(Idempotency)」だ。
何度も同じ更新処理を実行しても、プロジェクトの最終的なスケジュール状態が変わらないように設計しなければならない。
- DB連携の罠: DB側の日付が「予定日」なのか「実績日」なのかを明確に区別せよ。実績日(Actual Start)を `Task.Start` に書き込むと、MS Projectはそれを「確定事項」として扱うため、制約解除(Constraint Reset)を行っても、後続の計算ロジックが複雑化する。
- 同期の方向性: 「DB $\rightarrow$ MS Project」の同期であれば、上記コードの `ConstraintType` リセットは必須だ。逆に「MS Project $\rightarrow$ DB」の場合は、`Task.ConstraintType` が `pjTaskConstraintAsSoonAsPossible` であることを確認してから書き出すロジックを組まなければ、ユーザーが手動で設定した制約をDB側が「予定」として誤認してしまう。
結論
MS Project VBAにおける「日付の書き込み」は、単なるプロパティ操作ではない。それはプロジェクトの論理構造(Constraint)に対する介入行為である。
エンジニアとして、単に「値が入った」ことで満足してはならない。書き込みによって変化したオブジェクトの内部状態を正確に把握し、計算エンジンを適切なタイミングで、適切な状態(Constraint)へ引き戻す。この「制御の設計」こそが、現場でバグを生まない、プロフェッショナルな自動化ツールの境界線である。
