tafuji's blog

C#, Xamarin, Azure DevOps を中心に書いています。

Visual Studio の拡張機能でコンテキストメニュー上にコマンドを表示する方法

Qiita より転載

はじめに

とあるツールを作成しているときに、Visual Studioコンテキストメニュー上に表示する方法について調べたので、その方法についてメモを残しておきます。具体的には、以下のコンテキストメニュー上にカスタムコマンドを表示する方法について記載します。

サンプルコード

サンプルコードは GitHub に公開しています。

コンテキストメニューの表示方法

Visual Studio Command Table ファイル

Visual Studio のコマンドのアイコン、コマンド表示位置等は、 Visual Studio Command Table (Vsct) ファイルで定義されます。

この Vsct ファイルを編集することで、カスタムコマンドをコンテキストメニュー上に表示させることができます。以下に示すのは、Visual Studio の新規項目の追加で、カスタムコマンドを追加したときの Vsct ファイルの一部です。

<Groups>
  <Group guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
  </Group>
</Groups>

この Parent 要素の guid 属性と id 属性の値で、カスタムメニューコマンドの表示位置が決定されます。このまま、Parent 要素の属性を編集してもよいのですが、コマンドの表示位置が一つに限定されてしまうので、CommandPlacements 要素内に、コマンドの表示位置を定義していくことをお勧めします。

以下は、コマンドの表示位置を CommandReplacements 要素内に定義した場合の例です。このように、CommandPlacements要素をうまく活用すると、一つのコマンドに対して、二つのコマンドの表示位置が定義できるようになります。

<Groups>
  <Group guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0x0600" />
</Groups>

<!-- 中略 -->

<CommandPlacements>
    <!-- File Context Menu -->
    <CommandPlacement guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
      <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE" />
    </CommandPlacement>

    <!-- Web File Context Menu -->
    <CommandPlacement guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
      <Parent guid="guidCSHTMLFileContextMenu" id="CSHTMLFileContextMenu" />
    </CommandPlacement>
</CommandPlacements>

では、guid 属性、 id 属性の値をどのように指定すればよいかですが、以下の二つの方法があります。

  • Micorsoft のドキュメントに記載されていないかを調べる
  • ツールを利用して GUID 値、id 値を調べる

Micorsoft のドキュメントに記載されていないかを調べる

Microsoft のドキュメントには、Visual Studi のメニューの GUID 値、id 値がまとめられたものがあります。

このドキュメントでは、 GUID 値は、guidSHLMainMenu と記載されています。ドキュメントに直接記載はないのですが、ここに記載のある Vsct フィアルを検索すると、コンテキストメニューの id 値を調べることができます。主なコンテキストメニューの id 値を以下の表に記載します。

id 説明
IDM_VS_CTXT_SOLNNODE ソリューションのコンテキストメニュー
IDM_VS_CTXT_PROJNODE プロジェクトのコンテキストメニュー
IDM_VS_CTXT_XPROJ_MULTIPROJ 複数のプロジェクトを選択したときのコンテキストメニュー
IDM_VS_CTXT_WEBPROJECT Web プロジェクト(以前のバージョン)
IDM_VS_CTXT_ITEMNODE ソリューションエクスローラーで、プロジェクトの項目を選択したときのコンテキストメニュー
IDM_VS_CTXT_CODEWIN コード・ウィンドウのコンテキストメニュー

この表によれば、ソリューションのコンテキストメニュー上にコマンドを表示するには、GUID 値に guidSHLMainMenu を指定、id 値に IDM_VS_CTXT_SOLNNODE を利用すればよいことがわかります。なお、この値を調べるに時に、以下のサイトも参考にしました。

ツールを利用して GUID 値、id 値を調べる方法

ドキュメントに記載がないメニュー(例えば、動的に生成されるメニュー等)が持つ GUID 値、コマンドの ID を調べるには、Extensibility Tools というツールを利用することをお勧めします。このツールを利用すると、regedit 等でレジストリキーを編集する手間を省くことがでるからです。このツールをインストールした後、[View] メニューの [Enable VSIP Logging] を選択すると、Visual Studio を再起動するかどうかのダイアログが表示されます。

EnableVSIPLogging

Visual studio の再起動後、GUID 値、コマンドの ID を調べたいメニューを [shift] キー、[ctrl] キーを同時に押して選択すると、ダイアログ上にメニューのデータ(GUID 値、コマンドの IDなど)が表示されるようになります。

MenuData

このデータは、[Ctrl + c] でコピーして、メモ帳などに張り付けることができます。

