【テクニカル・上級編】【中級者向け】タスクの「UniqueID」と「ID」の不一致を解消する:削除・挿入に強いリンク設定術 – Project VBA解析バイブル

スポンサーリンク

タスクの「UniqueID」と「ID」の不一致を解消する:削除・挿入に強いリンク設定術

Project VBAの現場において、多くの開発者が最初に直面し、そして静かに心を折られていくトラップがある。それが「ID」と「UniqueID」の乖離だ。

画面上に表示される `ID` は、いわゆる行番号に過ぎない。ユーザーがタスクを1行挿入したり、削除したり、あるいはドラッグ&ドロップで並び替えを行ったりした瞬間、その値は容赦なく書き換わる。この `ID` をキーにしてタスク間の先行・後続関係(リンク)を構築しているVBAコードは、実運用において「一度公開したら二度と修正できない動く爆弾」と化す。

今回は、動的なWBSの挿入・削除に完全耐性を持ち、大規模なシステム間連携や自動工程表生成においても微動だにしない、`UniqueID` をベースとした堅牢なリンク設定アーキテクチャを解き明かす。

—

1. なぜ「ID」依存のコードは破綻するのか

Projectのオブジェクトモデルにおいて、`Task.ID` と `Task.UniqueID` は全く異なるライフサイクルとスコープを持つ。

| 特性 | ID (Task.ID) | UniqueID (Task.UniqueID) |
| :— | :— | :— |
| 性質 | 可変(ビュー上の表示順序に依存) | 不変(作成時に一意に割り振られ、削除まで不変) |
| 再計算 | タスクの挿入・削除・移動時に自動採番される | 再計算されない。削除された番号は原則再利用されない |
| リンク指定 | `Task.TaskPredecessors.Add` 等で直感的に使えるが危険 | 確実だが、オブジェクト参照かUniqueIDを明示的に扱う必要がある |

レガシーなVBAコードでは、以下のような記述が散見される。

‘ 【アンチパターン】IDを直接指定したリンク構築
ActiveProject.Tasks(1).TaskPredecessors.Add Predecessor:=ActiveProject.Tasks(2)

このコードの何が問題か。ユーザーが「やっぱりこの工程の前にタスクを追加しよう」と行を挿入した瞬間、`Tasks(1)` としていたタスクは別の何かを指すことになり、意図しない依存関係が組まれるか、最悪の場合は実行時エラー(Run-time error ‘1100’: この引数には有効なタスクを指定してください)でマクロがクラッシュする。

シニアエンジニアたる者、「UI上のインデックスと、データ構造上の識別子は完全に分離して扱う」という原則をProject VBAでも貫かなければならない。

—

2. UniqueIDをキーにした堅牢なリンク設定アーキテクチャ

真に堅牢なタスク間リンクを構築するためには、「UniqueIDからTaskオブジェクトをO(1)で引くインデックス戦略」と、「リンク定義の抽象化」が必要となる。

Projectのコレクションから特定の `UniqueID` を持つタスクを探す際、愚直に `For Each` でループを回すのは、タスク数が数千件規模に達した瞬間にパフォーマンス上の致命傷となる。幸い、Projectの `Tasks` コレクションには `UniqueID` を直接指定して取得する高速な方法が存在しないため、大規模データを扱う場合は、あらかじめDictionaryオブジェクト等に `(UniqueID -> Task)` のマッピングをキャッシュするのが定石だ。

実装コード:堅牢な依存関係設定エンジン

以下のコードは、外部システム(CSVやDBなど)から渡された「先行タスクのUniqueID」と「後続タスクのUniqueID」のペアを元に、タスクの並び順に依存せず、確実かつ高速にリンクを構築するプロシージャである。

Option Explicit

‘ ==============================================================================
‘ 外部データ(UniqueIDベース)に基づき、タスク間の依存関係を安全に構築する
‘ ==============================================================================
Sub ApplyRobustTaskLinks()
Dim prj As Project
Set prj = ActiveProject

On Error GoTo ErrorHandler

‘ 画面描画と自動計算を停止し、オブジェクトモデルの処理速度を極限まで引き上げる
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual

‘ 1. パフォーマンス最適化のため、UniqueIDをキーにしたTaskオブジェクトの高速検索Dictionaryを構築
Dim taskDict As Object
Set taskDict = CreateObject(“Scripting.Dictionary”)

Dim t As Task
For Each t In prj.Tasks
If Not t Is Nothing Then
‘ Key: UniqueID (Stringに変換), Item: Task Object
taskDict.Add CStr(t.UniqueID), t
End If
Next t

