銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Visual Studio 拡張でファイル保存イベントをフックする

Visual Studio拡張を作っているときに、特定のファイルの保存イベントを検知して、それに対する処理を書きたくなりました。 調べてみるとDocumentSaved というものが割とヒットするのですが、これMSDNには内部向けのAPIで利用者が使うことを想定していないようです。

_dispDocumentEvents_Event.DocumentSaved Event (EnvDTE)

また、csprojファイルなど一部のファイルについてはイベントが発火しないという問題があったので、StackOverflowに聞いたところIVsRunningDocumentTable サービスを使うといいという返事がきました。

stackoverflow.com

このサンプルコードそのままだと省略部分もあって動かなかったので、少し修正したのをメモ用にブログに書きたいと思います。

ここで出てくる Running Document Tableの詳細はMSDNにあります。

Running Document Table

IDEが内部で管理しているすべてのドキュメントに対する情報をもっているサービスで、ここでいう「開いている」というのは編集しているものだけではなく、(おそらくソリューションエクスプローラーなどで開かれている)すべてのドキュメントになります。

で、このRDTに対して、ファイル変更イベント(IVsRunningDocTableEvents)を通知するように自分自身をイベントハンドラとして登録するのがRunningDocumentTableEventsの役割になります。IVsRunningDocumentTableを引数に渡すように修正して、必要なメソッドをすべて継承したコードが下記になります。

using System;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;

internal abstract class RunningDocumentTableEvents : IDisposable, IVsRunningDocTableEvents
{
    private readonly IVsRunningDocumentTable rdt;

    private readonly uint sinkCookie;

    internal RunningDocumentTableEvents(IVsRunningDocumentTable rdt)
    {
        this.rdt = rdt;
        uint cookie;
        rdt.AdviseRunningDocTableEvents(this, out cookie);
        sinkCookie = cookie;
    }

    protected abstract void OnAfterSave(AfterSaveEventArgs e);

    public virtual int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
    {
        return VSConstants.S_OK;
    }

    public virtual int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
    {
        return VSConstants.S_OK;
    }

    int IVsRunningDocTableEvents.OnAfterSave(uint docCookie)
    {
        uint flags, readLocks, editLocks, itemId;
        string moniker;
        IVsHierarchy hierarchy;
        IntPtr docData;

        var hr = rdt.GetDocumentInfo(
            docCookie, out flags, out readLocks, out editLocks, out moniker,
            out hierarchy, out itemId, out docData);

        if (hr == VSConstants.S_OK)
        {
            var e = new AfterSaveEventArgs { FileName = moniker};
            OnAfterSave(e);
        }

        return VSConstants.S_OK;
    }

    public virtual int OnAfterAttributeChange(uint docCookie, uint grfAttribs)
    {
        return VSConstants.S_OK;
    }

    public virtual int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
    {
        return VSConstants.S_OK;
    }

    public virtual int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)
    {
        return VSConstants.S_OK;
    }

    public void Dispose()
    {
        rdt.UnadviseRunningDocTableEvents(sinkCookie);
    }
}

internal class AfterSaveEventArgs : EventArgs
{
    public string FileName { get; internal set; }
}

このクラスをPackageを継承したクラスから実際に利用する例がこうなります。

using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

private MyEvents events;

protected override void Initialize()
{
    base.Initialize();
    events = new MyEvents((IVsRunningDocumentTable)GetService(typeof(IVsRunningDocumentTable)));
}

class MyEvents : RunningDocumentTableEvents
{

    protected override void OnAfterSave(AfterSaveEventArgs e)
    {
        Debug.WriteLine($"saved: {e.FileName}");
    }

    public MyEvents(IVsRunningDocumentTable rdt) : base(rdt)
    {
    }
}

イベントクラスを継承してその中に処理を書くというのが書きやすいかといわれると気になるところですが、いったんはこれで動きました。

ちなみにこの手の特定のUIからのアクションにひもづかずに、プロジェクトがあれば自動で読み込まれてほしいパッケージは、PackageクラスにProvideAutoLoad属性をつけておくと読み込んでくれます。

using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
public sealed class VSPackage : Package