【実務・中級編】【上級者向け】カスタムフィールドの定義変更を検知し、ログを記録するイベントハンドラ – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

カスタムフィールドの「聖域」を守れ:MS Projectにおける数式改ざん検知の実装戦略

プロジェクト管理の自動化において、カスタムフィールド(Task/Resource Custom Fields)は単なる「メモ欄」ではない。それは、プロジェクトの成否を分ける計算ロジック、KPIの算出、リソース配分の最適化アルゴリズムが凝縮された「ビジネスロジックの心臓部」である。

もし、現場のユーザーが「少し使いやすくしたいから」という安易な理由で、複雑な数式を書き換えてしまったらどうなるか? 算出される進捗率は狂い、レポートは嘘をつき、経営判断は誤る。

多くの開発者は、カスタムフィールドの「値」の変化には気を配るが、その「定義(数式)」の変更を見逃す。本稿では、MS Projectのイベントモデルの限界を理解した上で、カスタムフィールドの定義変更を確実に捕捉し、監査ログを記録する「インテグリティ・ガード(整合性保護)」の設計手法を伝授する。

1. 凡ミスを回避せよ:MS Projectイベントモデルの「罠」

まず、初心者が陥る致命的な勘違いを指摘しておこう。
「カスタムフィールドの数式が変更された瞬間に発火するイベント(`OnFormulaChanged` のようなもの)を探す」ことだ。

断言するが、MS Projectにはカスタムフィールドの数式変更を直接検知するイベントは存在しない。

我々プロフェッショナルが取るべき戦略は、イベントを待つことではなく、「状態の差分(State Delta)」を検知することである。具体的には、以下のプロセスを実装する。

1. Snapshot(スナップショット)の保持: 正当な数式の状態をハッシュ値、あるいは文字列の集合として保存しておく。
2. Integrity Check(整合性チェック): `ProjectBeforeSave`(保存前)などのクリティカルなタイミングで、現在の数式群を再計算し、保存済みのスナップショットと比較する。
3. Detection & Logging(検知と記録): 差分があれば、それを「不正な変更」とみなし、ユーザー情報・時刻とともにログへ叩き込む。

2. 高度な設計:クラスモジュールによるカプセル化

スパゲッティコードを避けるため、ロジックは「検知」「記録」「管理」の3つの責務に分離する。
ここでは、保守性を高めるためにクラスモジュールを用いた設計を行う。

構成要素

  • `clsFormulaGuard`: 数式のスナップショット管理と差分比較を担当。
  • `clsAuditLogger`: ログファイルへの堅牢な書き込みを担当。
  • `ThisProject`: イベントの入り口。

3. プロダクション実装例

以下に、そのまま実務に投入可能なレベルのコード構造を示す。

A. クラスモジュール: `clsFormulaGuard`

数式の整合性を監視するエンジンだ。

' [Class Module: clsFormulaGuard]
Option Explicit

Private pSnapshot As String
Private pIsInitialized As Boolean

' カスタムフィールドの数式を連結して一つの文字列(指紋)として生成する
' パフォーマンスを考慮し、全てのフィールドをループして結合する
Private Function GenerateFormulaFingerprint() As String
    Dim t As Task
    Dim i As Integer
    Dim fingerprint As String
    Dim currentFormula As String
    
    On Error Resume Next ' フィールドが未定義の場合のランタイムエラーを回避
    
    ' Task Custom Fields (Text1-30, Number1-30, etc.) の数式を走査
    ' ここでは例として Text1-Text5 のみ。実務では全範囲をカバーすること
    For i = 1 To 30
        ' Projectオブジェクト経由で各フィールドのFormulaプロパティを取得
        ' ※注意: Formulaプロパティへのアクセスはオブジェクトのライフサイクルに依存する
        currentFormula = Project.TaskField(i, "Text").Formula
        fingerprint = fingerprint & "|T" & i & ":" & currentFormula
        
        currentFormula = Project.TaskField(i, "Number").Formula
        fingerprint = fingerprint & "|N" & i & ":" & currentFormula
    Next i
    
    GenerateFormulaFingerprint = fingerprint
    On Error GoTo 0
End Function

' 初期状態(正常な状態)を記録する
Public Sub InitializeSnapshot()
    pSnapshot = GenerateFormulaFingerprint()
    pIsInitialized = True
End Sub

' 現在の状態がスナップショットと一致するか検証する
' 戻り値: True = 正常, False = 変更を検知
Public Function CheckIntegrity() As Boolean
    If Not pIsInitialized Then
        ' 初期化されていない場合は、現在の状態を正としてセットする(初回起動時対応)
        InitializeSnapshot
        CheckIntegrity = True
        Exit Function
    End If
    
    If GenerateFormulaFingerprint() <> pSnapshot Then
        CheckIntegrity = False
    Else
        CheckIntegrity = True
    End If
End Function

' 変更があった場合、新しい状態をスナップショットとして更新する(管理者による修正後など)
Public Sub UpdateSnapshot()
    pSnapshot = GenerateFormulaFingerprint()
