かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

自作コードジェネレータを作ろう

先日、ViewModelのコード生成を行うT4 Templateを作りました。そこで使ったテクニックを説明したいと思います。

前処理されたテキストテンプレート

以前にも紹介した前処理されたテキストテンプレートを使用しています。これはT4 Templateで書いたものを生成してくれるC#のコードを書いてくれるものです。
以前にも紹介してます。

T4 Template自体は、ここらへんで紹介しています。

この前処理されたテキストテンプレートは、普通にT4 Templateを書くよりも単体テストがしやすいというのがメリットだと思ってます。先日作成したViewModelのコード生成では、プロパティを生成する前処理されたテキストテンプレート、コマンドを生成する前処理されたテキストテンプレート、ViewModleを生成する前処理されたテキストテンプレートの3つを作っています。このように小分けをすることで、1つのテンプレートがシンプルになって個人的におすすめです。

例えば、プロパティを生成するコードはこんな風になっています。

// Property.cs
namespace Livet.Support
{
    /// <summary>
    /// プロパティの情報
    /// </summary>
    public class Property
    {
        /// <summary>
        /// 型名
        /// </summary>
        public string TypeName { get; private set; }

        /// <summary>
        /// プロパティ名
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// 属性
        /// </summary>
        public string[] Attributes { get; private set; }

        /// <summary>
        /// プロパティに変更があったことを通知するためのメソッド名
        /// </summary>
        public string RaisePropertyChanged { get; set; }

        /// <summary>
        /// プロパティのコンストラクタ
        /// </summary>
        /// <param name="typeName">プロパティの型名を指定します</param>
        /// <param name="name">プロパティ名を指定します</param>
        /// <param name="attributes">プロパティにつける属性を指定します(例:Required(ErrorMessage=\"エラーメッセージ\"))</param>
        public Property(string typeName, string name, params string[] attributes)
        {
            this.TypeName = typeName;
            this.Name = name;
            this.Attributes = attributes;
            this.RaisePropertyChanged = "RaisePropertyChanged";
        }
    }
}
// PropertyGenerator.tt
<#@ template language="C#" #>
private <#= Metadata.TypeName #> _<#= Metadata.Name #>;

