【実務・中級編】【上級者向け】COMアドイン開発:プロジェクトのオープン・保存イベントを監視する常駐ツール – Project VBA解析バイブル

スポンサーリンク

【上級者向け】COMアドイン開発:プロジェクトのオープン・保存イベントを監視する常駐ツール

現場のプロジェクトマネージャーや情シス担当者から、こんな泣き言を聞いたことはないだろうか。
「また担当者がベースラインを設定せずに進捗を更新した」
「勝手にWBSの構造を改変されて、全社集計が崩壊した」
「ローカルに勝手に保存されて、最新版がどこにあるかわからない」

VBAのマクロ(Personal.xlsbやアドイン)でどうにかしようとした者なら全員ぶ S ぶる壁がある。それは、「VBAのイベントプロシージャは、対象のプロジェクトが開かれてからしかフックできない」という根本的な制約だ。ファイルを開く瞬間、あるいは保存しようとする瞬間を、マクロが起動する前に捉えることは、通常のVBAでは不可能に近い。

この泥沼のガバナンス崩壊を根底から解決唯一の解、それが「COMアドインによるグローバルイベント監視アーキテクチャ」である。

今回は、Project VBAの枠組みを飛び越え、COMアドイン(VB.NET製)を用いてMS Projectのライフサイクルそのものを常駐監視し、全社的なプロジェクト統制を強制執行するプロフェッショナルな設計と実装を伝授する。

1. なぜ「通常のVBA」では統制に失敗するのか?

多くの企業が、`ThisProject_Open()` や `ThisProject_BeforeSave()` といった標準イベントに処理を記述し、それをテンプレートやアドイン配ることで統制を図ろうとする。

しかし、このアプローチには致命的な欠陥が3つある。

1. イベントリスナーの起動遅延:
プロジェクトファイルを開いた「その瞬間」には、まだVBAプロジェクトの初期化やイベントハンドラの結びつけが完了していない。ファイルオープン直後の厳密な検証や改変検知をすり抜ける。
2. マクロの無効化リスク:
ユーザーがセキュリティ設定でマクロを無効化して開けば、すべての統制コードは沈黙する。
3. コードのバイパス:
別のVBAや外部スクリプトから自動化された場合、意図しないタイミングで保存や上書きが行われ、イベントが正しく発火しない。

究極の解決策:COMアドイン(IDTExtensibility2)

MS Projectそのもののアプリケーションインスタンス(`MSProject.Application`)のCOMイベントを、外側から直接フックする。これこそが、ユーザーの意図やマクロの有効無効に左右されず、OSレベル・プロセスレベルでプロジェクトの生死を監視する唯一のプロフェッショナルなアプローチである。

2. アーキテクチャ全体像と設計思想