End Sub

B. クラスモジュール: `clsAuditLogger`

ログ出力は、エラーが起きてもメイン処理を止めない「非同期的な堅牢さ」が求められる。

' [Class Module: clsAuditLogger]
Option Explicit

Private pLogPath As String

Private Sub Class_Initialize()
    ' ログファイルの保存先(実行ファイルと同じディレクトリを想定)
    pLogPath = ThisProject.Path & "\audit_log_" & Format(Date, "yyyymmdd") & ".csv"
End Sub

' ログを追記する。ファイルがなければ作成する。
Public Sub WriteLog(msg As String)
    Dim fileNum As Integer
    fileNum = FreeFile
    
    On Error GoTo ErrHandler
    
    ' CSV形式で書き出し(ユーザー名, 時刻, メッセージ)
    Open pLogPath For Append As #fileNum
    Print #fileNum, Format(Now, "yyyy-mm-dd HH:mm:ss") & "," & _
                    Environ("USERNAME") & "," & _
                    """" & msg & """"
    Close #fileNum
    Exit Sub

ErrHandler:
    ' ログ出力失敗がプロジェクトの保存を妨げてはならない。
    ' 致命的なエラーとして無視するか、デバッグウィンドウに出力するに留める。
    Debug.Print "CRITICAL ERROR: Failed to write audit log. " & Err.Description
    If fileNum > 0 Then Close #fileNum
End Sub

C. プロジェクトモジュール: `ThisProject`

ここでイベントを捕捉し、ガードを起動させる。

' [ThisProject Module]
Option Explicit

Private guard As clsFormulaGuard
Private logger As clsAuditLogger

' プロジェクトが開かれた時に初期化
Private Sub Project_Open()
    Set guard = New clsFormulaGuard
    Set logger = New clsAuditLogger
    
    ' 初回起動時は現在の状態を「正」としてスナップショットを作成
    guard.InitializeSnapshot
End Sub

' 保存直前に整合性をチェック
Private Sub Project_ProjectBeforeSave(ByVal SaveOK As Boolean)
    ' 整合性チェック
    If Not guard.CheckIntegrity() Then
        ' 変更を検知した場合
        logger.WriteLog "WARNING: Custom Field Formula integrity breach detected."
        
        ' ユーザーへの警告(プロダクション環境では、強制停止させるか、ログのみにするか設計次第)
        Dim response As VbMsgBoxResult
        response = MsgBox("警告: カスタムフィールドの数式が変更されています。" & vbCrLf & _
                          "変更を記録し、現在の状態を新しい基準として保存しますか?", _
                          vbExclamation + vbYesNo, "Integrity Alert")
        
        If response = vbYes Then
            guard.UpdateSnapshot()
            logger.WriteLog "INFO: User acknowledged the change and updated the snapshot."
        Else
            ' 保存をキャンセルさせる(厳格な運用の場合)
            ' SaveOK = False 
            logger.WriteLog("INFO: User cancelled the save due to integrity breach.")
        End If
    End If
End Sub

4. アーキテクトの視点:実務における注意点

このコードをプロダクション環境に導入する際、以下の3点を必ず考慮せよ。

① パフォーマンスの重み

`GenerateFormulaFingerprint` 内で全てのカスタムフィールドをループさせている。フィールド数が膨大(数百規模)な場合、`ProjectBeforeSave` の実行時に一瞬のラグが生じる。
しかし、数式文字列の連結はメモリ上で極めて高速だ。ユーザーが「保存ボタンを押した瞬間に数秒待たされる」ことのデメリットと、「データの整合性が保たれる」ことのメリットを天秤にかけ、適切なフィールド範囲に限定せよ。

② 権限とログの整合性

ログをローカルのCSVに出力する設計にしているが、これは「ログの改ざん」に対して無力だ。
真にエンタープライズな環境(セキュリティ要件が高いプロジェクト)であれば、`clsAuditLogger` の内部実装を、ネットワーク上の共有ディレクトリや、API経由でのデータベース(SQL Server等)への書き込みに差し替えよ。クラスを分離しているため、インターフェースを変えるだけで容易にアップグレードが可能だ。

③ 「正解」の定義(Snapshotの更新タイミング)

この設計の肝は、`guard.UpdateSnapshot` をいつ呼ぶかにある。
「管理者が意図的に数式を更新したとき」にだけ、このメソッドを呼び出す管理用マクロを別途用意しておくのが、最もスマートな運用である。

まとめ

カスタムフィールドの数式は、プロジェクトの「暗黙のルール」をコード化したものである。
それらを「ただの入力項目」として扱うか、「保護すべきロジック」として扱うか。その差が、自動化エンジニアとしての格の違いを決める。

今回示した「スナップショット比較による整合性監視」は、MS Projectに限らず、あらゆるオブジェクト指向のシステムにおける「状態監視」の基本原則である。この設計思想を血肉とし、堅牢なツールを構築してほしい。

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