VisualSutudio2010でカスタムウィザードを作成する

仕事で、ちょっと複雑な定型クラスを作成する必要があった。
既存のプロジェクトに対する追加機能で、追加対象のプロジェクトに密接に関連するため通常のテンプレートでは対応が難しい。追加機能プロジェクトの追加は追加対象のプロジェクトと同一ソリューションに対して行う。
今回は、カスタムウィザードで対応することにしてみた。
カスタムウィザード作成のおおまかな流れは方法 : プロジェクト テンプレートを組み合わせたウィザードを使用するで確認できる。

まずはプロジェクトテンプレートの作成から

通常のプロジェクトテンプレートの作成方法は、Project Template を作ろう! ~ Visual Studio 2010 の "Project Template" プロジェクト テンプレートあたりを参照。要は、自動的に置換される文字列があるので適宜編集して型紙作りましょってこと。テンプレートのカスタマイズはプロジェクトと項目テンプレートのカスタマイズを参考に。
最初はプロジェクトのエクスポート機能をつかって型紙を作成すると楽かも。ただし、一般的にはそのままでは使いものにならないと思われる。
完成したらZIPで圧縮してテンプレートディレクトリ(\My Documents\Visual Studio 2010\Templates\ProjectTemplates\<言語>\)*1に配置する。ZIP圧縮する際、トップにディレクトリを含めないように注意。展開したときに、*.vstemplate等が直接見える(良い言葉が浮かばない)ような構成とすること。
その後、「devenv.com /setup」のようにコマンドラインから実行するとテンプレートが更新される。*2別にVisualStudio起動中に実行しても問題ないようだが、テンプレートの反映はVisualStudioの再起動が必要。

ウィザードを作成する

ウィザードは、.NETクラスライブラリとして作成する。使用するときはGACに登録する。
新しくプロジェクトを作成し、適当な名前で保存する。
Microsoft.VisualStudio.TemplateWizardInterface.dllと、EnvDTE80.dll、vslangproj.dllを参照に追加する。ちなみに、EnvDTE90.dllとEnvDTE100.dllもあってVisualStudioのバージョンを表していると思われるが、EnvDTE80にしか存在しない機能があるのでEnvDTE80.dllを使う。VisualStudio2010でEnvDTE80.dllを利用しても特に問題ないようだ。
vslangproj.dllは、プロジェクトに参照を追加したりするときに必須。
また、GACに登録するには、アセンブリに署名する必要がある。方法 : アセンブリに署名する (Visual Studio)を参考に。

IWizardインターフェイスの実装

クラスを1つ作成し、Microsoft.VisualStudio.TemplateWizard.IWizardインターフェイスを実装する。
BeforeOpeningFile、ProjectFinishedGenerating、ProjectItemFinishedGenerating、RunFinished、RunStarted、ShouldAddProjectItemの6つのメソッドが定義されている。

RunStartedメソッド

ここでまず注目すべきはRunStartedメソッドだ。このメソッドは、ウィザード開始時に一番最初に呼ばれるエントリポイントとなる。

'宣言
Sub RunStarted ( _
	automationObject As Object, _
	replacementsDictionary As Dictionary(Of String, String), _
	runKind As WizardRunKind, _
	customParams As Object() _
)

引数を順に見ていこう。
最初は automationObject 。これは、VisualStudioそのものを表すオブジェクトだ。つまり、ウィザードを実行しているVisualStudioそのものを指す。EnvDTE80.EnvDTE2インターフェイスにキャストして使う。ソリューションプロパティからプロジェクト群を取得して、とある既存のプロジェクトに、テンプレートで追加しようとしている新規プロジェクトに対する参照を追加したり、逆に既存のプロジェクトに含まれる参照をテンプレートで追加しようとしている新規プロジェクトにも追加するなんてこともできる。RunStartedメソッド以外で取得はできないようなので、必要ならばフィールド変数に保持しておくと良い。
ステータスバーの文字列をいじって進捗を表示したりしてあげると誰かに喜ばれるかもしれない。