<#
foreach (var attr in Metadata.Attributes)
{
#>
[<#= attr #>]
<#
}
#>
public <#= Metadata.TypeName #> <#= Metadata.Name #>
{
	get
	{
		return this._<#= Metadata.Name #>;
	}
	
	set
	{
		if (Equals(this._<#= Metadata.Name #>, value))
		{
			return;
		}
		
		this._<#= Metadata.Name #> = value;
		base.<#= Metadata.RaisePropertyChanged #>("<#= Metadata.Name #>");
	}
}
//PropertyGenerator.partial.cs
namespace Livet.Support
{
    /// <summary>
    /// プロパティを生成するジェネレータ
    /// </summary>
    public partial class PropertyGenerator
    {
        /// <summary>
        /// 生成するプロパティの情報
        /// </summary>
        public Property Metadata { get; private set; }

        public PropertyGenerator(Property metadata)
        {
            this.Metadata = metadata;
        }
    }
}

こんな風にすることで

var g = new PropertyGenerator(
  new Property("int", "Age"));
Console.WriteLine(g.TransformText());

こんな感じでプロパティのコードが吐き出されるようになります。

生成されるコードのテスト

前処理されたテキストテンプレートはC#のクラスになるので単体テストが出来ます。ただし、生成するコードは10行とかになってくるとテストで期待する結果を書くのがめんどくさくなります。
なので、Assertを抜いたテストコードをまずかいて、デバッガで止めて生成結果を目視確認したら、それをコピペしてAssertに仕立て上げます。

こうすることで、生成するコードに変更があったときに影響範囲を特定するのが楽になります。

前処理されたテキストテンプレートを組み合わせる

プロパティの前処理されたテキストテンプレートとViewModelの前処理されたテキストテンプレートをどうやって組み合わせるかということを見てみます。
ViewModelのジェネレータは、以下のファイルで構成されています。

  • ViewModel.cs
  • ViewModelGenerator.tt
  • ViewModelGenerator.partial.cs

まず、ViewModel.csでプロパティの情報を配列で保持させています。そしてプロパティの情報からプロパティのコードを生成するジェネレータを作成するメソッドを準備しておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Livet.Support
{
    public class ViewModel
    {
        // 省略

        public Property[] Properties { get; set; }

        // 省略

        public PropertyGenerator[] CreatePropertyGenerator()
        {
            if (this.Properties == null)
            {
                return new PropertyGenerator[0];
            }

            return this.Properties
                .Select(property => new PropertyGenerator(property))
                .ToArray();
        }
    }
}

ViewModelGenerator.partial.csでは、コンストラクタでViewModelを受け取るようにしています。

namespace Livet.Support
{
    /// <summary>
    /// ViewModelの自動生成を行う
    /// </summary>
    public partial class ViewModelGenerator
    {
        /// <summary>
        /// 自動生成するViewModelの情報
        /// </summary>
        public ViewModel ViewModel { get; private set; }

        public ViewModelGenerator(ViewModel viewModel)
        {
            this.ViewModel = viewModel;
        }
    }
}

あとはViewModelGenerator.ttで、このメソッドを使ってジェネレータが作成したコードをT4 Templateで出力してやります。

<#@ template language="C#" #>
namespace <#= ViewModel.Namespace #>
{
	// 省略

	public partial class <#= ViewModel.ClassName #> : <#= ViewModel.BaseClassName #>
	{
<#
PushIndent("\t");
PushIndent("\t");
foreach (var g in ViewModel.CreatePropertyGenerator())
{
#>
<#= g.TransformText() #> // ここで出力してる!!
<#
}
#>
PopIndent();
PopIndent();
#>
	// 省略
	}
}

こうして、小さな部品として作ったテンプレートを組み合わせて1つのコードを組み立てています。

本番のT4 Templateで使うための簡単なインターフェースを定義しておく

これで、基本的にViewModelを出力する前処理されたテキストテンプレートが出来たのですが、最終的なエンドユーザから見てコードが短くなるように簡単に使えるインターフェースを用意しておきます。

namespace Livet.Support
{
    /// <summary>
    /// ViewModel生成クラス
    /// </summary>
    public static class VM
    {
        /// <summary>
        /// 引数で渡されたデータに従ってViewModelを生成する
        /// </summary>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        public static string Generate(ViewModel viewModel)
        {
            return new ViewModelGenerator(viewModel).TransformText();
        }
    }
}

これで、以下ような簡単なコードでコード生成が出来るようになります。

var code = VM.Generate(new ViewModel
{
  // ViewModelのプロパティを設定する
});

T4 Templateに仕上げよう

これまでの状態だと、まだViewModelのコードを文字列で返してくれるクラスなだけです。普通のT4 Templateを組み合わせることでコード生成までしてくれるようになります。
まず、先ほど作ったジェネレータとかをコンパイルして1つのdllに纏めます。まとめたdllをソリューションフォルダのLibsの下に置きます。(ここではLivet.Support.dllがそれになります)

<#@ assembly name="$(SolutionDir)\Libs\Livet.Support.dll" #>でアセンブリを読み込んで<#@ import namespace="Livet.Support" #>で名前空間を読み込みます。後は<#= #>の中で先ほど作ったVMクラスを使ってコードを吐かせるだけです。
以下のような感じになります。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="$(SolutionDir)\Libs\Livet.Support.dll" #>
<#@ import namespace="Livet.Support" #>
<#@ output extension=".generated.cs" #>
<#= VM.Generate(new ViewModel
{
	Namespace = "SampleNamespace",
	Using = new[]
	{
		"System"
	},
	ClassName = "SampleViewModel",
	Properties = new[]
	{
		new Property("int", "Age")
	},
	Commands = new []
	{
		new Command("Add"),
	}
}) #>

<#@ output extension=".generated.cs" #>で出力されるコードの拡張子を.generated.csにしてるのも1つのポイントで、こうすることで普通のC#のコードを同じファイル名で作ることが出来ます。
以下のようなイメージです。

  • Hogehoge.tt // T4 Template
    • Hogehoge.generated.cs // 自動生成されるコード
  • Hogehoge.cs // 自動生成されたクラスにカスタムコードを追加するパーシャルクラス


とまぁ、先日作ったViewModelのコード生成は、こんな感じで作ってます。ソースコードの全体は、以下からダウンロードできるので興味のある方は見てみてください。
多分、自前のコード生成の処理は、簡単に作れるようになると思います。

前処理されたテキストテンプレート vs 普通のテキストテンプレート

さて、今回は前処理されたテキストテンプレートで主体となるコードを作成して、最後にカスタマイズ(プロパティやコマンドやら)したい部分を普通のT4 Templateで書きました。
この方法は、コードを生成する処理を単体テスト出来るというのが最大のメリットだと思います。ただし、デメリットとしては、全部T4 Templateで書いたときよりもカスタマイズするときに手間がかかるということです。


どっちをとるかはケースバイケースですが、プロジェクトとかでコード生成の仕組みを作るんだったら、生成処理をきちんとテストしてリリースするほうが無難な気が個人的にはします。
どんなコードを生成するか決めてしまえば、そんなに頻繁に変更することもないですし、dllの形で生成処理をまとめてしまえば誰かが間違えて編集してしまうという危険も下がるような気がします。

次のステップ

だいぶ簡略化できましたが、実際に使おうと思ったらコード生成をしたいと思ったタイミングでT4 TemplateC#のコードをコピペして、ファイル名を直して、ちまちまと弄っていくことになると思います。
これは、個人的に地味にめんどくさいのでアイテムテンプレートとしてまとめて「新しい項目」から追加してやると楽に作れるようになります。

次の記事では、この作り方をやろうと思います。ではでは。