NotePad

これ以降は、各コンテキストメニュー上にコマンドを表示する方法について記載します。

ソリューション

ソリューションのコンテキストメニューにコマンドを表示する方法を記載します。

SolutionContextMenu

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_SOLNNODE を設定します。

<CommandPlacements>
  <!-- Solution Context Menu -->
  <CommandPlacement guid="guidSolutionContextMenuCommandPackageCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_SOLNNODE" />
  </CommandPlacement>
</CommandPlacements>

コマンドの処理: ソリューション名を表示する

サンプルでは、ソリューション名をダイアログに表示しています。ソリューション名の取得には、EnvDTE.DTE インタフェースを利用しています。EnvDTE.DTE オブジェクトの取得処理は、カスタムコマンド作成時に自動生成されたコマンドのクラスの InitializeAsync メソッド内で取得します。

private EnvDTE.DTE _dte;

public static async Task InitializeAsync(AsyncPackage package)
{
    // Verify the current thread is the UI thread - the call to AddCommand in SolutionContextMenuCommand's constructor requires
    // the UI thread.
    ThreadHelper.ThrowIfNotOnUIThread();
    OleMenuCommandService commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService;
    Instance = new SolutionContextMenuCommand(package, commandService);
    Instance._dte = await package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
}

以下は、コマンド実行時にソリューション名を表示するサンプルです。