次は replacementsDictionary 。これは置換文字列を表すディクショナリだ。$projectname$とかそういうやつ。
独自の置換文字列を追加したい場合はこのオブジェクトを操作する。独自の置換文字列はテンプレートだけでも実現できるが、内容を動的に変更したい場合はウィザードを利用しないと不可能だ。
ディクショナリであるので、キーを指定すれば値が取れる。既定の置換文字列の値を取得することも可能だ。既定の置換文字列を変更することも可能だが、プロジェクト名はテンプレート選択時に決定しているので、プロジェクトのディレクトリ名等にもこだわるのならプロジェクト生成完了後に正しいプロジェクト名でコピーして、もとのプロジェクトを削除する といった骨の折れる作業が必要。
少し話がそれるが、対話的に内容を取得したい場合は自分でフォームを作って入力してもらえば良い。ウィザード用のフォームクラスがあったりはしないので、自前でテキストボックスを配置して、ボタンもおいて、イベントを処理して…と通常のフォームアプリと同じ事をする。
逆に言えば、ウィザードだからといってフォームを表示する必要はない。すべてクラス内で処理できればプログラマとの対話は必要ない。

次は runKind 。ウィザードの実行の種類を示す WizardRunKind 列挙体。あまり利用価値はない。
気になる人はWizardRunKind 列挙体 (Microsoft.VisualStudio.TemplateWizard)でも覗いてください。

最後は customParams 。これも replacementsDictionary と同じく置換文字列を表すが、カスタムパラメータのみ。
次のようなテンプレートのカスタムパラメータのコレクションである。

<CustomParameter Name="parameterName" Value="parameterValue" />

しかし、replacementsDictionaryを使えば良いのであって、わざわざObject配列などという使い勝手の悪いものを使う必要はない。

ProjectFinishedGeneratingメソッド

新しいプロジェクトの生成完了後に呼ばれる。RunStartedメソッドだけでたいていの場合は十分なのだが、新しいプロジェクトに参照を追加したりだとかゴニョゴニョしたいときに利用する。
引数でEnvDTE.Project型のオブジェクトが渡ってくるので好きに使う。
例えば、参照を追加するときは次のようにする。

Sub ProjectFinishedGenerating (project As Project) Implements IWizard.ProjectFinishedGenerating
  
  Dim vsproj As VSProject = DirectCast(project.Object, VSProject)
  vsproj.References.Add("C:\path\program.dll")

End Sub

Addの代わりにAddProjectを使えばプロジェクト参照も追加できる。詳細はVSProject インターフェイス (VSLangProj)とかReferences インターフェイス (VSLangProj)あたりを参照。

その他のメソッド

正直使わなかったのでよく調べていない。
ProjectItemFinishedGeneratingやShouldAddProjectItemはアイテムテンプレートに関連するメソッドなので今回は無関係だし。
オライリーのページに詳しい解説があるので参照のこと。

テンプレートに手を入れる

ウィザードを作成してGACに登録したら、あと少し。
方法 : プロジェクト テンプレートを組み合わせたウィザードを使用するの最後の方を参考に、

<WizardExtension>
    <Assembly>CustomWizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=fa3902f409bb6a3b</Assembly>
    <FullClassName>CustomWizard.IWizardImplementation</FullClassName>
</WizardExtension>

のようなセクションを*..vstemplateのTemplateContent 要素の後に追加する。
ウィザードアセンブリの名前と、バージョン、カルチャ、公開キーで指定するパターン。
ZIPで再度固めてVisualStudioをセットアップすれば完了。
テンプレート作成時にウィザード用の要素を追加しておけば二度手間にならずにすむ。

最後に

WindowsDevCenter.com がなければウィザードの作成は無理でした。オライリーと、記事を書いたRon Petrushaに感謝。

*1:アイテムテンプレートなら「\My Documents\Visual Studio 2010\Templates\ItemTemplates\<言語>\」だが、ここでは省略

*2:自分しか使わないならUserTemplateディレクトリでもよいが