チュートリアル: テキスト テンプレートを使用したコード生成
コード生成は、厳密に型指定されたプログラム コードを生成できることに加え、ソース モデルに変更が生じた場合に簡単に変更できるのが特徴です。 これとは対照的に、構成ファイルを受け入れる汎用的なプログラムを作成する方法もあります。ただし、その場合、柔軟性は高くなりますが、コードの読みやすさと変更のしやすさが犠牲になり、パフォーマンスも劣る傾向があります。 このチュートリアルでは、この利点を実例で示します。
XML を読み取るための型指定されたコード
System.Xml 名前空間には、XML ドキュメントを読み込み、メモリ内で自由に操作するための広範なツールが用意されています。 ただし、すべてのノードは XmlNode という同じ型になります。 これが原因で、子ノードの型を間違ったり、誤った属性を想定したりするなど、プログラミング上のミスが発生することがよくあります。
ここで取り上げるサンプル プロジェクトでは、テンプレートでサンプル XML ファイルを読み取り、それぞれのノードの型に対応するクラスを生成します。 手動で記述するコードでは、これらのクラスを使用して XML ファイルを操作できます。 また、このアプリケーションは、同じノード型を使用する他の任意のファイルでも実行できます。 サンプル XML ファイルの目的は、アプリケーションで扱うことのできるすべてのノード型の例を提供することです。
注意
Visual Studio に付属する xsd.exe というアプリケーションを使用すると、XML ファイルから厳密に型指定されたクラスを生成できます。 ここに示すテンプレートは、あくまでも例として提供するものです。
サンプル ファイルを以下に示します。
<?xml version="1.0" encoding="utf-8" ?>
<catalog>
<artist id ="Mike%20Nash" name="Mike Nash Quartet">
<song id ="MikeNashJazzBeforeTeatime">Jazz Before Teatime</song>
<song id ="MikeNashJazzAfterBreakfast">Jazz After Breakfast</song>
</artist>
<artist id ="Euan%20Garden" name="Euan Garden">
<song id ="GardenScottishCountry">Scottish Country Garden</song>
</artist>
</catalog>
このチュートリアルを通じて構築するプロジェクトでは、次のようなコードを作成できます。コードの入力に合わせて、正しい属性と子の名前が IntelliSense によって表示されます。
Catalog catalog = new Catalog(xmlDocument);
foreach (Artist artist in catalog.Artist)
{
Console.WriteLine(artist.name);
foreach (Song song in artist.Song)
{
Console.WriteLine(" " + song.Text);
}
}
テンプレートを使用せずに作成した場合の型指定のないコードと比較してみてください。
XmlNode catalog = xmlDocument.SelectSingleNode("catalog");
foreach (XmlNode artist in catalog.SelectNodes("artist"))
{
Console.WriteLine(artist.Attributes["name"].Value);
foreach (XmlNode song in artist.SelectNodes("song"))
{
Console.WriteLine(" " + song.InnerText);
}
}
厳密に型指定されたバージョンでは、XML スキーマに変更を加えると、クラスにも変更が生じます。 コンパイラは、アプリケーション コード内の変更が必要な部分を強調表示します。 汎用的な XML コードを使用する型指定のないバージョンには、そのような支援機能はありません。
このプロジェクトでは、単一のテンプレート ファイルを使用して、型指定されたバージョンを可能にするクラスを生成します。
プロジェクトの設定
C# プロジェクトを作成する、開く
この方法は、すべてのコード プロジェクトに適用できます。 このチュートリアルでは C# プロジェクトを使用し、テスト用にコンソール アプリケーションを使用します。
プロジェクトを作成するには
[ファイル] メニューの [新規作成] をポイントし、[プロジェクト] をクリックします。
[Visual C#] ノードをクリックし、[テンプレート] ペインの [コンソール アプリケーション] をクリックします。
プロトタイプの XML ファイルをプロジェクトに追加する
このファイルの目的は、アプリケーションで読み取ることができる XML ノード型のサンプルを提供することです。 このファイルは、アプリケーションのテストに使用することもできます。 テンプレートでは、このファイル内の各ノード型に対応する C# クラスを生成します。
このファイルは、テンプレートから読み取ることができるようにプロジェクトの一部として含める必要があります。ただし、コンパイル後のアプリケーションには組み込まれません。
XML ファイルを追加するには
ソリューション エクスプローラーで、プロジェクトを右クリックし、[追加] をクリックして、[新しい項目] をクリックします。
[新しい項目の追加] ダイアログ ボックスで、[テンプレート] ペインの [XML ファイル] を選択します。
このファイルにサンプルのコンテンツを追加します。
このチュートリアルでは、このファイルに exampleXml.xml という名前を付けます。 前のセクションに示した XML と等しくなるようにファイルのコンテンツを設定します。
..
テスト コード ファイルを追加する
C# ファイルをプロジェクトに追加し、どのような記述方法を実現したいかを踏まえて、そのファイルにコードのサンプルを記述します。 次のように入力します。
using System;
namespace MyProject
{
class CodeGeneratorTest
{
public void TestMethod()
{
Catalog catalog = new Catalog(@"..\..\exampleXml.xml");
foreach (Artist artist in catalog.Artist)
{
Console.WriteLine(artist.name);
foreach (Song song in artist.Song)
{
Console.WriteLine(" " + song.Text);
} } } } }
この段階では、このコードはコンパイルに失敗します。 コンパイルを可能にするクラスは、テンプレートを作成する過程で生成することになります。
より広範囲なテストを実行すれば、サンプル XML ファイルの既知のコンテンツに照らして、テスト関数の出力をチェックすることもできます。 ただし、このチュートリアルでは、テスト メソッドがコンパイルされたら目的が達成されたと見なします。
テキスト テンプレート ファイルを追加する
テキスト テンプレート ファイルを追加し、出力の拡張子を ".cs" に設定します。
テキスト テンプレート ファイルをプロジェクトに追加するには
ソリューション エクスプローラーで、プロジェクトを右クリックし、[追加] をクリックして、[新しい項目] をクリックします。
[新しい項目の追加] ダイアログ ボックスで、[テンプレート] ペインの [テキスト テンプレート] を選択します。
注意
[前処理されたテキスト テンプレート] ではなく、[テキスト テンプレート] を追加するようにしてください。
ファイルの template ディレクティブで、hostspecific 属性を true に変更します。
この変更によって、テンプレート コードが Visual Studio のサービスにアクセスできるようになります。
output ディレクティブの extension 属性を ".cs" に変更します。これで、このテンプレートから C# ファイルが生成されるようになります。 Visual Basic プロジェクトの場合は、これを ".vb" に変更します。
ファイルを保存します。 この段階で、テキスト テンプレート ファイルには、次の行が含まれていることになります。
<#@ template debug="false" hostspecific="true" language="C#" #> <#@ output extension=".cs" #>
.
ソリューション エクスプローラーで、テンプレート ファイルの下位項目として .cs ファイルが表示されることに注意してください。 これは、テンプレート ファイルの名前の横にある [+] をクリックすることで確認できます。 このファイルは、テンプレート ファイルを保存したり、テンプレート ファイルからフォーカスを移動したりするたびに、テンプレート ファイルから生成されます。 生成されたファイルは、プロジェクトの一部としてコンパイルされます。
テンプレート ファイルの開発中は、テンプレート ファイルと生成されたファイルのウィンドウを並べて表示すると便利です。 このようにすると、テンプレートの出力結果をすぐに確認できます。 また、テンプレートから無効な C# コードが生成された場合、エラー メッセージ ウィンドウにエラーが表示されることがわかります。
生成されたファイルに直接加えた編集は、テンプレート ファイルを保存するとすべて失われます。 したがって、生成されたファイルは一切編集しないようにするか、一時的な実験に限って編集するようにしてください。 生成されたファイル内では IntelliSense が有効になるので、短いコード片を試すのに便利です。その後、コード片をテンプレート ファイルにコピーすることができます。
テキスト テンプレートの開発
ここでは、アジャイル開発の慣例に従って、テンプレートを小さなステップに分けて開発します。1 つのステップを経るたびにいくつかのエラーを解消し、最終的にテスト コードを正常にコンパイルして実行できる状態にまでしあげます。
生成されるコードのプロトタイプを作成する
テスト コードは、ファイル内の各ノードに対応するクラスを必要としています。 したがって、いくつかのコンパイル エラーは、次のコード行をテンプレートに追加して保存すると解消されます。
class Catalog {}
class Artist {}
class Song {}
これで要件はわかりましたが、これらの宣言は、サンプル XML ファイル内のノード型から生成される必要があります。 これは試験的なコード行なので、テンプレートから削除してください。
モデル XML ファイルからアプリケーション コードを生成する
XML ファイルを読み込んでクラスの宣言を生成するには、テンプレート コンテンツを次のテンプレート コードで置き換えます。
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#
XmlDocument doc = new XmlDocument();
// Replace this file path with yours:
doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
foreach (XmlNode node in doc.SelectNodes("//*"))
{
#>
public partial class <#= node.Name #> {}
<#
}
#>
ファイルのパスは、実際のプロジェクトのパスに置き換えてください。
コード ブロックの区切り記号 (<#...#>) に注目してください。 テキストを生成するプログラム コードのフラグメントは、これらの区切り記号で囲みます。 文字列に評価できる式は、式ブロックの区切り記号 (<#=...#>) で囲みます。
アプリケーションのソース コードを生成するテンプレートを記述しているときは、このように 2 種類のプログラム テキストを扱っていることになります。 コード ブロックの区切り記号の内側に記述されたプログラムは、テンプレートを保存したり、フォーカスを別のウィンドウに切り替えたりするたびに実行されます。 そこから生成されるテキストは区切り記号の外側に配置され、生成されたファイルにコピーされ、アプリケーション コードの一部となります。
<#@assembly#> ディレクティブは参照と同じように動作し、テンプレート コードでアセンブリを使用できるようにします。 テンプレートから見えるアセンブリのリストは、アプリケーション プロジェクトの参照設定のリストとは異なります。
<#@import#> ディレクティブは using ステートメントと同じように動作し、インポートされた名前空間内のクラスを短い名前で使用できるようにします。
このテンプレートはコードを生成しますが、残念なことに、サンプル XML ファイル内のノードごとにクラス宣言が出力されます。したがって、<song> ノードの複数のインスタンスが存在すると、song クラスの宣言も複数生成されることになります。
モデル ファイルを読み取ってからコードを生成する
多くのテキスト テンプレートは、最初にソース ファイルを読み取り、次にテンプレートを生成するというパターンに従います。 ここでも、サンプル ファイルをすべて読み取り、その中に含まれているノード型をまとめてから、クラス宣言を生成する必要があります。 Dictionary<>: を使用できるようにするために、<#@import#> がもう 1 つ必要です。
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#
// Read the model file
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\MySolution\MyProject\exampleXml.xml");
Dictionary <string, string> nodeTypes =
new Dictionary<string, string>();
foreach (XmlNode node in doc.SelectNodes("//*"))
{
nodeTypes[node.Name] = "";
}
// Generate the code
foreach (string nodeName in nodeTypes.Keys)
{
#>
public partial class <#= nodeName #> {}
<#
}
#>
補助メソッドを追加する
クラス機能コントロール ブロックは、補助メソッドを定義できるブロックです。 このブロックは <#+...#> で囲み、ファイル内の最後のブロックとして記述する必要があります。
クラス名の先頭文字を大文字にするには、テンプレートの最後の部分を次のテンプレート コードで置き換えます。
// Generate the code
foreach (string nodeName in nodeTypes.Keys)
{
#>
public partial class <#= UpperInitial(nodeName) #> {}
<#
}
#>
<#+
private string UpperInitial(string name)
{ return name[0].ToString().ToUpperInvariant() + name.Substring(1); }
#>
この段階では、生成された .cs ファイルには次の宣言が含まれます。
public partial class Catalog {}
public partial class Artist {}
public partial class Song {}
同じアプローチを使用して、子ノードのプロパティ、属性、内部テキストなど、より詳細な情報を追加することもできます。
Visual Studio API にアクセスする
<#@template#> ディレクティブの hostspecific 属性を設定すると、テンプレートから Visual Studio API にアクセスできます。 テンプレートでは、これを使用してプロジェクト ファイルの場所を取得することにより、テンプレート コードでの絶対パスの使用を避けることができます。
<#@ template debug="false" hostspecific="true" language="C#" #>
...
<#@ assembly name="EnvDTE" #>
...
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
.GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
テキスト テンプレートの完成
次のテンプレート コンテンツは、テスト コードのコンパイルと実行を可能にするコードを生成します。
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Collections.Generic" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
namespace MyProject
{
<#
// Map node name --> child name --> child node type
Dictionary<string, Dictionary<string, XmlNodeType>> nodeTypes = new Dictionary<string, Dictionary<string, XmlNodeType>>();
// The Visual Studio host, to get the local file path.
EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host)
.GetService(typeof(EnvDTE.DTE));
// Open the prototype document.
XmlDocument doc = new XmlDocument();
doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "exampleXml.xml"));
// Inspect all the nodes in the document.
// The example might contain many nodes of the same type,
// so make a dictionary of node types and their children.
foreach (XmlNode node in doc.SelectNodes("//*"))
{
Dictionary<string, XmlNodeType> subs = null;
if (!nodeTypes.TryGetValue(node.Name, out subs))
{
subs = new Dictionary<string, XmlNodeType>();
nodeTypes.Add(node.Name, subs);
}
foreach (XmlNode child in node.ChildNodes)
{
subs[child.Name] = child.NodeType;
}
foreach (XmlNode child in node.Attributes)
{
subs[child.Name] = child.NodeType;
}
}
// Generate a class for each node type.
foreach (string className in nodeTypes.Keys)
{
// Capitalize the first character of the name.
#>
partial class <#= UpperInitial(className) #>
{
private XmlNode thisNode;
public <#= UpperInitial(className) #>(XmlNode node)
{ thisNode = node; }
<#
// Generate a property for each child.
foreach (string childName in nodeTypes[className].Keys)
{
// Allow for different types of child.
switch (nodeTypes[className][childName])
{
// Child nodes:
case XmlNodeType.Element:
#>
public IEnumerable<<#=UpperInitial(childName)#>><#=UpperInitial(childName) #>
{
get
{
foreach (XmlNode node in
thisNode.SelectNodes("<#=childName#>"))
yield return new <#=UpperInitial(childName)#>(node);
} }
<#
break;
// Child attributes:
case XmlNodeType.Attribute:
#>
public string <#=childName #>
{ get { return thisNode.Attributes["<#=childName#>"].Value; } }
<#
break;
// Plain text:
case XmlNodeType.Text:
#>
public string Text { get { return thisNode.InnerText; } }
<#
break;
} // switch
} // foreach class child
// End of the generated class:
#>
}
<#
} // foreach class
// Add a constructor for the root class
// that accepts an XML filename.
string rootClassName = doc.SelectSingleNode("*").Name;
#>
partial class <#= UpperInitial(rootClassName) #>
{
public <#= UpperInitial(rootClassName) #>(string fileName)
{
XmlDocument doc = new XmlDocument();
doc.Load(fileName);
thisNode = doc.SelectSingleNode("<#=rootClassName#>");
}
}
}
<#+
private string UpperInitial(string name)
{
return name[0].ToString().ToUpperInvariant() + name.Substring(1);
}
#>
テスト プログラムを実行する
テスト メソッドは、コンソール アプリケーションの Main にある次のコード行によって実行されます。 F5 キーを押して、プログラムをデバッグ モードで実行します。
using System;
namespace MyProject
{ class Program
{ static void Main(string[] args)
{ new CodeGeneratorTest().TestMethod();
// Allow user to see the output:
Console.ReadLine();
} } }
アプリケーションの作成と更新
これで、汎用的な XML コードの代わりに生成されたクラスを使用して、厳密に型指定されたスタイルでアプリケーションを作成できるようになりました。
XML スキーマが変更された場合は、新しいクラスを簡単に生成できます。 コンパイラは、アプリケーション コードの変更が必要な部分を開発者に示します。
サンプルの XML ファイルが変更されたときにクラスを再生成するには、ソリューション エクスプローラーで、ツール バーの [すべてのテンプレートの変換] をクリックします。
まとめ
このチュートリアルでは、コード生成に関して、いくつかの手法と利点を紹介しました。
コード生成とは、アプリケーションのソース コードの一部をモデルから作成することです。 モデルには、アプリケーション ドメインに適した形式で情報が格納されています。モデルは、アプリケーションの有効期間を通して変化する可能性があります。
コード生成の利点の 1 つは、厳密な型指定が可能になることです。 モデルは、よりユーザーに適した形式で情報を表現します。これに対して生成されたコードは、アプリケーションの他の部分で、一連の型を使用して情報を扱うことができるようにします。
新しいコードを作成する場合も、スキーマが更新された場合も、IntelliSense とコンパイラによって、モデルのスキーマに沿ったコードを効率的に作成できます。
単純なテンプレート ファイルを 1 つプロジェクトに追加するだけで、このような利点が生み出されます。
テキスト テンプレートは、すばやく増分的に開発およびテストできます。
このチュートリアルでは、実際にモデルのインスタンスからプログラム コードが生成されます。このモデルは、アプリケーションによって処理される XML ファイルの典型的な例です。 より本格的なアプローチでは、XML スキーマを .xsd ファイルまたはドメイン固有言語定義の形式でテンプレートへの入力として使用します。 このアプローチの方が、リレーションシップの多重度など、さまざまな特性をテンプレートで判断しやすくなります。
テキスト テンプレートのトラブルシューティング
テンプレートの変換エラーやコンパイル エラーが [エラー一覧] に表示された場合、または出力ファイルが正しく生成されなかった場合は、「TextTransform ユーティリティを使用したファイルの生成」で説明されている方法を使用してテキスト テンプレートをトラブルシューティングできます。