private void Execute(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    if (_dte == null)
        return;
    EnvDTE.Solution solution = _dte.Solution;
    string message = $"Selected Solution is {solution.FullName}";
    string title = "Solution Context Menu Command";

    // Show a message box
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

プロジェクト

プロジェクトのコンテキストメニューにコマンドを表示する

プロジェクトのコンテキストメニューにカスタムコマンドを表示する方法を記載します。

ProjectContextMenu

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_PROJNODE を設定します。

<CommandPlacements>
  <!-- Project Node -->
  <CommandPlacement guid="guidProjectContextMenuCommandPackageCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_PROJNODE" />
  </CommandPlacement>
</CommandPlacements>

コマンドの処理: 選択したプロジェクト名を表示する

選択したプロジェクトの取得には、EnvDTE.DTE インタフェースの ActiveSolutionProjects プロパティを利用します。以下にサンプルを示します。

private void Execute(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    if (_dte == null)
        return;
    EnvDTE.Solution solution = _dte.Solution;
    EnvDTE.Project project = (EnvDTE.Project)((object[])_dte.ActiveSolutionProjects)[0];
    string message = $"Selected project is {project.Name}";
    string title = "Project Context Menu Command";

    // Show a message box
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

複数のプロジェクト選択時のコンテキストメニューにコマンドを表示する

複数のプロジェクトが選択されたときのコンテキストメニュー上にカスタムコマンドを表示する方法を記載します。

MultiProject

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_XPROJ_MULTIPROJ を設定します。

<CommandPlacements>
  <!-- Multi Projects Selected -->
  <CommandPlacement guid="guidMultiProjectsContextMenuCommandPackageCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_XPROJ_MULTIPROJ" />
  </CommandPlacement>
</CommandPlacements>

コマンドの処理: 選択したすべてのプロジェクト名を表示する

選択されたプロジェクトは、EnvDTE.DTEActiveSolutionProjects プロパティから取得することができます。以下にサンプルを示します。

private void Execute(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    if (_dte == null)
        return;

    EnvDTE.Solution solution = _dte.Solution;
    object[] projects = ((object[])_dte.ActiveSolutionProjects);
    var builder = new StringBuilder();
    foreach (var item in projects)
    {
        EnvDTE.Project project = (EnvDTE.Project)item;
        builder.Append($"Project name: {project.Name}");
        builder.Append(Environment.NewLine);
    }
    string message = builder.ToString();
    string title = "Multi Projects Context Menu Command";

    // Show a message box
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

特定のプロジェクトのコンテキストメニューにコマンドを表示する

ここでは、ASP.NET の Web アプリケーションプロジェクト(Visual Studio 2017 の プロジェクト作成では、以前のバージョンの Web アプリケーションプロジェクト)のコンテキストメニュー上にコマンドを表示する方法について記載します。

PreviousWebProject

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_XPROJ_MULTIPROJ を設定します。

<CommandPlacements>
  <!-- Web Project (Previous Versions) Context Menu-->
  <CommandPlacement guid="guidPreviousVersionWebProjectContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_WEBPROJECT" />
  </CommandPlacement>
</CommandPlacements>

コマンドの処理: 選択されたプロジェクト名を表示する

選択されたプロジェクト名を取得する方法は、"プロジェクトのコンテキストメニューにコマンドを表示する" と同様です。ActiveSolutionProjects プロパティを取得することがポイントになります。

EnvDTE.Solution solution = _dte.Solution;
EnvDTE.Project project = (EnvDTE.Project)((object[])_dte.ActiveSolutionProjects)[0];
string message = $"Selected project is {project.Name}";

選択されたプロジェクトの種類によって動的にコマンドを表示する方法

ここでは、ASP.NET Core の Web プロジェクトのコンテキストメニュー上にコマンドを表示する方法について記載します。

AspNetCoreWebProject

ASP.NET Core の Web プロジェクトのコンテキストメニューの GUID 値、id 値は、一般的なクラスライブラリのコンテキストメニューの GUID 値、id 値と同じです。

  • ASP.NET Core プロジェクトの場合

AspNetCoreGuid

  • クラスライブラリプロジェクトの場合

ClassLibraryGuid

Parent 要素の guid 属性と id 属性の値を指定するだけでは、クラスライブラリプロジェクトのコンテキストメニュー上にもコマンドが表示されてしまいます。従って、選択されたプロジェクトの種類を判定して、コマンドの表示・非表示を切り替える必要があります。具体的には、方法で表示・非表示を切り替えます。

  • Vsct ファイルでコマンドの表示が動的であることを宣言する
  • コマンドの処理で、プロジェクトの種類を判定し、コマンドの表示・非表示の設定を行う

Vsct ファイル

コマンドの表示が動的であることを宣言するために、以下の設定を行います。

  • コマンドの Button 要素の子要素に CommandFlag 要素を利用して、以下の設定を行う
    • 要素の値に DynamicVisibility を設定し、動的に表示を切り替える設定を定義する
    • デフォルトでコマンドを非表示にするため、DefaultInvisible に値を設定する

以下にサンプルを記載します。

<Button guid="guidWebProjectContextMenuCommandPackageCmdSet" id="cmdidWebProjectContextMenuCommand" priority="0x0100" type="Button">
  <Parent guid="guidWebProjectContextMenuCommandPackageCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPic1" />
  <!-- Dynamic Visibility -->
  <CommandFlag>DefaultInvisible</CommandFlag>
  <CommandFlag>DynamicVisibility</CommandFlag>
  <Strings>
    <ButtonText>Invoke Web Project Context Menu Command</ButtonText>
  </Strings>
</Button>

<!-- 中略 -->

<CommandPlacements>
    <!-- Web Project Context Menu-->
    <CommandPlacement guid="guidWebProjectContextMenuCommandPackageCmdSet" id="MyMenuGroup" priority="0xFFFF">
      <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_PROJNODE" />
    </CommandPlacement>
</CommandPlacements>

コマンドの処理

  • OleMenuCommand クラスを利用する
    • Visual Studio により自動生成されたコードでは、MenuCommand クラスが利用されていが、これを利用しない。
  • OleMenuCommand クラスの BeforeQueryStatus イベントハンドラを実装する

以下にサンプルを記載します。

private WebProjectContextMenuCommand(AsyncPackage package, OleMenuCommandService commandService)
{
    this.package = package ?? throw new ArgumentNullException(nameof(package));
    commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

    var menuCommandID = new CommandID(CommandSet, CommandId);
    // Instantiate OleMenuCommand object
    var menuItem = new OleMenuCommand(this.Execute, menuCommandID);

    // Add event handler
    menuItem.BeforeQueryStatus += new EventHandler(BeforeQueryStatus);
    commandService.AddCommand(menuItem);
}

private void BeforeQueryStatus(object sender, EventArgs e)
{
    if (_dte == null)
        return;
    EnvDTE.Solution solution = _dte.Solution;
    EnvDTE.Project project = (EnvDTE.Project)((object[])_dte.ActiveSolutionProjects)[0];

    var cmd = (OleMenuCommand)sender;
    // プロジェクトが ASP.NET Core のプロジェクトの場合はコマンドを見えるようにする
    cmd.Visible = project.Kind == "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}";
}

また、拡張機能の読み込みが遅延するのを避けるために、ProvideAutoLoad 属性を付与しておきます。

[ProvideAutoLoad(VSConstants.UICONTEXT.SolutionExists_string)]
// 省略
public sealed class VsPackage : AsyncPackage
{
  // 省略
}

プロジェクトの種類の GUID 値を調べる方法

カスタムコマンドを見えるように設定する判定処理では、Project.Kind の値を ASP.NET Core のプロジェクトの種類を表す GUID 値と比較しています。この GUID 値を調べる時にも、Extensibility Tools が利用できます。プロジェクトを右クリックして、[Show Project Information] を実行します。

ShowProjectInformation

すると、テキストファイルにプロジェクトに関する情報が出力されます。テキストファイルの Kind の値がプロジェクトの種類を表す GUID 値になります。

ProjectKind

ファイル

ソリューションエクスプローラー上でファイル(通常の *.cs*.cshtml ファイル)が選択されたときのコンテキストメニュー上にカスタムコマンドを表示する方法を記載します。

FileContextMenu

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_ITEMNODE を設定します。ASP.NET Core の cshtml ファイルのコンテキストメニューは、前述の GUID 値と id 値ではありませんでした。 Extensibility Tools を利用して、コンテキストメニューの GUID 値と id 値を取得し、GuidSymbol 要素に設定を加えています。

<CommandPlacements>
  <!-- File Context Menu -->
  <CommandPlacement guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE" />
  </CommandPlacement>
  <!-- Web File Context Menu -->
  <CommandPlacement guid="guidFileContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidCSHTMLFileContextMenu" id="CSHTMLFileContextMenu" />
  </CommandPlacement>
</CommandPlacements>

<GuidSymbol name="guidCSHTMLFileContextMenu" value="{D309F791-903F-11D0-9EFC-00A0C911004F}">
  <IDSymbol value="1138" name="CSHTMLFileContextMenu" />
</GuidSymbol>

コマンドの処理

選択されたファイルを取得するには、EnvDTE.DTESelectedItems コレクションを取得し、Item プロパティにインデックスを指定します。(インデックスは、0 ではなく 1 から始まるようです)

EnvDTE.SelectedItem item = _dte.SelectedItems.Item(1);
string message = $"Selected File is {item.ProjectItem.Name}";
string title = "File Context Menu Command";

コード・ウィンドウ

コード・ウィンドウのコンテキストメニュー上にコマンドを表示する方法を記載します。通常の *.cs*.cshtml ファイルのコードウィンドウ上でコンテキストメニューを表示する方法を記載します。

CSFileCodeWindowMenu

Vsct ファイル

GUID 値に guidSHLMainMenu 、id 値に IDM_VS_CTXT_CODEWIN を設定します。ASP.NET Core の cshtml ファイルのコンテキストメニューは、前述の GUID 値と id 値ではありませんでした。 Extensibility Tools を利用して、コンテキストメニューの GUID 値と id 値を取得し、GuidSymbol 要素に設定を加えています。

<CommandPlacements>
  <!-- Web File Code Window Context Menu -->
  <CommandPlacement guid="guidCodeWindowContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFFF">
    <Parent guid="guidCSHTMLContextMenu" id="CSHTMLContextMenu" />
  </CommandPlacement>
  <!-- Code Windows Context Menu -->
  <CommandPlacement guid="guidCodeWindowContextMenuCommandCmdSet" id="MyMenuGroup" priority="0xFFF">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
  </CommandPlacement>
</CommandPlacements>

<GuidSymbol name="guidCSHTMLContextMenu" value="{78F03954-2FB8-4087-8CE7-59D71710B3BB}">
  <IDSymbol value="1" name="CSHTMLContextMenu" />
</GuidSymbol>

コマンドの処理

コードウィンドウで表示されたコードを取得するには、EnvDTE.DTEActiveDocument プロパティを利用します。以下にサンプルを記載します。

private void Execute(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    EnvDTE.Document doc = _dte.ActiveDocument;
    string message = $"Active window is {doc.FullName}";
    string title = "Code Window Context Menu Command";

    // Show a message box
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

まとめ

  • Visual Studio のカスタムコマンドの表示位置は、Vsct ファイルで設定できる
    • Vsct ファイルの、CommandPlacement 要素内にコマンドの位置を定義した方がよい
    • Parent 要素の guid 属性、id 属性に適切な値を設定する
    • コンテキストメニューの GUID 値、id 値が分からない場合は、Extensibility Tools を利用してみよう
  • 動的にコンテキストメニューに表示するかどうかを判断する場合
    • CommandFlag 要素を利用する
    • OleMenuCommand クラスの BeforeQueryStatus イベントハンドラで表示・非表示の判定を行う
    • VsPackage の読み込みが遅延されないように、ProvideAutoLoad 属性を付与する

参考資料