tafuji's blog

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

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

Qiita より転載

はじめに

この記事は、Visual Studio Advent Calendar 2017 の 23日目の記事です。12月23日は、私の誕生日でして、誕生日の節目に何か記事を書いてみたいと思い、Advent Calendar にエントリーさせていただきました。よろしくお願いいたします。

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

サンプルコード

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

EnvDTE インターフェース

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

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

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

ソリューション・プロジェクトの階層関係と EnvDTESolutionProjectとの階層関係

Mappoing

Microsoft Visual Studio 2015 Unleashed, 3rd Edition (ISBN-13: 978-0-672-33736-9) より引用

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

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

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 プロパティで取得することができます。以下にサンプルコードを示します。

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 条件で絞り込むことができます。以下にサンプルコードを示します。

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 プロパティのツリー構造をたどって検索する必要があります。

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

// 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 から、その項目のフルパスを取得する必要がありますが、ProjectItemProperties プロパティに "FullPath" というキーを渡すことで取得することができます。

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

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

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

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

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

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

ファイルをネストさせる

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

NestedFile

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

<Compile Include="App.xaml.cs">
    <DependentUpon>App.xaml</DependentUpon>
    <SubType>Code</SubType>
</Compile>

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

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

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

// ターゲットの ProjectItems プロパティにファイルを追加する
target.ProjectItems.AddFromFileCopy("C:\temp\App.xaml.Debug.cs");

以下のように、ファイルがネストされていることがわかります。

NestFileResultGif

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

NestFile-Result

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

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

プロジェクト間の参照関係の設定は、VSLangProj 名前空間VSProject インターフェースの References プロパティに対して、AddProject メソッドを利用することで実現することができます。

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

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

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

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

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

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

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

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

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

おわりに

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

明日は、@tomoriaki さんの拡張機能の紹介の記事とのことです。楽しみです。