Azureのテンプレートプロジェクトを作成するときにマルチプロジェクトなテンプレートが必要だったのでやり方を記述しておきます。
ポイントは
- プロジェクトのGUIDの参照方法
- プロジェクト名の参照方法
です。
方法は、IWizardを実装してGUIDやプロジェクト名を渡すだけです。
だけなのですが、そこにはちょっとしたテクニックが必要です。
この記事では、Azureプロジェクトをテンプレートにしたいと思います。
以下のようなプロジェクト構成を複数作りたい場合を想定して、テンプレートで作成したいと思います。
AppSample
の部分はユーザが入力した値になります。
├── AppSample
│ ├── AppSample-Cloud
│ │ ├── AppSample-Cloud.ccproj
│ │ ├── AppSample-Cloud.ccproj.user
│ │ ├── AppSample-CodeContent
│ │ ├── ServiceConfiguration.Local.cscfg
│ │ ├── ServiceDefinition.csdef
│ ├── AppSample-Code
│ │ ├── AppSample-Code.csproj
│ │ ├── app.config
│ │ └── packages.config
│ │ ├── Dto
│ │ ├── Properties
│ │ ├── WorkerRole.cs
│ └── AppSample-Tests
│ ├── App.config
│ ├── AppSample-Tests.csproj
│ ├── Properties
│ ├── Sample.feature
│ ├── Sample.feature.cs
│ ├── SampleSteps.cs
│ └── packages.config
├── AppSample.sln
└── AppSample.v12.suo
-Cloud
はクラウドプロジェクト(Azure構成ファイルなど)、-Code
はプログラムコードプロジェクト(WorkerRoleプロジェクト)、-Tests
はテストプロジェクト(Specflowなど)が入ります。
準備
Visual Studio SDKをインストールします。
http://www.microsoft.com/en-us/download/details.aspx?id=40758
Azureプロジェクトなので、Azure SDKもインストールします。
マルチプロジェクトテンプレート
以下のようなプロジェクト構成を作ります。
.
├── AppSample-Template
│ ├── ProjectTemplate-Cloud
│ │ ├── $safeprojectname$.ccproj
│ │ ├── Properties
│ │ ├── ServiceConfiguration.Local.cscf
│ │ ├── ServiceDefinition.csdefg
│ │ ├── diagnostics.wadcfgx
│ │ └── MyTemplate.vstemplate
│ ├── ProjectTemplate-Code
│ │ ├── $safeprojectname$.csproj
│ │ ├── AssemblyInfo.cs
│ │ ├── Dto
│ │ ├── Properties
│ │ ├── WorkerRole.cs
│ │ ├── app.config
│ │ ├── packages.config
│ │ └── MyTemplate.vstemplate
│ ├── ProjectTemplate-Tests
│ │ ├── $safeprojectname$.csproj
│ │ ├── App.config
│ │ ├── AssemblyInfo.cs
│ │ ├── Properties
│ │ ├── Sample.feature
│ │ ├── Sample.feature.cs
│ │ ├── SampleSteps.cs
│ │ ├── packages.config
│ │ └── MyTemplate.vstemplate
│ ├── __TemplateIcon.png
│ ├── AppSample-Template.csproj
│ └── AppSample-Template.vstemplate
└── AppSample-Template.sln
ProjectTemplate-Cloud、ProjectTemplate-Code、ProjectTemplate-Testsがテンプレート本体になります。
テンプレート本体に関しては後述します。
VisualStudioでテンプレートプロジェクトを新規作成
「新規作成」 → 「プロジェクト」 で、テンプレートの「機能拡張」→「C# Project Template」を選択します。(Visual Studio SDKを入れておくと表示されます。)
マルチプロジェクト用のvstemplate
AppSample-Template.vstemplate
以外のファイルはマルチプロジェクトなテンプレートのルートには必要ないので削除します。
<VSTemplate>のType属性をProjectGroup
にするのがマルチプロジェクトにするポイントです。
<?xml version="1.0" encoding="utf-8"?>
<VSTemplate Version="2.0.0" Type="ProjectGroup" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
次に<TemplateContent>にテンプレート本体のvstemplateを記述します。
ここで記述した$myprojectnameroot$
は、VSのテンプレートパラメータではありません。
後述するIWizardの実装で自分で設定するカスタムパラーメータになります。
<TemplateContent>
<ProjectCollection>
<ProjectTemplateLink ProjectName="$myprojectnameroot$-Cloud">
ProjectTemplate-Cloud\MyTemplate.vstemplate
</ProjectTemplateLink>
<ProjectTemplateLink ProjectName="$myprojectnameroot$-Code">
ProjectTemplate-Code\MyTemplate.vstemplate
</ProjectTemplateLink>
<ProjectTemplateLink ProjectName="$myprojectnameroot$-Tests">
ProjectTemplate-Tests\MyTemplate.vstemplate
</ProjectTemplateLink>
</ProjectCollection>
</TemplateContent>
また、これから作るIWizardの設定も記述しておきます。
PublicKeyToken
の値はCustomWizard.dllを作り、署名後に調べて記述します。
<FullClassName>は作成するIWizardの実装クラスを記述します。
<WizardExtension>
<Assembly>CustomWizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=ここは実際の値を設定する</Assembly>
<FullClassName>CustomWizard.CustomWizard</FullClassName>
</WizardExtension>
IWizardの実装
ソリューションで右クリックして「追加」 → 「新しいプロジェクト」 → 「Visual C#」 → 「クラスライブラリ」で、今回はCustomWizard
というプロジェクト名で作ります。
また、CustomWizard.cs
クラスも作成します。
以下の参照を追加します。
- EnvDTE.dll
- Microsoft.VisualStudio.TemplateWizardInterface.dll
- System.Windows.Forms.dll
「参照設定」で「参照の追加」→「アセンブリ」で上記を検索して追加してください。
class CustomWizard : IWizard
を実装していきますが、ポイントは以下の部分だけです。
public static Dictionary<string, string> globalDictionary;
public void RunStarted(object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind, object[] customParams)
{
var userInput = replacementsDictionary["$safeprojectname$"];
replacementsDictionary.Add("$myprojectnameroot$", userInput);
Guid g = Guid.NewGuid();
replacementsDictionary.Add("$codeguidroot$", g.ToString());
globalDictionary = new Dictionary<string, string>();
globalDictionary.Add("$myprojectnameroot$", replacementsDictionary["$myprojectnameroot$"]);
globalDictionary.Add("$codeguidroot$", replacementsDictionary["$codeguidroot$"]);
}
もう一つクラスを追加してから説明します。
・・・
public void RunStarted(object automationObject,Dictionary<string, string> replacementsDictionary,WizardRunKind runKind, object[] customParams)
{
replacementsDictionary.Add("$myprojectname$",CustomWizard.globalDictionary["$myprojectnameroot$"]);
replacementsDictionary.Add("$codeguid$",CustomWizard.globalDictionary["$codeguidroot$"]);
・・・
単純にIWizardを実装してカスタムパラメータを渡してもルートのAppSample-Template.vstemplate
でしかカスタムパラメータの値を参照できません。
つまり、今回の場合は、ProjectTemplate-Cloud
,ProjectTemplate-Code
,ProjectTemplate-Tests
でIWizardで設定した値が利用できないので、意味がありません。
それを解決するために上記のように二つのクラスを用意し、CutomWizardChildをそれぞれのプロジェクト(ProjectTemplate-Cloudなど)の.vstemplate
に指定することで、ユーザが入力した値をテンプレートに反映させることができます。
(CutomWizardChildにはstatic変数経由で値を引き継いで設定しています。)
<WizardExtension>
<Assembly>CustomWizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=ここは実際の値を設定する</Assembly>
<FullClassName>CustomWizard.CustomWizardChild</FullClassName>
</WizardExtension>
前に「マルチプロジェクト用のvstemplate」で記述したAppSample-Template.vstemplate
の$myprojectnameroot$
は、このCustomWizardクラスで設定した値だったわけです。
そして、CutomWizardChildクラスで設定した値が重要になります。
実装プロジェクト(-Code)のguidを-Cloudや-Testsの参照で利用しています。
また、プロジェクト作成したときの入力値($myprojectname$)を利用して自身のプロジェクト名($safeprojectname$=$myprojectnameroot$-Cloudなど)以外を設定できるようにしています。
・・・
<ProjectGuid>{$codeguid$}</ProjectGuid>
・・・
・・・
<ItemGroup>
<ProjectReference Include="..\$myprojectname$-Code\$myprojectname$-Code.csproj">
<Name>$myprojectname$-Code</Name>
<Project>$codeguid$</Project>
<Private>True</Private>
<RoleType>Worker</RoleType>
<RoleName>$myprojectname$-Code</RoleName>
<UpdateDiagnosticsConnectionStringOnPublish>True</UpdateDiagnosticsConnectionStringOnPublish>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="$myprojectname$-CodeContent\" />
</ItemGroup>
<ItemGroup>
<DiagnosticsConfiguration Include="$myprojectname$-CodeContent\diagnostics.wadcfgx" />
</ItemGroup>
・・・
・・・
<ProjectReference Include="..\$myprojectname$-Code\$myprojectname$-Code.csproj">
<Project>{$codeguid$}</Project>
<Name>$myprojectname$-Code</Name>
</ProjectReference>
・・・
CustomWizardを署名する
ここに書いてある内容と同じですが一応書いておきます。
IWizard を実装するアセンブリは、厳密な名前で署名して、グローバル アセンブリ キャッシュにインストールする必要があるそうです。
1.ソリューション エクスプローラーでCustomWizardプロジェクトで右クリックで開き、[プロパティ] を選択します。
2.[署名] タブを選択します。
3.[アセンブリの署名] ボックスを選択します。
4.[厳密な名前のキー ファイルを選択してください] ボックスで [<参照>] をクリックし、キー ファイルに移動します。 新しいキー ファイルを作成するには、[<新規作成>] を選択し、[厳密な名前キーの作成] ダイアログ ボックスでその名前を入力します。
PublicKeyTokenの値を設定する
まずは、CustomWizardプロジェクトをbuildしときます。
CustomWizard\bin\Debug\CustomWizard.dll
にできます。
Sn.exe
でdllのPublicKeyTokenを調べられます。
これはWindows SDKに含まれています。
自分の環境では以下にありました。
C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools\x64
これをVisual StudioツールにあるVisual Studioコマンドプロンプトで実行します。
C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts
sn -T CustomWizard.dll
上記で取得した値をすべての.vstemplate
の<WizardExtension>のPublicKeyTokenに設定します。
テンプレートプロジェクト作成
あとはテンプレートを作成するだけです。
まずは、テンプレートの元になるプロジェクトを作成してビルドが通る状態にしておくと簡単です。
今回はC#のテンプレートからAzureクラウドサービスを選択して、WorkerRoleを選択しておきます。
プロジェクトのエクスポート
先ほど作成したビルドの通るプロジェクトをVisual Studioの機能でテンプレートにします。
「ファイル」 → 「プロジェクトのテンプレートを作成」で画像のウィザードが立ち上がるので、
「プロジェクト テンプレート」を選びます。
「テンプレートを自動的にvisual studioにインポートする」は必要ないので外しときます。
SampleApp-Cloud(Azureクラウドサービス)、SampleApp-Code(WorkerRoleProject)、SampleApp-Testsをそれぞれ別々にテンプレートを作成します。
完了すると出力フォルダが開くので、その中のzipを解凍します。
エクスポート機能を使わないで手動で作成する場合は、「新規作成」 → 「プロジェクト」 で、テンプレートの「機能拡張」→「C# Project Template」を選択します。(Visual Studio SDKを入れておくと表示されます。)
テンプレートパラメーター
zipにあった<プロジェクト名>.ccproj
(or .csproj
) ファイルを、テンプレート展開されたときのプロジェクト名にしたいです。
そのため、テンプレートパラメータ名を使って $safeprojectname$.ccproj
に変更します。
ファイル内も変更します。
テンプレートパラメータについては以下を参照してください。
http://msdn.microsoft.com/ja-jp/library/eehb4faa.aspx
同じようにソースや構成ファイルなどにテンプレートパラメータを入れて、プロジェクト生成時に書き換えてほしい部分を記述しておきます。
(元々のプロジェクト名を$safeprojectname$に一括置換します。)
例えば、クラウドプロジェクトの場合は、以下のファイルを修正します。
$safeprojectname$.ccproj
,ServiceConfiguration.Cloud.cscfg
,ServiceConfiguration.Local.cscfg
,ServiceConfiguration.csdef
,MyTemplate.vstemplate
$safeprojectname$.csprojの参照変更
プロジェクト構造を入れ子にしているので、dllの参照を変更します。
..¥packages
を..¥..¥packages
にします。
これは、例えば、プロジェクトテンプレートで作成した場合に以下のようなフォルダ構成になるからです。
.
├── AppProject ← テンプレートで作成したプロジェクト
│ ├── AppProject-Cloud
│ └── AppProject-Code
├── ReferenceProject ← これは元からある共通プロジェクト
└── packages ← ここにある
.vstemplateの修正
テンプレートでプロジェクトを生成させるときにMyTemplate.vstemplate
に記述した内容を見てプロジェクトを生成しているので、このファイルを修正していきます。
ReplaceParameters
属性をtrueにしたファイルだけテンプレートパラメータが展開されます。
クラウドプロジェクト
<TemplateContent>
<Project TargetFileName="$safeprojectname$.ccproj" File="$safeprojectname$.ccproj" ReplaceParameters="true">
<ProjectItem ReplaceParameters="true" TargetFileName="$safeprojectname$-Code">$safeprojectname$-Code</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="diagnostics.wadcfgx">diagnostics.wadcfgx</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="ServiceConfiguration.Cloud.cscfg">ServiceConfiguration.Cloud.cscfg</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="ServiceConfiguration.Local.cscfg">ServiceConfiguration.Local.cscfg</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="ServiceDefinition.csdef">ServiceDefinition.csdef</ProjectItem>
</Project>
</TemplateContent>
コードプロジェクト
<TemplateContent>
<Project TargetFileName="$safeprojectname$-Code.csproj" File="$safeprojectname$-Code.csproj" ReplaceParameters="true">
<ProjectItem ReplaceParameters="true" TargetFileName="app.config">app.config</ProjectItem>
<Folder Name="Dto" TargetFolderName="Dto">
<ProjectItem ReplaceParameters="true" TargetFileName="SampleDto.cs">SampleDto.cs</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="true" TargetFileName="packages.config">packages.config</ProjectItem>
<Folder Name="Properties" TargetFolderName="Properties">
<ProjectItem ReplaceParameters="true" TargetFileName="AssemblyInfo.cs">AssemblyInfo.cs</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="true" TargetFileName="WorkerRole.cs">WorkerRole.cs</ProjectItem>
</Project>
</TemplateContent>
こうして作成したファイルは、ProjectTemplate-Cloud,ProjectTemplate-Code,ProjectTemplate-Testsに追加します。
テンプレートルートのプロジェクトファイルを修正(.csproj)
今回の場合はAppSample-Template.csproj
です。
コンパイルエラーが出ているのは、ここを修正すると直ります。(エディタを開いている場合にエラーが出ていても、閉じたら消える場合は問題ありません。)
ポイントは、
- <VSTemplate>タグにtemplateファイルを指定すること
- <None Inclue="">を利用すること
クラウドプロジェクト
<ItemGroup>
<None Include="diagnostics.wadcfgx" />
<None Include="$safeprojectname$.ccproj" />
<None Include="ServiceConfiguration.Cloud.cscfg" />
<None Include="ServiceConfiguration.Local.cscfg" />
<None Include="ServiceDefinition.csdef" />
</ItemGroup>
<ItemGroup>
<VSTemplate Include="MyTemplate.vstemplate" />
</ItemGroup>
コードプロジェクト
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="Dto\SampleDto.cs" />
<None Include="Properties\AssemblyInfo.cs" />
<None Include="WorkerRole.cs" />
<None Include="$safeprojectname$.csproj" />
</ItemGroup>
<ItemGroup>
<VSTemplate Include="MyTemplate.vstemplate" />
</ItemGroup>
<None>にするのは、コンパイル可能なC#のコードでもそうします。
これは、テンプレートパラメータを使ったり、依存DLLがなくてもビルドできるようにするためです。
ビルドして「文字$が予期されていません」などが出たら、自プロジェクトの.csproj
で<Compile>になっているはずなので、<None>にします。
インストーラー作成プロジェクトの作成
2010までは、.vscontent
ファイルを作って.vsi
インストーラーを作っていましたが、2013で同じことをやろうとすると%USERPROFILE%¥Documents\Visual Studio 2010\
の方に入れようとして2013では認識されませんでした。
2013以降は、vsixを作るようです。
テンプレート作成プロジェクトのソリューションで、
「新規作成」 → 「プロジェクト」 で、「Visual C#」の「VSIXプロジェクト」を選択して作成します。
以下の画像のようにsource.extension.vsixmanifest
ファイルを開いて、「Assets」を選択して、「Type」を「ProjectTemplate」、「Source」を「Current Project」、「Project」を作成したテンプレートプロジェクトにします。
CustomWizardプロジェクトも「Type」を「Assembly」にして追加します。
また「ProjectName」がツール名になるので最低限ここだけは適切な名前にしましょう。
このプロジェクトをビルドするとbinに.visx
インストーラーができます。
このファイルを配布して実行させれば、作ったテンプレートがインストールされます。
インストールされる場所は以下のランダムフォルダになります。
%USERPROFILE%\AppData\Local\Microsoft\VisualStudio\12.0\Extensions
ちなみにアンインストールは
ツール → 拡張機能と更新プログラムで、該当のテンプレートを選択すれば削除できます。
サンプルソース
githubに置いてあります。
参考にしてください。
https://github.com/ko2ic/Spike-VS-Multi-Project-Template