Visual Studio 拡張機能から Visual Studio プロジェクトを弄ってみる
はじめに
この記事は、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
インターフェースを取得することができます。
以下に、これらのインターフェースの階層関係の図を引用します。
ソリューション・プロジェクトの階層関係と
EnvDTE
のSolution
・Project
との階層関係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
のインスタンスを取得することができます。
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
プロパティのツリー構造をたどって検索する必要があります。
ツリー構造をたどる必要があるため、以下の例のように、ProjectItems
や ProjectItem
から項目を検索するメソッドを用意しておくとよいでしょう。
// 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" というキーを渡すことで取得することができます。
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");
以下にプロジェクトにファイルを追加する例を示します。
ファイルをネストさせる
XAML ファイルとそのコードビハインドファイルのように、Visual Studio のソリューションエクスプローラー上で複数のファイルが親子関係で表示されている場合があります。
このようにファイルがネスト構造で 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");
以下のように、ファイルがネストされていることがわかります。
この方法を使って、子供のファイルを追加していくと、以下のスクリーンショットのように、C# のコードの子供に別のコードを追加することもできます。(役に立たないですが、画像ファイルも追加可能です。)
なお、このファイルのネスト構造が実現されるプロジェクトは、プロジェクトファイルでネスト構造がサポートされている場合のみです。例えば、.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);
以下のように、プロジェクト参照が追加されることがわかります。
特定のプロジェクトをスタートアッププロジェクトに設定する
特定のプロジェクトをスタートアッププロジェクトに設定するには、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;
以下のようにスタートアッププロジェクトを変更することができます。
出力 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 にメッセージを出力した様子を示します。
おわりに
最後まで読んでいただき、ありがとうございました。Visual Studio の拡張機能に関しては、ドキュメントやサンプルも少ないため、情報を集めるのに苦労することが多いため、この資料が、少しでも役に立てば幸いです。
明日は、@tomoriaki さんの拡張機能の紹介の記事とのことです。楽しみです。