Visual Studio 拡張機能から Visual Studio プロジェクトを弄ってみる

## はじめに

この記事は、[Visual Studio Advent Calendar 2017](https://qiita.com/advent-calendar/2017/vs) の 23日目の記事です。12月23日は、私の誕生日でして、誕生日の節目に何か記事を書いてみたいと思い、Advent Calendar にエントリーさせていただきました。よろしくお願いいたします。

記事の内容ですが、Visual Studio の拡張機能から Visual Studio のソリューションやプロジェクトを操作（ファイルの追加、参照関係の設定など）を行うための Tips となります。

## サンプルコード

Visual Studio の拡張機能から Visual Studio のプロジェクトを操作するサンプルです。GitHub に公開しています。

- [EnvDteSample](https://github.com/tafuji/EnvDteSample)

## EnvDTE インターフェース

Visual Studio では、ソリューションやプロジェクトを操作するための```EnvDTE.DTE``` インタフェースが提供されており、このインタフェース経由で Visual Studio のソリューションにプロジェクトへのプロジェクトの追加、プロジェクトへのアイテム（コード、フォルダなど）を追加することができます。

- [EnvDTE.DTE Interface](https://docs.microsoft.com/en-us/dotnet/api/envdte.dte?view=visualstudiosdk-2017)

この ```EnvDTE.DTE``` インターフェース経由で、ソリューションを操作するためのインターフェースである ```Solution``` インターフェース、プロジェクトを操作するためのインターフェースである ```Project``` インターフェース、プロジェクト内の項目を表す ```ProjectItem``` インターフェースを取得することができます。

以下に、これらのインターフェースの階層関係の図を引用します。

>ソリューション・プロジェクトの階層関係と ```EnvDTE``` の ```Solution```・```Project```との階層関係
>
> ![Mappoing](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/4ce82e281f43efd454d7a5e247c440f8ed3791aa/Mapping-solution-project-hierarchy.png)
>
>Microsoft Visual Studio 2015 Unleashed, 3rd Edition (ISBN-13: 978-0-672-33736-9) より引用
>

では、この ```Solution```、```Project``` オブジェクトを Visual Studio の拡張機能からどのように取得するかですが、```Microsoft.VisualStudio.Shell.Package``` クラスのインスタンスから取得することができます。

例えば、Visual Studio の拡張機能でカスタムコマンドを作成したときに、以下のようなコードブロックがありますが、```Microsoft.VisualStudio.Shell.Package``` を ```IServiceProvider``` にキャストして、```GetService``` メソッドから ```DTE``` のインスタンスを取得することができます。

```csharp
private IServiceProvider ServiceProvider
{
    get
    {
        return this._package; // Microsoft.VisualStudio.Shell.Package
    }
}

// Get the DTE Interface
var dte = ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
```

## ソリューション・プロジェクトを取得する

ソリューションは、```EnvDTE.DTE``` インタフェースを取得し、```Solution``` プロパティから取得することができます。プロジェクトは、```Solution``` プロパティの ```Projects``` プロパティで取得することができます。以下にサンプルコードを示します。

```csharp
var dte = ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;

Solution solution = dte.Solution;
Projects projects = solution.Projects;
foreach (Project project in projects)
{
    var projectName = project.Name; // Project name
    var projectPath = project.FullName;
}
```

## プロジェクト名でプロジェクトを取得する

特定のプロジェクトを取得して、ファイルを追加するなどの処理を行いたい場合があると思います。そのような場合は、LINQ を用いて取得することができます。```Solution``` インターフェースの ```Projects``` コレクションを ```EnvDTE.Project``` にキャストして、Where 条件で絞り込むことができます。以下にサンプルコードを示します。

```csharp
Solution solution = dte.Solution;
Project project = solution.Projects.Cast<Project>().Where(p => p.Name == "SampleClassLibrary").Select(p => p).FirstOrDefault();
```

## プロジェクトの項目を検索する

Visual Studio のプロジェクトからプロジェクトの項目（例えば、C# のソースコード）を検索した場合を考えます。```EnvDTE.Project``` インターフェースの ```ProjectItems``` プロパティのツリー構造をたどって検索する必要があります。

ツリー構造をたどる必要があるため、以下の例のように、```ProjectItems``` や ```ProjectItem``` から項目を検索するメソッドを用意しておくとよいでしょう。

```csharp

// ProjectItems からプロジェクト項目（フルパス指定）で ProjectItem を検索する
private ProjectItem FindByProjectItemByName(ProjectItems items, string fullPath)
{
    ProjectItem result = null;
    foreach(ProjectItem item in items)
    {
        result = FindByProjectItemByName(item, fullPath);
        if(result == null)
        {
            result = FindByProjectItemByName(item.ProjectItems, fullPath);
        }
        if (result != null) return result;
    }
    return result;
}

// ProjectItem からプロジェクト項目（フルパス指定）で ProjectItem を検索する
private ProjectItem FindByProjectItemByName(ProjectItem item, string fullPath)
{
    ProjectItem result = null;
    if(item != null)
    {
        // プロジェクト項目のフルパスを取得する
        var path = (string)item.Properties.Item("FullPath").Value;
        if (fullPath.Equals(path, StringComparison.OrdinalIgnoreCase))
        {
            result = item;
        }
        else
        {
            result = FindByProjectItemByName(item.ProjectItems, fullPath);
        }
    }
    return result;
}
```

プロジェクト項目の存在チェックに ```ProjectItem``` から、その項目のフルパスを取得する必要がありますが、```ProjectItem``` の ```Properties``` プロパティに "FullPath" というキーを渡すことで取得することができます。

```csharp
var path = (string)item.Properties.Item("FullPath").Value;
```

## プロジェクトにファイルを追加する

プロジェクトへのファイルを追加は、```EnvDTE.ProjectItems``` インターフェースの ```AddFromFile``` あるいは、 ```AddFromFileCopy``` メソッドにファイルのフルパスを渡すことで実現できます。二つのメソッドの挙動の違いは、以下の通りです。

- ```AddFromFile```
  - 引数で渡したファイルをそのままプロジェクトに追加する
  - 例えば、C:\temp\Sample.cs を追加した場合は、そのパスのファイルをプロジェクトが参照する形になる
- ```AddFromFileCopy```
  - 引数で渡したファイルをコピーして、プロジェクトフォルダの直下に追加する

サンプルコードを以下に示します。

```csharp
var dte = ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;

Solution solution = dte.Solution;
Project project = solution.Projects.Cast<Project>().Where(p => p.Name == item.Name).Select(p => p).FirstOrDefault();
project.ProjectItems.AddFromFileCopy("C:\temp\Sample.cs");

```

以下にプロジェクトにファイルを追加する例を示します。

![AddFile](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/7413505fc29ba7d1f51b89a93b46c18399fdd10d/AddFileResult.gif)

## ファイルをネストさせる

XAML ファイルとそのコードビハインドファイルのように、Visual Studio のソリューションエクスプローラー上で複数のファイルが親子関係で表示されている場合があります。

![NestedFile](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/fbb4ef579ebf12cdbcd69fea775e737a9ea29576/NestedFile.png)

このようにファイルがネスト構造で Visual Studio 上で表示されるのは、Visual Studio のプロジェクトファイル（csproj）で以下のようにファイルの親子関係が定義されているためです。

```xml
<Compile Include="App.xaml.cs">
    <DependentUpon>App.xaml</DependentUpon>
    <SubType>Code</SubType>
</Compile>
```
このようなソリューションエクスプローラー上で、親子関係で表示されるように、プロジェクトにファイルを追加するには、 ```EnvDTE.ProjectItem``` インターフェースの ```ProjectItems``` プロパティに子供のファイルを追加する必要があります。ファイルの追加は、```EnvDTE.ProjectItems``` インターフェースの ```AddFromFile``` あるいは、 ```AddFromFileCopy``` メソッドで実現できます。

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

```csharp
// プロジェクト項目をフルパスで取得する
ProjectItem target = FindProjectItemByName(item.FullPath);

// ターゲットの ProjectItems プロパティにファイルを追加する
target.ProjectItems.AddFromFileCopy("C:\temp\App.xaml.Debug.cs");
```
以下のように、ファイルがネストされていることがわかります。

![NestFileResultGif](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/c9c151c879301827b7d58e899e48552ad3ac82e9/NestFileResult-Gif.gif)

この方法を使って、子供のファイルを追加していくと、以下のスクリーンショットのように、C# のコードの子供に別のコードを追加することもできます。（役に立たないですが、画像ファイルも追加可能です。）

![NestFile-Result](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/328d7e8937f359be849b45b3266ad026bc6ff2f3/NestedFile-Result.png)

なお、このファイルのネスト構造が実現されるプロジェクトは、プロジェクトファイルでネスト構造がサポートされている場合のみです。例えば、.NET Standard のクラスライブラリプロジェクトで、この操作を行ってもファイルのネストは行われません。

## プロジェクト間の参照関係を設定する

プロジェクト間の参照関係の設定は、[```VSLangProj```](https://docs.microsoft.com/en-us/dotnet/api/vslangproj.vsproject?view=visualstudiosdk-2017) 名前空間の ```VSProject``` インターフェースの ```References``` プロパティに対して、```AddProject``` メソッドを利用することで実現することができます。

```csharp
using VSLangProj;

var dte = ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;

Solution solution = dte.Solution;
Project parentProject; // 参照するプロジェクト
Project project; // 参照されるプロジェクト

// プロジェクト取得処理

((VSProject)parentProject.Object).References.AddProject(project);
```

以下のように、プロジェクト参照が追加されることがわかります。

![ProjectReference](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/9bbf4b7b3e5e9bd24616c2935d04dad29957fea2/AddReference.gif)

## 特定のプロジェクトをスタートアッププロジェクトに設定する

特定のプロジェクトをスタートアッププロジェクトに設定するには、```Solution``` インターフェースの ```Properties``` プロパティの "StartupProject" の値にプロジェクトの名前を指定することで実現することができます。

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

```csharp
var dte = ServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;

Solution solution = dte.Solution;
Project project = solution.Projects.Cast<Project>().Where(p => p.Name == item.Name).Select(p => p).FirstOrDefault();
solution.Properties.Item("StartupProject").Value = project.Name;
```

以下のようにスタートアッププロジェクトを変更することができます。

![StartupProject](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/13cbbd3281afef1011a1812f732de422ecb19139/SetStartupProject-Gif.gif)

## 出力 Window にメッセージを表示する

Visual Studio の出力 Window（ビルドの結果が表示される Window など）にメッセージを出力する方法について記載します。```DTE2``` インタフェースの ```ToolWindows``` 経由で ```OutputWindow``` (出力 Window)を取得し、そのプロパティの ```OutputWindowPanes``` に対して、タイトルを設定することで出力 Window を作成することができます。作成した出力 Window に対して、メッセージを出力するには、```OutputString``` メソッドを利用します。

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

```csharp
var dte = ServiceProvider.GetService(typeof(SDTE)) as EnvDTE.DTE;
OutputWindowPanes panes = ((DTE2)dte).ToolWindows.OutputWindow.OutputWindowPanes;
OutputWindowPane outputPane = null;
try
{
    outputPane = panes.Item("Sample Output Window");
}
catch (ArgumentException)
{
    panes.Add("Sample Output Window");
    outputPane = panes.Item("Sample Output Window");
}

outputPane.OutputString("出力 Windows に表示するメッセージ");
```

以下に、カスタムの出力 Window にメッセージを出力した様子を示します。

![OutputWindow](https://gist.githubusercontent.com/tafuji/86d8561bc27b912d5134a06788d28f57/raw/202ee3e5df9cce58e5158fa8ff2bb3f61628723b/OutputWindow-ResultGif.gif)

## おわりに

最後まで読んでいただき、ありがとうございました。Visual Studio の拡張機能に関しては、ドキュメントやサンプルも少ないため、情報を集めるのに苦労することが多いため、この資料が、少しでも役に立てば幸いです。

明日は、[@tomoriaki](https://qiita.com/tomoriaki) さんの拡張機能の紹介の記事とのことです。楽しみです。