【上級者向け】COMアドイン開発:プロジェクトのオープン・保存イベントを監視する常駐ツール
レガシーな現場において、Microsoft Projectは依然としてプロジェクト管理の要塞である。しかし、VBAマクロ単体によるアプローチには致命的な限界がある。ユーザーがマクロを有効化し忘れる、あるいはローカルに保存されたワークブックやMppファイルを勝手に改変・保存した場合、組織全体のプロジェクトガバナンスは容易に崩壊する。
真に堅牢なエンタープライズ・ガバナンスを実現するためには、VBAの枠組みを超越し、COMアドイン(COM Add-in)によるグローバルイベントの常駐監視を実装しなければならない。
本稿では、VB.NET(C#でも可)を用いてMS Projectのライフサイクルイベントをフックし、プロジェクトのオープンおよび保存時における強制的な統制を司るアーキテクチャの極限の知見を解説する。
—
1. アーキテクチャの全体像と設計思想
MS ProjectのCOMアドインは、宿主アプリケーション(Win32プロセス)のメモリ空間にロードされる。ここで最も重要なのは、「VBAのようにアドイン側から勝手にガベージコレクションが走るわけではない」という点だ。マネージドコード(.NET)とアンマネージドコード(MS Project COMオブジェクト)の境界において、参照カウント(Reference Counting)を誤れば、メモリリークや最悪のプロセスフリーズを引き起こす。
統制の要件
- オープン時(`ProjectOpen`): 読み込み時にサマリータスクの構造や必須カスタムフィールドの存在を検証し、違反している場合は強制終了、または読み取り専用へダウングレードする。
- 保存時(`ProjectBeforeSave`): ベースラインの設定状況やリソース割り当ての整合性を検証し、条件を満たさない場合の保存をキャンセルする。
—
2. 開発環境の要件とセットアップ
- 開発言語: VB.NET ( .NET Framework 4.8 又は .NET 6/8 Windowsデスクトップ)
- 対象ホスト: Microsoft Project (2016 / 2019 / 2021 / Microsoft 365)
- 参照設定:
- `Microsoft.Office.Interop.MSProject` (PIA: Primary Interop Assembly)
- `Extensibility` (COMアドイン用インターフェイス)
—
3. 実装:COMアドインのコアロジック
以下は、`Extensibility.IDTExtensibility2` を実装し、MS ProjectのアプリケーションイベントをフックするVB.NETのプロダクションコードである。
Imports Extensibility
import Microsoft.Office.Interop.MSProject
import System.Runtime.InteropServices
import System.Windows.Forms
Public Class Connect
Implements IDTExtensibility2
Private WithEvents m_app As MSProject.Application
‘ 1. アドイン読み込み時の初期化
Public Sub OnConnection(application As Object, connectMode As ext_ConnectMode, addInInst As Object, ByRef custom As Array) Implements IDTExtensibility2.OnConnection
Try
m_app = CType(application, MSProject.Application)
‘ イベントハンドラの結線はアドインライフサイクルの最重要ポイント
AddHandler m_app.ProjectOpen, AddressOf OnProjectOpenHandler
AddHandler m_app.ProjectBeforeSave, AddressOf OnProjectBeforeSaveHandler
Catch ex As Exception
MessageBox.Show(“COMアドイン初期化エラー: ” & ex.Message, “Governance Engine”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
‘ 2. アドインアンロード時のクリーンアップ(メモリ最適化の極意)
Public Sub OnDisconnection(disconnectMode As ext_DisconnectMode, ByRef custom As Array) Implements IDTExtensibility2.OnDisconnection
Try
If m_app IsNot Nothing Then
‘ イベントのデタッチ(メモリリークの根絶)
RemoveHandler m_app.ProjectOpen, AddressOf OnProjectOpenHandler
RemoveHandler m_app.ProjectBeforeSave, AddressOf OnProjectBeforeSaveHandler
‘ COMオブジェクトの明示的解放
Marshal.ReleaseComObject(m_app)
m_app = Nothing
End If
Catch
‘ 終了時の例外は握りつぶすかログ出力に留める
End Try
GC.Collect()
GC.WaitForPendingFinalizers()
End Sub
‘ — イベントハンドラ:プロジェクトオープン時 —
Private Sub OnProjectOpenHandler(proj As MSProject.Project)
If proj Is Nothing Then Return
Try
‘ 例:組織標準のカスタムフィールド「WBS_ID」が空の場合の統制
Dim customFieldVal As String = proj.ProjectSummaryTask.GetField(MSProject.PjField.pjTaskText1)
If String.IsNullOrEmpty(customFieldVal) Then
MessageBox.Show(“【統制警告】このプロジェクトには組織標準のWBS_IDが設定されていません。”, _
“Project Governance”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
‘ 必要に応じた強制クローズやフラグ立て
End If
Catch ex As Exception
‘ ログ出力処理
Finally
‘ ループ内やイベント内のCOMオブジェクトは速やかに解放
If proj IsNot Nothing Then Marshal.ReleaseComObject(proj)
End Try
End Sub
‘ — イベントハンドラ:プロジェクト保存前 —
Private Sub OnProjectBeforeSaveHandler(proj As MSProject.Project, ByRef Cancel As Boolean)
If proj Is Nothing Then Return
Try
‘ 例:ベースラインが設定されていない状態での保存をブロックする場合
Dim isBaselineSet As Boolean = False
‘ ベースライン確認のロジック(簡略化)
If proj.BaselineSavedDate(MSProject.PjBaseline.pjBaseline) = DateTime.MinValue Then
Dim result As DialogResult = MessageBox.Show(
“ベースラインが設定されていません。このまま保存を強行しますか?”,
“Governance Gatekeeper”,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question)
If result = DialogResult.No Then
Cancel = True ‘ 保存プロセスの強制キャンセル
End If
End If
Catch ex As Exception
Cancel = True ‘ 例外発生時は安全のため保存をブロック
Finally
If proj IsNot Nothing Then Marshal.ReleaseComObject(proj)
End Try
End Sub
‘ その他のIDTExtensibility2必須インターフェイス(実装は空で可)
Public Sub OnAddInsUpdate(ByRef custom As Array) Implements IDTExtensibility2.OnAddInsUpdate End Sub
Public Sub OnStartupComplete(ByRef custom As Array) Implements IDTExtensibility2.OnStartupComplete End Sub
Public Sub OnBeginShutdown(ByRef custom As Array) Implements IDTExtensibility2.OnBeginShutdown End Sub
End Class
—
4. チーフアーキテクトが指摘する「罠」とメモリ最適化の極意
VBA開発者がCOMアドイン開発に移行した際、最も陥りがちな罠が「RCW (Runtime Callable Wrapper) の寿命管理」である。
① COMオブジェクトの即時解放 (Marshal.ReleaseComObject)
.NETからMS ProjectのCOMオブジェクト(`Project`, `Task`, `Resource`など)にアクセスするたびに、裏ではRCWが生成される。これらを放置すると、GCが機能せず、MS Projectプロセス(`WINPROJ.EXE`)が終了してもメモリ上にゾンビプロセスとして残り続ける。
イベントハンドラ内で取得した `MSProject.Project` などのインスタンスは、処理の完了直後に必ず `Marshal.ReleaseComObject()` を呼んで参照カウントをデクリメントしなければならない。
② イベントデタッチの義務
`OnDisconnection` メソッドにおいて、`RemoveHandler` によるイベントのデタッチを行わない場合、マネージド側のハンドラがアンマネージド側のイベントソースを指し続けたままとなり、アプリケーション終了時に致命的な `AccessViolationException` を引き起こす。
—
5. レジストリ登録とデプロイメントの自動化
COMアドインは、VBAのように `.mpp` ファイルやグローバルテンプレートにコードを埋め込む必要がないため、「ユーザーに意識させずに全社展開できる」という圧倒的なメリットがある。
アドインを有効化するには、レジストリ(`HKEY_CURRENT_USER\Software\Microsoft\Office\Project\Addins\`)に適切なキーと値を書き込む必要がある。組織展開の際は、MSIインストーラーやグループポリシー(GPO)のスクリプトを用いてレジストリを自動構成するのが定石である。
[HKEY_CURRENT_USER\Software\Microsoft\Office\Project\Addins\ProjectGovernanceAddin.Connect]
“FriendlyName”=”Enterprise Project Governance Addin”
“Description”=”組織全体のプロジェクト管理を強制統制する常駐監視ツール”
“LoadBehavior”=dword:00000003
(※ `LoadBehavior` の `3` は、起動時に自動読み込みを意味する)
—
総括
VBAマクロによる局所的なハックの時代は終わった。組織全体のプロセスを統制し、ガバナンスを担保するためのシステムを構築するにあたり、COMアドインによるイベント監視は極めて強力かつ唯一無二の解である。
マネージドとアンマネージドの境界を完全に掌握し、メモリのライフサイクルを制御し切った者だけが、真に安定したエンタープライズ・アドインの構築を成し遂げることができる。