今回構築する常駐ツールのアーキテクチャは以下の通りだ。

  • ホスト: Microsoft Project (COMサーバー)
  • アドイン: .NET Framework (C#) によるCOMアドイン (`IDTExtensibility2` 実装)
  • 監視対象イベント:
  • `Application.ProjectOpen`(オープン時統制:ベースライン未設定の警告・強制設定)
  • `Application.BeforeSave`(保存時統制:必須メタデータの検証・サーバー強制保存への誘導)

※COMアドインの開発には Visual Studio (Community以上) が必要となる。VBA開発者であっても、ワンランク上のインフラストラクチャを構築するためには、このレイヤーのコードを読み書きできる知見が不可欠だ。

3. プロダクションコード実装

以下のC#コードは、Visual Studioの「Shared Add-in」または「COM Add-in」プロジェクトテンプレートをベースに、MS Projectのアプリケーションイベントをキャプチャし、統制ロジックを走らせるための堅牢な実装である。

using System;
using System.Runtime.InteropServices;
using Extensibility;
using Microsoft.Office.Core;
using MSProject = Microsoft.Office.Interop.MSProject;

namespace ProjectGovernanceAddIn
{
[Guid(“A1B2C3D4-E5F6-7890-ABCD-EF0123456789”)]
[ProgId(“ProjectGovernanceAddIn.Connect”)]
public class Connect : IDTExtensibility2
{
private MSProject.Application _app;

// アドインがロードされた瞬間にフックを仕掛ける
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
_app = application as MSProject.Application;

if (_app != null)
{
// イベントハンドラのバインド(アプリケーションレベルのグローバル監視)
_app.ProjectOpen += OnProjectOpen;
_app.BeforeSave += OnBeforeSave;
}
}

public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
{
if (_app != null)
{
// メモリリークおよびCOMオブジェクトの参照解放
_app.ProjectOpen -= OnProjectOpen;
_app.BeforeSave -= OnBeforeSave;
_app = null;
}
GC.Collect();
}

// — オープン時イベント:プロジェクトの初期状態を検査 —
private void OnProjectOpen(MSProject.Project pj)
{
try
{
if (pj == null) return;

// 例:PMOが指定する命名規則に従っているかチェック
if (!pj.Name.StartsWith(“PRJ-“, StringComparison.OrdinalIgnoreCase))
{
System.Windows.Forms.MessageBox.Show(
“【統制アラート】\nこのプロジェクトファイル名は組織の命名規則(PRJ-から開始)に違反しています。”,
“PMOガバナンス統制システム”,
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Warning);
}

// ベースラインが設定されているかチェック(例:Baseline1が存在するか)
// MSProjectのCOMオブジェクトモデルを安全に操作
bool hasBaseline = false;
foreach (MSProject.Baseline b in pj.Baselines)
{
if (b.Saved) { hasBaseline = true; break; }
}

if (!hasBaseline)
{
// 必要に応じて自動的にベースラインを設定する、あるいはフラグを立てる
System.Windows.Forms.MessageBox.Show(
“【ガバナンス警告】\nこのプロジェクトにはベースラインが設定されていません。速やかに計画値を確定させてください。”,
“PMO統制システム”,
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Information);
}
}
catch (Exception ex)
{
// 予期せぬ例外でMS Project本体をクラッシュさせないための厳格な例外防御
System.Diagnostics.Debug.WriteLine($”[Error] OnProjectOpen: {ex.Message}”);
}
}

// — 保存前イベント:不正な保存やメタデータの欠落をブロック —
private void OnBeforeSave(MSProject.Project pj, ref bool Cancel)
{
try
{
if (pj == null) return;

// 必須カスタムプロパティ(例:cost center)が入力されているか検証
bool costCenterExists = false;
foreach (MSProject.DocumentProperty prop in pj.CustomDocumentProperties)
{
if (prop.Name == “CostCenter” && !string.IsNullOrEmpty(prop.Value?.ToString()))
{
costCenterExists = true;
break;
}
}

if (!costCenterExists)
{
System.Windows.Forms.MessageBox.Show(
“【保存拒否】\nカスタムプロパティ ‘CostCenter’ が設定されていないため、プロジェクトを保存できません。”,
“PMOガバナンス統制システム”,
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);

// 保存処理を強制キャンセル
Cancel = true;
}
}
catch (Exception ex)
{
// 例外発生時は安全側に倒し、保存をキャンセルさせるか、ログに残す
System.Diagnostics.Debug.WriteLine($”[Error] OnBeforeSave: {ex.Message}”);
}
}

// その他のIDTExtensibility2インターフェース実装(空でOK)
public void OnAddInsUpdate(ref Array custom) { }
public void OnStartupComplete(ref Array custom) { }
public void OnBeginShutdown(ref Array custom) { }
}
}

4. 実務で絶対に外せない「3つの罠」と対策

このアーキテクチャを現場に導入する際、シニアエンジニアとして必ず考慮すべき実務的罠が存在する。

1. COMオブジェクトの解放とメモリリーク

VBAと同様に、C#からCOMオブジェクト(`MSProject.Project` や各種コレクション)を扱う際、不必要に中間オブジェクトを変数に格納し解放を怠ると、MS Project終了後もバックグラウンドでプロセスが残留する(ゾンビプロセス問題)。

  • 対策: ループ処理内でのCOMオブジェクトの生成を最小限にし、必要に応じて `Marshal.ReleaseComObject()` を明示的に呼び出すか、ガベージコレクションの挙動を意識したコードを書くこと。

2. 例外処理(Exception Handling)の厳格化

COMアドイン内で未処理の例外(Unhandled Exception)が発生した場合、MS Projectアプリケーション全体が前触れもなく強制終了する。 ユーザーが編集中のデータがすべて吹き飛ぶという最悪の事故に繋がるため、イベントハンドラの根元(`try-catch`)で必ず全ての例外をキャッチし、ログ出力に留め、ホストアプリケーションを絶対にクラッシュさせない設計が必須。

3. デプロイメントとセキュリティ(レジストリ登録)

COMアドインは、単にファイルを所定の位置に置くだけでは動作せず、Windowsレジストリへの適切な登録(`HKEY_CURRENT_USER\Software\Microsoft\Office\MS Project\Addins\`)が必要となる。

  • 対策: Visual StudioのDeploymentプロジェクトや、WiX Toolset、あるいは企業内のSCCM/Intune等を用いたサイレントインストールパッケージとして配布する仕組みを整えること。

5. チーフアーキテクトからの総括

VBAのマクロコードを各ファイルにバラバラにばら撒く時代は終わった。組織の規模が大きくなり、プロジェクト管理の精度が企業城壁の強さを左右する現代において、ガバナンスは「個人の意識」や「マニュアルの遵守」に頼るべきではない。

「システム構造によって、不正やミスの発生し得ない環境を強制的に構築する」

これこそが、上級エンジニアに求められる真のシステム設計である。VBAの限界を知り、COMアドインという一段上のレイヤーへ踏み出すことで、あなたの作るツールは単なる「便利マクロ」から、組織を動かす「インフラストラクチャ」へと昇華する。

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