T4 テンプレート

T4 のコード生成ソリューションの複雑性に対処する

Peter Vogel

 

MSDN マガジン 4 月号の記事「T4 を使用してコード生成への懸念を取り除く」 ( (msdn.microsoft.com/magazine/hh882448) では、開発者が Microsoft Text Template Transformation Toolkit (T4) を使って、コード生成ソリューションを簡単に作成する方法 (およびコード生成の機会を知る方法) について説明しました。しかし、他のプログラミング環境と変わらず、T4 ソリューションもメンテナンスや拡張が困難になるほど、モノリシックで複雑なソリューションになってしまうことがあります。そのような事態にならないように、T4 ソリューションのコードをリファクタリングし、コード生成ソリューションに統合するさまざまな方法を知っておく必要があります。また T4 のコード生成プロセスを理解しておくことも必要です。

T4 のコード生成プロセス

T4 のコード生成プロセスの中心となるのが T4 エンジンで、定型コード、制御ブロック、クラス機能、ディレクティブから構成される T4 テンプレートを受け取ります。エンジンはこれらの入力を使い、Microsoft TextTransformation クラスから継承する一時クラス ("生成済み変換クラス") を作成します。次に、アプリケーション ドメインを作成し、生成済み変換クラスをそのドメイン内でコンパイルおよび実行して、HTML ページから C# プログラムまで、さまざまな出力を生成します。

テンプレートに含まれる定型テキストと制御ブロックは、生成済み変換クラスの 1 つのメソッド (TransformText) に組み込みます。しかし、(<#+ … #> 区切り記号で囲まれた) クラス機能のコードはこのメソッドには配置しません。クラス機能のコードは T4 のプロセスが作成するメソッドを使わずに、生成済み変換クラスに追加します。たとえば前回の記事では、クラス機能ブロックを使って、メソッド外部で宣言された ArrayList を、生成済み変換クラスに追加しました。その後、コード生成プロセスの一環として、コード生成の制御ブロックで ArrayList にアクセスしています。ArrayList などのフィールドの追加だけでなく、クラス機能を使って、コード ブロックから呼び出すプライベート メソッドを追加することもできます。

プロセスの中心となるのはエンジンですが、ほかにも 2 つのコンポーネント (ディレクティブ プロセッサとホスト) が T4 のコード生成プロセスに参加します。ディレクティブはパラメーターに基づいてコードの追加方法やプロセスの制御方法を提供します。たとえば、T4 の Import ディレクティブには、生成済み変換クラスに using ステートメントまたは Imports ステートメントを作成するための名前空間パラメーターがあり、Include ディレクティブには、他のファイルからテキストを取得し、それを生成済み変換クラスに追加するファイル パラメーターがあります。既定のディレクティブ プロセッサが T4 で使うディレクティブに対処します。

T4 のコード生成プロセスには、プロセスとエンジンの実行環境を統合するホストも必要です。たとえば、ホストはアセンブリと名前空間の標準セットをエンジンに提供し、生成済み変換クラスのコードが必要とするアセンブリのすべてをテンプレートで指定しなくてもよいようにします。エンジンまたはディレクティブ プロセッサから要求された場合、ホストはアセンブリへの参照を追加します。またエンジンのためのファイルを取得 (場合によっては読み取り) します。またカスタム ディレクティブ プロセッサを取得したり、パラメーターが省略されたディレクティブに既定の値を提供する場合もあります。ホストは生成済み変換クラスを実行する AppDomain を提供し、エンジンが生成するエラーや警告メセージを表示します。Visual Studio は (カスタム ツール TextTemplatingFileGenerator によって) T4 エンジンをホストできます。また Visual Studio を使わずに、T4 テンプレートを処理する TextTransform コマンドライン ユーティリティを使ってホストすることも可能です。

こうしたプロセスの例として、静的コード、制御ブロック、クラス機能を組み合わせた T4 テンプレートを見てみましょう (図 1 参照)。

図 1 T4 テンプレートのサンプル

<#@ template language="VB" #>
public partial class ConnectionManager
{
<#
  For Each conName As String in Connections
#>
  private void <#= conName #>(){}
<#
  Next
#>
<#+
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Function
#>

生成済み変換クラスは図 2 に示す例のようになります。

図 2 生成済み変換クラス

Public Class GeneratedTextTransformation
  Inherits Microsoft.VisualStudio.TextTemplating.TextTransformation
  Public Overrides Function TransformText() As String
    Me.Write("public partial class ConnectionManager{")
    For Each conName As String in Connections
      Me.Write("private void ")
      Me.Write(Me.ToStringHelper.ToStringWithCulture(conName))
      Me.Write("(){}")    
    Next
  End Function
  Private Function GetFormattedDate() As String
    Return DateTime.Now.ToShortDateString()
  End Fuction
End Class

この T4 のコード生成プロセスの例にあるように、次の 3 つの方法のいずれかにより、モノリシックになりがちなソリューションをメンテナンス可能なコンポーネントにリファクタリングできる場合があります。

  1. クラス機能
  2. TextTransformation 基本クラスの拡張
  3. カスタム ディレクティブ プロセッサ

これらのメカニズムにより、複数のコード生成ソリューション間でコードの再利用が可能になります。ここからはごく簡単なケース スタディを使ってこれらの方法を説明します。それは生成済みのコードに著作権表示を追加するものです。コードを生成するためのコードを作成するという「メタ性」のため、複雑なケース スタディ (ご自分でやってみてください) を使わなくても十分に複雑です。

ここで扱わない方法も、少なくとももう 1 つあります。独自のホストを開発し、T4 テンプレートからホスト固有の機能を呼び出す方法です。しかし、新しいホストの作成が必要となるのは、Visual Studio の外部で T4 処理を呼び出そうとする場合で、TextTransform コマンドライン ツールを使用しない場合のみです。

クラス機能

クラス機能では、T4 プロセスとコードの再利用の複雑性を容易に緩和できます。クラス機能を使えばコード生成プロセスのパーツをメソッドにカプセル化できます。カプセル化したメソッドは、コードの中心となる TransformText メソッドから呼び出すことができます。クラス機能を複数のソリューションから再利用する場合は、Include ディレクティブも活用できます。

クラス機能を使うには、まず、複数のコード生成ソリューションで再利用するクラス機能を含むプロジェクトに、T4 ファイルを <#+ … #> 区切り記号で囲んで追加します。これにはメソッド、プロパティ、フィールドを含めることができます。テンプレート ファイルには、ディレクティブなど T4 特有の機能を含めることも可能です。作成したファイルも T4 ファイルなので、アプリケーションにコンパイルされるコードを生成します。したがってこのファイルのコード生成は行わないようにします。コード生成を行わない最も簡単な方法は、クラス機能ファイルの Custom Tool プロパティをクリアすることです。Visual Basic で記述した以下の例では、ReturnCopyright というメソッドをクラス機能として定義しています。

<#+
 Public Function ReturnCopyright() As String
   Return "Copyright by PH&V Information Services, 2012"
 End Function
#>

クラス機能を定義したら、Include ディレクティブを使ってテンプレートに追加し、機能を T4 テンプレートの制御ブロックで使用します。次の例では、上記のクラス機能を CopyrightFeature.tt というファイルで定義しているものとして、ReturnCopyright メソッドを式として使用しています。

<#@ Template language="VB"   #>
<#@ Output extension=".generated.cs" #>
<#= ReturnCopyright() #>
<#@ Include file="CopyrightFeature.tt" #>

また、T4 の通常の定型構文を使っても、同様のコードを生成できます。

<#+
  Public Function ReturnCopyright() As String
#>
  Copyright by PH&V Information Services, 2012
<#+
  End Function
#>

次の例のように、クラス機能の中で Write または WriteLine メソッドを使ってコードを生成することも可能です。

<#+
  Public Sub WriteCopyright()
    Write("Copyright by PH&V Information Services, 2012")
  End Function
#>

最後の 2 つの方法では、Include ディレクティブをテンプレートに追加し、次のコードを使ってメソッドを呼び出すことになります。

<# WriteCopyright() #>

通常の関数の引数とデイジー チェーン機能を一緒に使って、クラス機能をパラメーター化することもできます。ここでは適切な構造を持ち、再利用可能なライブラリを構成するために、Include ディレクティブを使って、他の T4 ファイルで T4 ファイルをインクルードします。

クラス機能を使用する場合、他のソリューションと比べて、少なくとも 1 つのメリットと 1 つのデメリットがあります。コード生成ソリューションを開発してみると、そのメリットがわかります。クラス機能 (および一般に Include) にはコンパイル済みのアセンブリが関与しません。パフォーマンス上の理由により、T4 エンジンは、生成済みの変換クラスをアプリケーション ドメインに読み込む際に、使用するアセンブリをロックする場合があります。このため、コンパイル済みのコードをテストして変更する場合、関連するアセンブリを置換するため Visual Studio の終了と再起動が必要となります。クラス機能では、その必要はありません。Visual Studio 2010 SP1 ではアセンブリのロックを行わないため、この問題は解決されています。

一方、開発者はコード生成ソリューションを使用するときに、デメリットに直面する場合もあります。プロジェクトに T4 テンプレートを追加するだけでなく、サポートするすべての T4 Include ファイルを追加する必要があります。コード生成ソリューションのために開発者が必要とするすべての T4 ファイルを含む、Visual Studio テンプレートを作成し、グループとして追加できるようにすることを検討する必要があるシナリオです。Include ファイルをソリューション内の 1 つのフォルダーに分離することも有用です。次の例では Include ディレクティブを使って、クラス機能を含むファイルを Templates フォルダーから追加しています。

<#@ Include file="Templates\classfeatures.tt" #>

もちろん、Include ファイルにあるクラス機能だけを使用するように制限されるわけではありません。任意の制御ブロックや組み込む必要のあるテキストを、生成済みの変換クラスの TransformText メソッドに含めることができます。しかし、ちょうど GoTo を避けるのと同様に、Include ファイルで適切に定義されたメンバーを使用することで、ソリューションの概念的な複雑性に対処することができます。

TextTransformation クラスの拡張

TextTransformation クラスを独自のクラスに置き換えることにより、カスタム機能をそのクラスのメソッドに組み込み、生成済み変換クラスから呼び出すことができます。コード生成ソリューションの多く (またはすべて) で使うコードがある場合は、TextTransformation クラスを拡張するのが優れた選択肢です。実質的には、ソリューションから T4 エンジンへコードをファクタリングします。

TextTransformation クラスを拡張するには、まず、TextTransformation クラスを継承する抽象クラスを作成します。

public abstract class PhvisT4Base:
  Microsoft.VisualStudio.TextTemplating.TextTransformation
{
}

TextTransformation クラスを含む Microsoft.VisualStudio.TextTemplating DLL がなければ、参照を追加する前に Visual Studio のバージョンに合った SDK をダウンロードします。

T4 のバージョンによっては TextTransformation からの継承の一環として、TransformText メソッドの実装を提供する必要がある場合があります。その場合、提供するメソッドでは、TextTransformation クラスの GenerationEnvironment プロパティに保持される、生成済み変換クラスの出力を含む文字列を返す必要があります。必要な場合、TransformText メソッドを次のようにオーバーライドします。

public override string TransformText()
{
  return this.GenerationEnvironment.ToString();
}

クラス機能と同様に、生成済み変換クラスにコードを追加するには 2 つの方法があります。次の例のように、文字列値を返すメソッドを作成できます。

protected string Copyright()
{
  return @"Copyright PH&V Information Services, 2012";
}

次の例のように、式または T4 Write メソッドまたは WriteLine メソッドでこのメソッドを使うことができます。

<#= CopyRight() #>
<#  WriteLine(CopyRight()); #>

または、次の例のように、TextTransformation 基本クラスの Write メソッドまたは WriteLine メソッドを、TextTransformation 基本クラスに追加するメソッドの中で直接使うこともできます。

protected void Copyright()
{
  base.Write(@"Copyright PH&V Information Services, 2012");
}

その後、次のように、このメソッドを制御ブロックから呼び出すことができます。

<# CopyRight(); #>

既定の TextTransformation クラスを置き換える最後の手順は、生成済み変換クラスが新しいクラスを継承することを T4 テンプレートで指定することです。これは Template 属性の Inherits パラメーターを使って行います。

<#@ Template language="C#" inherits="PhvT4Utils.PhvisT4Base" #>

さらに、DLL の物理フルパス名を使って、基本クラスを含む DLL を参照するアセンブリ ディレクティブを T4 テンプレートに追加する必要があります。

<#@ Assembly name="C:\T4Support\PhvT4Utils.dll" #>

または、基本クラスをグローバル アセンブリ キャッシュ (GAC) に追加します。

Visual Studio 2010 では、環境変数とマクロ変数を使って DLL のパスを簡素化できます。たとえば、開発中は $(ProjectDir) マクロを使って、基本クラスを含む DLL を参照します。

<#@ Assembly name="$(ProjectDir)\bin\PhvT4Utils.dll" #>

実行時には、クラス ファイルを Program Files フォルダーにインストールしているとすると、32 ビット版の Windows では環境変数 %programfiles% を使います。64 ビット版では Program Files フォルダーには %ProgramW6432% を使い、Program Files (x86) フォルダーには %ProgramFiles(x86)% を使います。この例では 64 ビット版の Windows で C:\Program Files\PHVIS\T4Tools に DLL が置かれていることを想定しています。

<#@ Assembly name="%ProgramW6432%\PHVIS\T4Tools\PHVT4Utils.DLL" #>

コンパイル済みのコードを使う他のソリューションと同様に、生成済み変換クラスの実行時に、Visual Studio はコードを含む DLL をロックする場合があります。TextTransformation クラスの開発中にロックがかけられた場合、変更を行う前に Visual Studio を再起動し、コードを再コンパイルする必要があります。

補足記事: 警告とエラー

堅牢なプロセスは、進行中にもフィードバックを返します。クラス機能の中に、または TextTransformation クラスを拡張する際に、TextTransformation クラスの Error メソッドと Warning メソッドへの呼び出しを追加することにより、生成済み変換クラスの実行中の問題を報告できます。どちらのメソッドも文字列入力を 1 つ受け取り、その文字列を Microsoft Text Template Transformation Toolkit (T4) ホストに渡します (メッセージの表示方法はホストが決定します)。Visual Studio ではメッセージが [エラー一覧] ウィンドウに表示されます。

次の例ではクラス機能でエラーを報告します。

<#= GetDatabaseData("") #>
<#+  private string GetDatabaseData(string ConnectionString)
{
    if (ConnectionString == "")
    {
      base.Error("No connection string provided.");
    }
    return "";
    }
...

クラス機能では、Error と Warning を T4 プロセスの問題の報告には使用できず、生成済み変換クラスの実行時のエラーのみに使用できます。つまり、クラス機能では、Error メッセージと Warning メッセージは、生成済み変換クラスが T4 ファイルから正しく組み立てられ、コンパイルおよび実行された場合のみ表示されます。

カスタム ディレクティブ プロセッサを作成する場合、CompilerError オブジェクトをクラスの Errors プロパティに追加することにより、T4 エラー報告処理を統合できます。CompilerError オブジェクトはエラーに関する複数の情報 (行番号とファイル名を含む) の受け渡しをサポートしますが、以下の例では ErrorText プロパティだけを設定しています。

System.CodeDom.Compiler.CompilerError err =  new 
  System.CodeDom.Compiler.CompilerError();
err.ErrorText = "Missing directive parameter";
this.Errors.Add(err);

          —P.V.

カスタム ディレクティブ プロセッサ

コード生成プロセスの複雑性に対処する最も強力で柔軟なのが、カスタム ディレクティブ プロセッサを使う方法です。他の機能とは異なり、ディレクティブ プロセッサでは、生成済み変換クラスへの参照を追加でき、生成済み変換クラスの TransformText メソッド前後に実行するメソッドにコードを挿入できます。また開発者にとって、ディレクティブは使いやすいものです。ディレクティブのパラメーターに値を指定すれば、ディレクティブが提供するメンバーを使うことができます。

ディレクティブ プロセッサで重要な問題は、開発者がプロセッサを使うためには、Windows のレジストリにキーを追加する必要があることです。ここでも、T4 ソリューションをパッケージして、レジストリ エントリを自動的に作成することを検討する必要があります。32 ビット版の Windows では、キーは次のとおりです。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\
  visualstudioversion\TextTemplating\DirectiveProcessors

64 ビット版の Windows では、キーは次のとおりです。

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\
  VisualStudio\10.0\TextTemplating\DirectiveProcessors

作成するキーの名前は作成するディレクティブ プロセッサ クラスの名前で、マイクロソフトの名前付け規則に従うと、"DirectiveProcessor" で終わります。次のサブキーが必要です。

  • Default: 空またはディレクティブの説明。
  • Class: Namespace.ClassName という形式のクラスの完全な名前。
  • Assembly/CodeBase: ディレクティブの DLL を GAC に配置している場合、DLL の名前 (Assembly)。またはディレクティブの DLL へのフルパス (CodeBase)。

プロセッサを開発するときに、このエントリを正しく設定しないと、Visual Studio はディレクティブ プロセッサが見つからないか、型を解決できないというエラーを生成します。レジストリ エントリのエラーを修正したら、Visual Studio を再起動して変更を反映します。

ディレクティブ プロセッサを使うには、開発者は名前をつけてディレクティブを T4 テンプレートに追加し、(Windows のレジストリ キーを参照する) ディレクティブの Processor パラメーターを使って、ディレクティブをプロセッサに関連付けます。T4 テンプレートの実行時に、ディレクティブ プロセッサは開発者が使ったディレクティブの名前を、開発者が指定したパラメーターと合わせて渡します。

この例では Copyright というディレクティブを CopyrightDirectiveProcessor というプロセッサに関連付け、Year というパラメーターを含めています。

<#@ Copyright Processor="CopyrightDirectiveProcessor" Year=”2012” #>

クラス機能と同様に、ディレクティブ プロセッサからの出力は、TransformText メソッドを使わずに、生成済み変換クラスに追加されます。結果として、開発者はプロセッサを使って、テンプレート制御ブロックで使用可能な生成済み変換クラスに新しいメンバーを追加できます。上記のサンプル ディレクティブには、次のように、開発者が式で使用できるプロパティや文字列変数を追加することもできます。

<#= Copyright #>

次は、当然、このディレクティブを処理するディレクティブ プロセスを作成します。

カスタム ディレクティブ プロセッサの作成

T4 ディレクティブ プロセッサは Microsoft.VisualStudio.TextTemplating.DirectiveProcessor クラスを継承します (TextTemplating ライブラリを入手するには、Visual Studio SDK をダウンロードします)。作成したディレクティブ プロセッサは、GetClassCodeForProcessingRun メソッドから、生成済み変換クラスに追加されるコードを返す必要があります。しかし、GetClassCodeForProcessingRun メソッドを呼び出す前に、T4 エンジンはプロセッサの IsDirectiveSupported メソッド (ディレクティブの名前を渡す) と ProcessDirective メソッド (ディレクティブの名前とパラメーターの値を渡す) を呼び出します。ディレクティブを実行すべきではない場合は IsDirectiveSupported メソッドから false を、実行する場合には true を返す必要があります。

ProcessDirective メソッドはディレクティブについてのすべての情報を渡すため、通常はそこで GetClassCodeForProcessingRun が返すコードを構築します。メソッドの第 2 パラメーター (引数) から読み出すことによって、ディレクティブで指定されたパラメーターの値を抽出できます。ProcessDirective メソッドの以下のコードは Year というパラメーターを探して、それを使って変数宣言を含む文字列を構築します。次に文字列を GetClassCodeForProcessingRun から返します。

string copyright = string.Empty;
public override void ProcessDirective(
  string directiveName, IDictionary<string, string> arguments)
{
  copyright = "string copyright " +
              "= \"Copyright PH&V Information Services, " +
              arguments["Year"] +"\";";
}
public override string GetClassCodeForProcessingRun()
{
  return copyright;
}

ディレクティブ プロセッサに生成済み変換クラスへの参照や using/import ステートメントを追加して、GetClassCodeForProcessingRun によって追加されたコードをサポートできます。生成済み変換クラスへの参照を追加するには、GetReferencesForProcessingRun メソッドからの文字列配列でライブラリ名を返す必要があります。たとえば、生成済み変換クラスに追加されたコードが System.XML 名前空間からのクラスを必要とする場合、次のようなコードを使います。

public override string[] GetReferencesForProcessingRun()
{
  return new string[] {"System.Xml"};
}

同様に、GetImportsForProcessingRun メソッドから文字列配列を返すことにより、生成済み変換クラスに追加する名前空間を (using ステートメントまたは Imports ステートメントを使って) 指定できます。

生成済みコード変換クラスは、TransformText メソッドの前に呼び出される初期化前と初期化後のメソッドを含みます。これらのメソッドに追加するコードは、GetPreInitializationCodeForProcessingRun メソッドと GetPostInitializationCodeForProcessingRun メソッドから返すことができます。

デバッグの際には、[カスタム ツールの実行] を使っても Visual Studio はソリューションのビルドを行わないことに注意してください。ディレクティブ プロセッサに変更を加える場合、テンプレートの実行前に最新の変更を反映するようにソリューションをビルドする必要があります。繰り返しになりますが、T4 は使用するアセンブリをロックするため、再テスト前に Visual Studio を再起動して、再コンパイルする必要があります。

T4 によって、コード生成のツールキットへの統合が非常に容易になりました。しかし、他のアプリケーションと同様、保守性や拡張性をサポートするには、ソリューション全体の構築方法を十分検討する必要があります。T4 のコード生成プロセスにはいくつかの方法があり、それぞれメリットとデメリットがあるため、方法に応じてコードのリファクタリングとプロセスへの挿入を行います。どの方法を使う場合でも、適切なアーキテクチャによる T4 ソリューションを構築することが重要です。

補足記事: 実行時に T4 を使用する

実行時にテキストを生成できる Preprocessed Text Templates を使うことで、生成済み変換クラスとはどのようなものかは理解できたことと思います。Microsoft Text Template Transformation Toolkit (T4) の実行時のコード生成結果をコンパイルしたり実行したりすることは、多くの開発者はあまり取り組みたいとは思わないでしょう。しかし、「似て非なる」 XML ドキュメントや HTML ドキュメント (または他のテキスト) がいくつか必要な場合には、Pre-processed Text Templates を使うと T4 によってそれらのドキュメントを実行時に生成できます。

他の T4 ソリューションと同様に、実行時に T4 を使うには、まず、[新しいアイテム] ダイアログから T4 ファイルをプロジェクトに追加します。ただし、テキスト テンプレート ファイルではなく、Preprocessed Text Template を追加します (Visual Studio の [新しいアイテム] ダイアログに表示されます)。Preprocessed Text Template は、Custom Tool プロパティが 通常の TextTemplatingFileGenerator でなく、TextTemplatingFileProcessor に設定されていること以外は、標準の T4 テキスト テンプレート ファイルと同じです。

テキスト テンプレートとは異なり、Preprocessed Text Template から生成されたコードを含む子ファイルは、生成済み変換クラスからの出力コードを含みません。その代わり、ファイルには生成済み変換クラスの 1 つによく似たものがあります。TransformText というメソッドを持つ、Preprocessed Text Template ファイルと同じ名前のクラスです。TransformText メソッドを実行時に呼び出すと、T4 テンプレートのコード ファイルの中に、予想通り、生成されたコードが文字列として返されます。GenerateHTML という Preprocessed Text Template ファイルでは、次のようなコードを使って、生成されたコードを実行時に取得できます。

GenerateHTML HtmlGen = new GenerateHTML();
string html = HtmlGen.TransformText();

      —P.V.

Peter Vogel は PH&V Information Services の社長です。彼の最新の著書は『Practical Code Generation in .NET』 (Addison-Wesley Professional、2010 年) です。PH&V Information Services は、サービス ベース アーキテクチャの設計の促進、およびこれらのアーキテクチャへの .NET テクノロジの統合に特化しています。Vogel はコンサルティング業務以外にも、世界中に受講者がいる Learning Tree International 用のサービス指向アーキテクチャ (SOA) 設計コースを執筆しています。

この記事のレビューに協力してくれた技術スタッフの Gareth Jones に心より感謝いたします。