複数プロジェクトのテンプレートを作成する (Visual Studio)
はじめに
Blazor Web App で、自分がよく使うパターンのテンプレを作りたかったので、やってみました。
試行錯誤の末なので、「これでできたみたい」ですが、「普通はこうだ」とか「これで正しい」とかではないかもしれません。
環境
- Windows 11
- Visual Studio Community 2022
やりたいこと
Visual Studioには、プロジェクトをテンプレートとしてエクスポートする機能があります。
この機能は、単一のプロジェクトに対して作用するため、複数のプロジェクトを含むソリューションの雛形は作れません。
例えば、Blazor Web Appの規定のテンプレートは、サーバ側とクライアント側の二つのプロジェクトを含んでいます。
そういった、複数のプロジェクトを内包するソリューションを生成するテンプレートを作りたいです。
具体的には、Blazor Web Appのテンプレをベースに、MudBlazor、PetaPoco、MySqlConnector、Authentication.Googleを加えて、すぐ使えるようにしたものがほしいです。
解決方法
実は、そのままの表題の公式の資料があります。
書いてあるとおりに進めればできるはずなのですが、判然としなかったり、アレンジしたかったりする部分があって、将来の再試行のために、自分なりの解釈をまとめておきます。
テンプレートの構造
- テンプレートは、
%HOMEPATH%\Documents\Visual Studio 2022\Templates\ProjectTemplates\
に配置された.zip
ファイルです。-
~.zip
ファイルの名前は、UIに出てきません。
-
-
~.zip
ファイルのルートには、必ず、ルート・テンプレート(~.vstemplate
)が存在します。-
~.vstemplate
ファイルの名前は、UIに出てきません。
-
- その他、ルートには、テンプレートが使用するリソースの他、ソリューションを構成するプロジェクトのフォルダが配置されます。
%HOMEPATH%\Documents\Visual Studio 2022\Templates\ProjectTemplates\MudBlazorWebApp.zip
├─Project
│ ├─~
│ └─Project.vstemplate
├─Project.Client
│ ├─~
│ └─Project.Client.vstemplate
├─__TemplateIcon.png
└─MudBlazorWebApp.vstemplate
元プロジェクトの準備
テンプレート化したいソリューションを作成します。
ソリューションやプロジェクトの名前は後で変更するので、適当に無難なものにしておきます。
各プロジェクトのテンプレートをエクスポート
ソリューションに含まれているプロジェクトを個別に全てエクスポートします。
エクスポートすると、%HOMEPATH%\Documents\Visual Studio 2022\My Exported Templates
に.zip
ファイルが生成されます。(このフォルダは、ファイルの書き換えができません。編集して保存しても元に戻って残らないので注意が必要です。)
仮に、Project.zip
とProject.Client.zip
を生成したものとします。
テンプレートの構成
- テンプレのソースを格納するフォルダを用意します。
- 仮に
MudBlazorWebAppTemplate
とします。 - このフォルダは単なる入れ物です。フォルダ名はUIを含めて一切使われません。
- 仮に
- 先ほどエクスポートした
.zip
ファイルを、個別にフォルダを作って展開します。- このフォルダ名は、生成されるソリューションに反映されます。
- ここでは、
$safeprojectname$
と$safeprojectname$.Client
に展開します。- この
$safeprojectname$
は、この文字並びのまま使用します。
- この
パラメータ
-
$~$
はパラメータ(Parameters
)と呼ばれ、テンプレートが実体化される際に、自動的に置き換えられます。 - パラメータは、フォルダ名やファイル名の他、テンプレートファイル(
~.vstemplate
)とその中で指定したファイル内でも使用可能です。 -
$safeprojectname$
は、ソリューション名に置き換わります。
ソリューションのテンプレート
- ルート・テンプレートファイルを作ります。
- 仮に、
MudBlazorWebApp.vstemplate
とします。- このファイル名はUIに現れません。
- 単一プロジェクトの場合は
Type="Project"
ですが、複数のプロジェクトを束ねるルートではType="ProjectGroup"
とします。
- 仮に、
<VSTemplate Version="2.0.0" Type="ProjectGroup"
xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
<TemplateData>
<Name>MudBlazor Web App with MySql via PetaPoco</Name>
<Description>Blazor Web App + MudBlazor + PetaPoco + MySqlConnector</Description>
<ProjectType>CSharp</ProjectType>
<ProjectSubType>
</ProjectSubType>
<SortOrder>1000</SortOrder>
<CreateNewFolder>true</CreateNewFolder>
<DefaultName>MudBlazorWebApp</DefaultName>
<ProvideDefaultName>true</ProvideDefaultName>
<LocationField>Enabled</LocationField>
<EnableLocationBrowseButton>true</EnableLocationBrowseButton>
<CreateInPlace>true</CreateInPlace>
<Icon>__TemplateIcon.png</Icon>
<TemplateGroupID>Web</TemplateGroupID>
<TemplateCategory>
<BuildSystem>MSBuild</BuildSystem>
<LanguageTag>Csharp</LanguageTag>
<PlatformTag>Linux</PlatformTag>
<PlatformTag>macOS</PlatformTag>
<PlatformTag>Windows</PlatformTag>
<ProjectTypeTag>Blazor</ProjectTypeTag>
<ProjectTypeTag>Cloud</ProjectTypeTag>
<ProjectTypeTag>Web</ProjectTypeTag>
</TemplateCategory>
</TemplateData>
<TemplateContent>
<ProjectCollection>
<ProjectTemplateLink ProjectName="$safeprojectname$" CopyParameters="true">
$safeprojectname$/Project.vstemplate
</ProjectTemplateLink>
<ProjectTemplateLink ProjectName="$safeprojectname$.Client" CopyParameters="true">
$safeprojectname$.Client/Project.Client.vstemplate
</ProjectTemplateLink>
</ProjectCollection>
</TemplateContent>
</VSTemplate>
- タグ
Name
とDescription
、TemplateCategory
などが、テンプレートの識別用にUIで使われます。 - タグ
DefaultName
は、生成時に提示されるデフォルトのソリューション名になります。 - アイコンは、
.ico
以外に.png
とかでもいけます。 - タグ
ProjectTemplateLink
には、フォルダに展開した各プロジェクトの~.vstemplate
への相対パスを記載します。- この
~.vstemplate
のファイル名は、ここに書かれるパスと一致していれば何でも構わず、任意に分かり易い名前に変更できます。 -
ProjectName="~"
には、実際に生成されるプロジェクトの名前を記載します。- ここでは、
$safeprojectname$
を使うことで、ソリューション名に関連付けています。
- ここでは、
-
CopyParameters="true"
を指定することで、ルートのパラメータが、プロジェクトのテンプレに伝達されます。- 子プロジェクト内では、
$safeprojectname$
は、自身のプロジェクト名になります。- 例えば、ルートで
$safeprojectname$.Client
と表現していたクライアント側のプロジェクト名は、そのプロジェクトの中では、$safeprojectname$
と記載するだけで、同じ文字列に置換されます。
- 例えば、ルートで
- 親から伝えられたパラメータにアクセスする際は、
$ext_safeprojectname$
のように、パラメータ名にext_
を前置します。 - ほとんどの場合は、
$safeprojectname$
で用が足りると思います。
- 子プロジェクト内では、
- この
プロジェクトのテンプレート
-
.zip
から展開されている各プロジェクトフォルダ内のファイルを編集します。 - まずは、プロジェクトの
~.vstemplate
を編集します。
<VSTemplate Version="3.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="Project">
<TemplateData>
<Name>$safeprojectname$</Name>
<Description>Blazor Web App + MudBlazor + PetaPoco + MySqlConnector</Description>
<ProjectType>CSharp</ProjectType>
<ProjectSubType>
</ProjectSubType>
<SortOrder>1000</SortOrder>
<CreateNewFolder>true</CreateNewFolder>
<DefaultName>$safeprojectname$</DefaultName>
<ProvideDefaultName>true</ProvideDefaultName>
<LocationField>Enabled</LocationField>
<EnableLocationBrowseButton>true</EnableLocationBrowseButton>
<CreateInPlace>true</CreateInPlace>
<Icon>__TemplateIcon.ico</Icon>
</TemplateData>
<TemplateContent>
<Project TargetFileName="$safeprojectname$.csproj" File="$safeprojectname$.csproj" ReplaceParameters="true">
<Folder Name="Properties" TargetFolderName="Properties">
<ProjectItem ReplaceParameters="true" TargetFileName="launchSettings.json">launchSettings.json</ProjectItem>
</Folder>
<Folder Name="wwwroot" TargetFolderName="wwwroot">
<ProjectItem ReplaceParameters="true" TargetFileName="app.css">app.css</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="favicon.png">favicon.png</ProjectItem>
</Folder>
<Folder Name="Components" TargetFolderName="Components">
<Folder Name="Layout" TargetFolderName="Layout">
<ProjectItem ReplaceParameters="false" TargetFileName="MainLayout.razor">MainLayout.razor</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="NavMenu.razor">NavMenu.razor</ProjectItem>
</Folder>
<Folder Name="Pages" TargetFolderName="Pages">
<ProjectItem ReplaceParameters="false" TargetFileName="AccessDenied.razor">AccessDenied.razor</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Error.razor">Error.razor</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Home.razor">Home.razor</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Weather.razor">Weather.razor</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="true" TargetFileName="_Imports.razor">_Imports.razor</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="App.razor">App.razor</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Routes.razor">Routes.razor</ProjectItem>
</Folder>
<Folder Name="Services" TargetFolderName="Services">
<ProjectItem ReplaceParameters="true" TargetFileName="$safeprojectname$DataSet.cs">$safeprojectname$DataSet.cs</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="$safeprojectname$AppModeService.cs">$safeprojectname$AppModeService.cs</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="true" TargetFileName="appsettings.json">appsettings.json</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="appsettings.Development.json">appsettings.Development.json</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="Program.cs">Program.cs</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="revision.info">revision.info</ProjectItem>
</Project>
</TemplateContent>
</VSTemplate>
- エクスポート時に適当に名付けた部分を、
$ext_safeprojectname$
や$safeprojectname$
に置き換えます。- 子プロジェクトフォルダ内の
$safeprojectname$
は、そのプロジェクトの名前に置き換えられます。 - ソリューション名として、テンプレ・ルートの
$safeprojectname$
に置き換える場合は、$ext_safeprojectname$
を使います。
- 子プロジェクトフォルダ内の
-
ProjectItem
タグで、対象ファイル内のパラメータを置き換える場合は、ReplaceParameters="true"
が必要です。 - 上記で、プロジェクトファイル(
.csproj
)の名前は自身のプロジェクト名に関連付けられています。 - 以下の
_Imports.razor
ファイル内では、プロジェクト名が名前空間に使われています。
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MudBlazor
@using PetaPoco
@using Tetr4lab
@using $safeprojectname$
@using $safeprojectname$.Client
@using $safeprojectname$.Components
@using $safeprojectname$.Components.Layout
@using $safeprojectname$.Components.Pages
@using $safeprojectname$.Data
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize (Policy = "Users")]
テンプレートのデプロイ
- テンプレートが完成したら、テンプレートのルートフォルダを開き、テンプレートに含めるファイルを全て選択します。
-
~.vstemplate
ファイルを右クリックして、コンテキストメニューから「圧縮先...」>「ZIPファイル」を選択します。- フォルダ内に
~.zip
が生成されます。 - ルートフォルダそのものを圧縮しないように注意してください。
- フォルダ内に
- 生成された
~.zip
を切り取り、%HOMEPATH%\Documents\Visual Studio 2022\Templates\ProjectTemplates\
(またはそのサブフォルダに)に貼り付けます。 - Visual Studioを再起動して、「新規プロジェクトの作成」を選ぶと、「新規」テンプレートとして表示されるはずです。
テンプレートのキャッシュ
テンプレートはキャッシュされるようです。
テンプレの更新が反映されない場合は、いったんデプロイしたzipを削除してみましょう。
テンプレートの利用
規定のテンプレートと同様に、「新しいプロジェクトの作成」から、テンプレートを指定します。
書法の変化
テンプレートで自分なりの書法で記載していても、生成されたソリューションには反映されません。
以下に一例を示します。
using PetaPoco;
using Tetr4lab;
namespace $safeprojectname$.Data;
/// <inheritdoc/>
public class $safeprojectname$DataSet : MySqlDataSet {
/// <inheritdoc/>
public $safeprojectname$DataSet (Database database, string? key = "database") : base (database, key) {
}
/// <inheritdoc/>
public override Task<Result<bool>> GetListSetAsync () {
//throw new NotImplementedException ();
return Task.FromResult (new Result<bool> ());
}
}
↓
using PetaPoco;
using Tetr4lab;
namespace MudBlazorWebApp1.Data
{
/// <inheritdoc/>
public class MudBlazorWebApp1DataSet : MySqlDataSet
{
/// <inheritdoc/>
public MudBlazorWebApp1DataSet(Database database, string? key = "database") : base(database, key)
{
}
/// <inheritdoc/>
public override Task<Result<bool>> GetListSetAsync()
{
//throw new NotImplementedException ();
return Task.FromResult(new Result<bool>());
}
}
}
特に、namespace
のブロックが驚きですが、元々、内部的にはそのようになっているのでしょうね。
おわりに
テンプレートのパラメータやmsmakeの変数(.csproj
内)などを使って一般化を進めると、ソリューション生成後の作業を減らせそうです。
最後までお読みいただきありがとうございました。
Discussion