Visual Studio 2013

拡張機能による Visual Studio 2013 の拡張

Doug Erickson
Susan Norwood

Visual Studio の上位エディションにおける最も優れた機能の 1 つを、Visual Studio Community 2013 IDE ではだれもが無料で利用できるようになりました。それは、Visual Studio ギャラリーの拡張機能を使用できることです (拡張機能については、bit.ly/115mBzm を参照してください)。では、拡張機能とはどのようなものでしょうか。拡張機能とは、Visual Studio Community 2013 (https://www.visualstudio.com/products/visual-studio-community-vs からダウンロード可能) 自体や Visual Studio の機能を拡張できるプラグインです。拡張機能を使用すると、新たなタスクの実行、新機能の追加、コードのリファクタリング、さらには新しい言語のサポートを行うことができます。

マイクロソフトの開発者コミュニティからは、Visual Studio ギャラリーでさまざまな種類の拡張機能が提供されています。ご覧になるとわかるように、驚くほどさまざまな種類の拡張機能が提供されています。中には、Productivity Power Tools 2013 (bit.ly/1xE3EAT (英語) からダウンロード可能) のように、使い始めたら手放せなくなるツールもあるでしょう。こうした拡張機能は、Web からインストールできます。また、Visual Studio で [ツール] メニューの [拡張機能と更新プログラム] ウィンドウを開いて、オンライン拡張機能を検索することもできます。[オンライン] カテゴリを参照すれば、Visual Assist や ReSharper などの人気ツールが見つかります。「Code Recipe」(bit.ly/11nzi9Q) では、さらに多くの拡張機能を入手できます。

非常に優れたツールであっても、IDE で実行または自動化しようとしている特定のタスクに完全に適しているとは限りません。おそらく皆さんは、作業を簡単にするためにカスタマイズや調整を行った、独自の個人用スクリプトをお持ちでしょう。もしかしたら、ビルドが成功したかどうかビルド ディレクトリをチェックするツール、XML ファイルを変換するツール、または複雑なビルド処理の残骸をクリーンアップするツールをお持ちの方もいるでしょう。IDE の一部として、またはビルド プロセスの一部として、独自のツールを Visual Studio で実行してみるのはいかがでしょうか。Visual Studio 2013 Community と Visual Studio 2013 SDK を使用すれば、このようなツールをすぐに実現できます。

まずは、Visual Studio SDK をダウンロードします。Visual Studio SDK には、さまざまな拡張機能を作成するために必要な、すべてのライブラリ、ツール、およびプロジェクト テンプレートが用意されています。Visual Studio SDK をインストールすれば、準備完了です。

Visual Studio で実行するツールを作成する

Visual Studio SDK をインストールすれば、非常に簡単に独自のツールや実行可能なスクリプトを Visual Studio のメニューから実行できるようになります。この記事ではおなじみのメモ帳 (Notepad.exe) を例に取り上げますが、任意の実行可能ファイルを使用することも、独自のコードをコマンド ハンドラーに組み込むこともできます。

基本的な拡張機能は Visual Studio パッケージ バイナリとして用意されています。Visual Studio パッケージ テンプレートを表示するには、[新しいプロジェクト] ダイアログ ボックスで、[Visual Basic] の [機能拡張] ノード、[Visual C#] の [機能拡張] ノード、または [その他のプロジェクトの種類] の [機能拡張] ノードをクリックします。

この記事では、メモ帳を起動するシンプルな拡張機能の作成方法を紹介します。まず、[Visual C#] の [機能拡張] ノードにあるテンプレートを使用して、Visual C# の Visual Studio パッケージ プロジェクトを作成します。プロジェクトに StartNotepad という名前を付けて、D:\Code に配置します。この拡張機能の最終目標は、Visual Studio IDE のメニュー項目からメモ帳を起動することです。

テンプレートをダブルクリックすると、拡張機能の構成を支援するウィザードが起動します。まず、最初の 2 つのダイアログ ページでは、既定値をそのまま使用します。

次に、以下の手順を実行します。

  1. [Select VSPackage Options] (Visual Studio パッケージ オプションの選択) ページで、[Menu Command] (メニュー コマンド) チェック ボックスをオンにします。
  2. [Command Options] (コマンド オプション) ページで、[Command name] (コマンド名) ボックスに「Start Notepad」と入力し、[Command ID] (コマンド ID) ボックスに「cmdidStartNotepad」と入力します。
  3. [Select Test Project Options] (テスト プロジェクト オプションの選択) ページで、チェック ボックスを 2 つともオフにします。
  4. [Finish] (完了) をクリックします。

これで、パッケージ プロジェクトをビルドして実行できるようになります。プロジェクトが開いたら、デバッグを開始します (F5 キーを押すか、ツール バーの [開始] をクリックします)。Visual Studio Community 2013 の新しいインスタンスが起動します。起動が完了すると、IDE のタイトル バーに "スタートページ - Microsoft Visual Studio – 実験的なインスタンス" と表示されます ( 図 1 参照)。これが Visual Studio のテスト インスタンスです。テスト インスタンスは、Visual Studio の作業用インスタンスとは別に実行されます。そのため、問題が発生しても実際の開発環境に波及するおそれはありません。

Visual Studio の実験的なインスタンス
図 1 Visual Studio の実験的なインスタンス

"実験的なインスタンス" とは、開発者が自作の拡張機能をテストするために Visual Studio のサンドボックス インスタンスを起動したことを表す、凝った方法にすぎません。実験的なインスタンスには Visual Studio Community 2013 の機能がすべて備わっていますが、元の Visual Studio のインスタンスで行っている作業に悪影響が及ぶことはありません。今回のような単純な例では、この対策は過剰にも思えるでしょう。しかし、デザイン フレームワーク全体や相互に依存する一連の複雑なビルド ツールを構築する場合、構築中のフレームワークやツールを開発用と同じインスタンスで実行して開発中のコードを危険にさらすことは、お勧めしません。

実験的なインスタンスの [ツール] メニューで、[拡張機能と更新プログラム] ダイアログ ボックスを開きます。StartNotepad 拡張機能がこの画面 ( 図2 参照) に表示されます (Visual Studio の作業用インスタンスで [拡張機能と更新プログラム] を開いても、StartNotePad は表示されません)。

[拡張機能と更新プログラム] に表示されている StartNotepad
図 2 [拡張機能と更新プログラム] に表示されている StartNotepad

また、[ツール] メニューにも [Start Notepad] が表示されます ( 図 3 参照)。

[ツール] メニューに表示されている StartNotepad の新しいメニュー項目の確認
図 3 [ツール] メニューに表示されている StartNotepad の新しいメニュー項目の確認

ここで実験的なインスタンスの [ツール] メニューを展開すると、[Start Notepad] が表示されます。この時点では、[Start Notepad] をクリックしても "StartNotepad – Inside MSIT.StartNotepad.StartNotepadPackage.MenuItemCallback()" (StartNotepad – MSIT.StartNotepad.StartNotepadPackage.MenuItemCallback() 内) というメッセージ ボックスが表示されるだけです。実際にこのコマンドからメモ帳を起動する方法については、次のセクションで説明します。

メニュー コマンドを作成する

デバッグを停止して、Visual Studio の作業用インスタンスに戻ります。StartNotepadPackage.cs ファイルを開きます。このファイルには、Package クラスから派生したクラスが含まれています。このクラスがすべての Visual Studio パッケージ拡張機能の出発点です。このクラスの Initialize メソッドで、コマンドを作成しています。

// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService mcs =
  GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if ( null != mcs )
{
  // Create the command for the menu item.
  CommandID menuCommandID = new CommandID(GuidList.guidStartNotepadCmdSet,
    (int)PkgCmdIDList.cmdidStartNotepad);
  MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );
  mcs.AddCommand( menuItem );
}

コードの詳細については、今は気にする必要はありません。注目していただきたい点は、メニュー コマンドのインスタンスを作成する方法です。Visual Studio パッケージ拡張機能のメニューやその他の UI は、コード コメントに表示されているように、.vsct ファイルで定義します。

コマンド ハンドラーは、MenuItemCallback という名前です (同じ StartNotepadPackage クラスに含まれています)。既存のメソッドを削除して、次のコードを追加します。

private void MenuItemCallback(object sender, EventArgs e)
{
  Process proc = new Process();
  proc.StartInfo.FileName = "notepad.exe";
  proc.Start();
}

では、試してみましょう。プロジェクトのデバッグを開始し、[ツール] メニューの [Start Notepad] をクリックすると、メモ帳のインスタンスが表示されます。実際には、[Start Notepad] をクリックするたびにメモ帳の新しいインスタンスが表示されます。

System.Diagnostics.Process クラスのインスタンスを使用すると、メモ帳だけでなく、どのような実行可能ファイルでも実行できます。たとえば、calc.exe で試してみてください。

インターフェイスを定義する

今度は .vsct ファイルを使用して、拡張機能の UI を定義しましょう。StartNotepad.vsct ファイルをご覧ください。このファイルには、拡張機能で使用する Visual Studio UI の定義がすべて含まれているので便利です。また、XML ファイルなので山かっこに注意してください。

<Groups> ブロックをご覧ください。すべてのメニュー コマンドは、それぞれ 1 つの Group に属している必要があります。Group は、コマンドの配置場所を Visual Studio に指定する要素です。今回は、[Start Notepad] コマンドを [ツール] メニューに配置していて、[ツール] メニューの親はメイン メニューです。

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

詳細についてはまだそれほど気にする必要はありません。このコードの目的は単に、重要な要素の場所を示すことです。コマンド自体は <Buttons> ブロック内で次のように定義します。

<Button guid="guidStartNotepadCmdSet" id="cmdidStartNotepad"
  priority="0x0100" type="Button">
  <Parent guid="guidStartNotepadCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPic1" />
  <Strings>
    <ButtonText>Start Notepad</ButtonText>
  </Strings>
</Button>

ご覧のとおり、コマンド ボタンの定義には Group の GUID と同じ GUID (guidStartNotepadCmdSet) と、パッケージ ウィザードで指定した ID (cmdidStartNotepad) を使用します。コマンドの親は、前述の Group です。コマンドにはアイコンとテキストを設定していて、設定しているアイコンは、ソリューションに含まれている既定のアイコンの 1 つです。priority は、グループ内にコマンドが複数ある場合にこのボタンを配置する、メニュー上の場所を指定します。グループとボタンの GUID と ID は、<Symbols> ブロック内で次のように定義します。

<!-- This is the package GUID. -->
<GuidSymbol name="guidStartNotepadPkg"
  value="{18311db7-ca0f-419c-82b0-5aa14c8b541a}" />
<!-- This is the GUID used to group the menu commands together -->
<GuidSymbol name="guidStartNotepadCmdSet"
  value="{b0692a6d-a8cc-4b53-8b2d-17508c87f1ab}">
  <IDSymbol name="MyMenuGroup" value="0x1020" />
  <IDSymbol name="cmdidStartNotepad" value="0x0100" />
</GuidSymbol>

ビットマップも、<Symbols> ブロックで定義します。

<GuidSymbol name="guidImages" 
  value="{b8b810ad-5210-4f35-a491-c3464a612cb6}" >
  <IDSymbol name="bmpPic1" value="1" />
  <IDSymbol name="bmpPic2" value="2" />
  <IDSymbol name="bmpPicSearch" value="3" />
  <IDSymbol name="bmpPicX" value="4" />
  <IDSymbol name="bmpPicArrows" value="5" />
  <IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>

ボタンのアイコンを、ここで定義している他のいずれかのアイコンに変更してみましょう。メモ帳に格別適しているアイコンはないので、bmpPicStrikethrough を選択します。これを選ぶと、拡張機能の構造について別の面がわかるからです。各アイコンは <Symbols> ブロックで定義していますが、<Bitmaps> ブロックの usedList 属性でも列挙する必要があります。

<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1,
  bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>

bmpPicStrikethrough は定義済みですが、ご覧のとおり列挙には含まれていません。アイコンをこのビットマップに変更しても、メニューには表示されません。そこで、bmpPicStrikethrough を列挙に追加します。

<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1,
  bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough"/>

これで、メニュー ボタンのアイコンを bmpPicStrikethrough に変更できるようになりました。

<Button guid="guidStartNotepadCmdSet" id="cmdidStartNotepad"
  priority="0x0100" type="Button">
  <Parent guid="guidStartNotepadCmdSet" id="MyMenuGroup" />
  <Icon guid="guidImages" id="bmpPicStrikethrough" />
  <Strings>
    <ButtonText>Start Notepad</ButtonText>
  </Strings>
</Button>

では、デバッグしてみましょう。実験的なインスタンスが起動すると、[ツール] メニューに図 4 のようなコマンドが表示されます。

メモ帳を起動するコマンド
図 4 メモ帳を起動するコマンド

今度は、[Start Notepad] にキーボード ショートカットを追加しましょう。この例では、Ctrl + 1 を使用します。キーボード ショートカットを追加する場合は、標準的なキーの組み合わせと競合しないように、使用していないキーの組み合わせを必ず選んでください。キーボード ショートカットの追加は、.vsct ファイル内で完結します。キーボード ショートカットは、.vsct ファイルでは KeyBinding と呼ばれます。以下のブロックを .vsct ファイルに追加します。

<KeyBindings>
  <KeyBinding guid="guidStartNotepadCmdSet" 
    id="cmdidStartNotepad"    
    editor="guidVSStd97" key1="1" mod1="CONTROL"/>
</KeyBindings>

key 1 属性は通常のキーを宣言し、mod1 属性は修飾キーまたはアクセラレータ キー (一般的には CtrlAlt、または Shift) を宣言します。2 つ目の修飾キーとして mod2="ALT" を追加すると、キーボード ショートカットは Ctrl + Alt + 1 になります。今のところは Ctrl + 1 のままにしますが、.vsct ファイルから簡単にカスタマイズを開始できることに留意してください。これで、デバッグを開始できます。再びパッケージを起動します。実験的なインスタンスが起動したら、Ctrl + 1 を押すと、メモ帳のインスタンスが表示されます。

UI の応答性を維持する

現状の MenuItemCallback メソッドは、UI スレッドをブロックしません。MenuItemCallback メソッドの処理中も、[Start Notepad] をクリックする (または Ctrl キーを押しながら 1 キーを押す) とメモ帳を起動できます。また、Visual Studio IDE で作業することもできます。[メモ帳] ウィンドウを移動する、メニューの他のコマンドをクリッするなどの操作も可能です。仮に、MenuItemCallback を終了する前にツールの処理を完了しなければならないとしましょう。つまり、Process.WaitForExit を呼び出す必要があるとします。

残念ながら、Visual Studio の UI スレッドで WaitForExit を呼び出すと、この時点では MenuItemCallback が処理中なので、Visual Studeio の UI 全体がフリーズします。この状況を実際に確認しましょう。MenuItemCallback メソッド内で、WaitForExit を呼び出します。

private void MenuItemCallback(object sender, EventArgs e)
{
  Process proc = new Process();
  proc.StartInfo.FileName = "notepad.exe";
  proc.Start();
  proc.WaitForExit();
}

デバッグを開始し、実験的なインスタンスが起動したら、Ctrl キーを押しながら 1 キーを押します。メモ帳のインスタンスが表示されます。ここで、実験的なインスタンスの [Visual Studio] ウィンドウを移動してみると、まったく移動できません。Visual Studio の UI でも他の操作を実行できません。"Microsoft Visual Studio がビジー状態です" というポップアップが表示される場合もあります。明らかに修正が必要です。

さいわい、修正は簡単です。完了に時間がかかるツールやプロセスがある場合は、そのツールやプロセスを MenuItemCallback のタスクでラップします。

private void MenuItemCallback(object sender, EventArgs e)
{
  ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
  {
    Process proc = new Process();
    proc.StartInfo.FileName = "notepad.exe";
    proc.Start();
    await proc;
  });
}

これで、独自のツールを実行しながら Visual Studio で作業できるようになりました。

実験を削除する

複数の拡張機能を開発している場合、またはさまざまなバージョンの拡張機能コードによる結果を調べているだけの場合でも、実験的なインスタンスが正常に機能しなくなるときがあります。このような場合は、リセット スクリプトを実行する必要があります。

このスクリプトは Reset the Visual Studio 2013 Experimental Instance という名前で、Visual Studio 2013 SDK に付属しています。このスクリプトを使用すると、自作の拡張機能に対するすべての参照が実験的なインスタンスから削除されるので、ゼロからやり直すことができます。このスクリプトは、次の 2 とおりのうちいずれかの方法で実行できます。

  • デスクトップで、[Reset the Visual Studio 2013 Experimental Instance] を見つけます。
  • コマンド ラインで、次のコマンドを実行します。
<VSSDK installation>\VisualStudioIntegration\Tools\Bin\
        CreateExpInstance.exe /Reset /VSInstance=12.0 
       /RootSuffix=Exp && PAUSE

拡張機能を配置する

自分で作成したツール拡張機能が期待どおりに動作するようになったら、友人や同僚と共有することを検討しましょう。相手が Visual Studio 2013 をインストールしている場合は簡単です。ビルドした .vsix ファイルを送信するだけで共有できます。その際は、必ずリリース モードでビルドしてください。

今回の拡張機能の .vsix ファイルは、StartNotepad の bin ディレクトリにあります。リリース モードでビルドした場合は、D:\Code\StartNotepad\StartNotepad\bin\Release\StartNotepad.vsix です。

拡張機能をインストールするには、まず Visual Studio の開いているインスタンスをすべて閉じる必要があります。次に、.vsix ファイルをダブルクリックすると、VSIX インストーラーが起動します。拡張機能のファイルは %LocalAppData%\Microsoft\VisualStudio\12.0\Extensions ディレクトリにコピーされます。

Visual Studio を再び起動すると、[ツール] メニューの [拡張機能と更新プログラム] に StartNotepad 拡張機能が表示されます。[拡張機能と更新プログラム] から、拡張機能のアンインストールや無効化も可能です。

拡張機能を作成する方法を身に付けると、Visual Studio のエクスペリエンスを思いどおりにできます。また、生産性を向上する優れた機能や機能強化をコミュニティで共有することもできます。マイクロソフトは、開発者が自作の拡張機能を公開することを歓迎しています。

まとめ

この記事で説明した内容は、Visual Studio の拡張機能で実現できることのごく一部にすぎません。Visual Studio の拡張機能全般に関する詳細については、「アプリケーションやサービスを Visual Studio に統合する」(bit.ly/1zoIt59、英語) を参照してください。

いっそう詳しい解説については、「Visual Studio の拡張概要」セクション (bit.ly/1xWoA4k、英語) で紹介している MSDN ライブラリ記事を参照してください。この情報の応用として使用できる優れたサンプル コードについては、MSDN サンプル ギャラリー (bit.ly/1xWp5eD、英語) でサンプル コード集を参照してください。拡張機能をビルドして共有すれば、Visual Studio の環境を自由にカスタマイズできるようになります。


Susan Norwood は、マイクロソフトの社員として、主に Visual Studio SDK に関する記事を執筆しています。また、独自ツールを Visual Studio に組み込もうとする多くの開発者を支援してきました。

Doug Erickson は、マイクロソフトで開発者兼テクニカル ライターを 13 年間務めてきました。彼は、開発者があらゆるプラットフォーム向けのアプリケーションやゲームを開発するために Visual Studio Community を使用して行う活動を、楽しみにしています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Anthony Cangialosi と Ryan Molden に心より感謝いたします。