‘ 2. リンク定義のリスト(本来は外部DBや配列、Rangeから取得する)
‘ ここでは例として、ハードコーディングされたUniqueIDのペアを使用
‘ 例: UniqueID: 101 のタスクを、UniqueID: 105 の先行タスクにする
Dim linkPairs(1 To 2, 1 To 2) As Long
linkPairs(1, 1) = 101: linkPairs(1, 2) = 105 ‘ Predecessor, Successor
linkPairs(2, 1) = 105: linkPairs(2, 2) = 108

Dim i As Long
Dim predTask As Task, succTask As Task
Dim predUID As String, succUID As String

For i = LBound(linkPairs, 1) To UBound(linkPairs, 1)
predUID = CStr(linkPairs(i, 1))
succUID = CStr(linkPairs(i, 2))

‘ Dictionaryから安全にオブジェクトを取得
If taskDict.Exists(predUID) And taskDict.Exists(succUID) Then
Set predTask = taskDict(predUID)
Set succTask = taskDict(succUID)

‘ 重複リンクエラーを回避しつつ、FS(終了-開始)リンクを設定
Call AddPredecessorSafe(succTask, predTask, pjLinkFinishToStart)
Else
Debug.Print “Warning: UniqueIDが見つかりません (Pred: ” & predUID & “, Succ: ” & succUID & “)”
End If
Next i

CleanUp:
‘ 状態を復元
Application.Calculation = pjCalculationAutomatic
Application.ScreenUpdating = True

‘ オブジェクトの明示的解放(メモリリーク防止)
Set taskDict = Nothing
Set prj = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

‘ ==============================================================================
‘ 補助関数: 既存の重複リンクをチェックしつつ安全に先行タスクを追加する
‘ ==============================================================================
Private Sub AddPredecessorSafe(ByRef targetTask As Task, ByRef predTask As Task, ByVal linkType As PjLinkType)
Dim existingLink As Dependency
Dim isAlreadyLinked As Boolean
isAlreadyLinked = False

‘ 既に同じ依存関係が存在するかチェック
For Each existingLink In targetTask.TaskPredecessors
If existingLink.From.UniqueID = predTask.UniqueID Then
isAlreadyLinked = True
Exit For
End If
Next existingLink

‘ 存在しない場合のみ追加
If Not isAlreadyLinked Then
targetTask.TaskPredecessors.Add Predecessor:=predTask, Type:=linkType
End If
End Sub

—

3. エンジニアリングの要諦:メモリ管理とパフォーマンスの限界突破

Project VBAは、背後で重厚なCOMコンポーネント(MS ProjectのC++コアエンジン)を叩いている。そのため、以下の鉄則を破ると、メモリリークや「Automation Error」といった不可解なクラッシュに直面する。

1. 明示的なオブジェクト参照の切断

VBAのガベージコレクションは頼りにならない。特に `For Each` ループ内で取得した `Task` や `Dependency` オブジェクト、さらには `Dictionary` に格納したオブジェクト参照は、処理の終端で確実に `Set … = Nothing` を行い、COM参照カウントをデクリメントさせなければならない。

2. 計算エンジンの停止(`Application.Calculation`)

リンクを動的に何百件も追加・変更する場合、Projectはリンクが1本追加されるたびに全体のクリティカルパスやスケジュールを再計算しようとする。これがO(N^2)の遅延を生む原因となる。
必ず処理の冒頭で `Application.Calculation = pjCalculationManual` に落とし、全処理完了後に `pjCalculationAutomatic` に戻すこと。この一手間で、処理時間が数分から数秒へと劇的に短縮される。

3. トランザクション的なエラーハンドリング

外部システム連携において、途中でエラーが発生したまま中途半端にリンクが結ばれたプロジェクトファイルを保存されてはたまったものではない。エラーハンドラー内で必ず計算モードと画面描画を復元し、必要に応じてロールバック(あるいは変更の破棄 `ActiveProject.Saved = True` の制御)を考慮する設計が求められる。

—

総括

「ID」は人間が画面を見るための便宜上の数字に過ぎず、システムが信頼すべき識別子は常に 「UniqueID」 である。

この原則をコードの隅々にまで浸透させ、メモリのライフサイクルとProjectエンジンの挙動を完全に掌握した時、あなたの書くVBAコードは、どれほど複雑なWBSの改廃が起きようとも決して崩れない、揺るぎないインフラストラクチャへと昇華する。レガシーな呪縛を断ち切り、真に堅牢な自動化の地平へ到達してほしい。

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