かずきのBlog@hatena

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

宣言的にクラスを宣言しT4 Templateでコードを生成する

タイトルが何を言っているんだおまえは?といった感じですが、ちょっと思いついたのでやってみました。後悔はしていない。

今回やる事

今回は、クラスや名前空間などの最小限の構造を定義することでアプリケーションコードのどんがらを作ってくれるものを最終目標として、とりあえず基本となるモデルのクラスだけを作ってくれるコードジェネレータを作ることをやってみました。

目標

T4 Templateとなるとコードアシストが効かなくてイライラするのが特徴的?ですが、このイライラをなるべく軽減させてみようと思います。Pro以上だと適当にT4 Template用のエディタの拡張機能入れればいいけどExpress Editionだとシンタックスハイライトも何もない状態でやるしかないので、そこらへん考えています。

クラスの構造を表すクラスの定義

さて、クラスとかの構造を表すクラスを定義していきます。
まず、クラスライブラリプロジェクトをMetadataDefという名前で新規作成します。

PropertyDefクラス

末端のプロパティから定義していきます。プロパティは、型名とプロパティ名を文字列で持っています。そして、PropertyChangedイベントを発行するかしないかというbool型のフラグも持たせています。

namespace MetadataDef
{
    /// <summary>
    /// プロパティの定義を表すクラス
    /// </summary>
    public class PropertyDef
    {
        /// <summary>
        /// 型名
        /// </summary>
        public string PropertyType { get; set; }

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

        /// <summary>
        /// PropertyChangedイベントを発行させるかどうか
        /// </summary>
        public bool RaisePropertyChanged { get; set; }
    }
}
ClassDefクラス

次にクラスを表すクラスを定義します。こいつがちょっくら特徴的で、クラスの持つプロパティの定義を上記で作ったPropertyDefで定義するのではなく、匿名型を使って定義するようにしてみました。なので、このクラスが一番複雑です。

namespace MetadataDef
{
    using System;
    using System.Linq;
    using System.Text;

    /// <summary>
    /// クラス定義
    /// </summary>
    public class ClassDef
    {
        /// <summary>
        /// INotifyPropertyChangedを実装するかどうか
        /// </summary>
        public bool ImplementsNotifyPropertyChanged { get; set; }

        /// <summary>
        /// RaisePropertyChangedメソッドを呼ぶかどうか
        /// </summary>
        public bool RaisePropertyChanged { get; set; }

        private string baseClassName = "System.Object";
        /// <summary>
        /// 基本クラス名。デフォルトはSystem.Object
        /// </summary>
        public string BaseClassName
        {
            get
            {
                return baseClassName;
            }

            set
            {
                this.baseClassName = value;
            }
        }

        /// <summary>
        /// クラス名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// プロパティの定義.
        /// 匿名型を割り当てる
        /// 
        /// Properties = new
        /// {
        ///   // string型のNameプロパティ
        ///   Name = default(string),
        ///   // int型のAgeプロパティ
        ///   Age = default(int),
        ///   // 文字列を使って型名を指定することも出来る
        ///   Department = "Hogehoge.Department"
        /// }
        /// </summary>
        public object Properties { get; set; }

        /// <summary>
        /// Propertiesに指定された情報をPropertyDefへ変換したものを返す
        /// </summary>
        public PropertyDef[] PropertyDefinitions
        {
            get
            {
                // 何もないときは空のを返す
                if (this.Properties == null)
                {
                    return new PropertyDef[0];
                }

                return this.Properties.GetType()
                    .GetProperties()
                    .Select(p =>
                    {
                        // string型の場合だけ型名が直接指定されているか確認
                        if (typeof(string) == p.PropertyType)
                        {
                            var value = (string) p.GetValue(this.Properties, null);
                            if (!string.IsNullOrEmpty(value))
                            {
                                // 文字列で型名が指定されている場合はそれを型名として扱う
                                return new PropertyDef
                                {
                                    Name = p.Name,
                                    PropertyType = value,
                                    RaisePropertyChanged = this.RaisePropertyChanged
                                };
                            }
                        }
                        // それ以外は、PropertyInfoの型を使う
                        return new PropertyDef
                        {
                            Name = p.Name,
                            // 型名は整形するよ
                            PropertyType = CreateTypeName(p.PropertyType),
                            RaisePropertyChanged = this.RaisePropertyChanged
                        };
                    })
                    .ToArray();
            }
        }

        /// <summary>
        /// Generic型もちゃんとコード上でコンパイルできる形の型名にする
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static string CreateTypeName(Type type)
        {
            var name = type.FullName;
            if (!type.IsGenericType)
            {
                // Generic型じゃない場合はそのまま
                return name;
            }
            // Generic型の場合は文字列を整形
            var sb = new StringBuilder(name.Substring(0, name.IndexOf('`')));
            sb.Append("<");
            foreach (var param in type.GetGenericArguments())
            {
                sb.Append(CreateTypeName(param)).Append(", ");
            }
            sb.Replace(", ", ">", sb.Length - 2, 2);
            return sb.ToString();
        }
    }
}

このクラスでは、以下のことが定義できるようにしています。

  • ベースクラス名(デフォルトはSystem.Object)
  • クラス名
  • INotifyPropertyChangedを実装するかどうか
  • プロパティでPropertyChangedイベントを発行するかどうか
  • このクラスが持っているプロパティ

プロパティの定義部分は以下のようなイメージで定義できるようにしています。

// クラス名はPersonで
// INotifyPropertyChangedを実装して
// PropertyChangedイベントを発行して
// string型のNameプロパティと
// int型のAgeプロパティを持つ
new ClassDef
{
    ImplementsNotifyPropertyChanged = true,
    RaisePropertyChanged = true,
    Name = "Person",
    Properties = new
    {
        Name = default(string),
        Age = default(int)
    }
}

プロパティの定義もインテリセンスの恩恵を受けれるのが個人的に気に入ってます。

ApplicationDefBaseクラス

次に、アプリケーションを定義するクラスを作成します。今回はモデルしか定義しないので大げさな名前ですが、とりあえず気にしません。

namespace MetadataDef
{
    /// <summary>
    /// アプリケーションの定義
    /// </summary>
    public abstract class ApplicationDefBase
    {
        /// <summary>
        /// アプリケーションのNamespace
        /// </summary>
        public abstract string BaseNamespace { get; }

        /// <summary>
        /// ModelのNamespace
        /// </summary>
        public abstract string ModelNamespace { get; }

        /// <summary>
        /// BaseNamespace.ModelNamespace
        /// </summary>
        public string FullModelNamespace
        {
            get { return this.BaseNamespace + "." + this.ModelNamespace; }
        }

        /// <summary>
        /// モデルの定義
        /// </summary>
        public abstract ClassDef[] Models { get; }
    }
}

基本的にabstractなプロパティが定義されています。アプリケーションのベースの名前空間とモデルの名前空間とモデルにどんなクラスがあるのかを定義するプロパティがあります。

実際にアプリケーションの構造を定義してみる

次に、クラスライブラリのプロジェクトをMetadataという名前で作成してみました。(名前は何でもいいですが)そして、MetadataDefプロジェクトを参照設定します。
次に、プロジェクトにSampleApplicationという名前のクラスを作成します。このクラスで先ほど定義したApplicationDefBaseクラスを継承して抽象プロパティを実装していきます。

namespace Metadata
{
    using System.Collections.Generic;
    using MetadataDef;

    /// <summary>
    /// サンプルアプリケーションの定義
    /// </summary>
    public class SampleApplicationDef : ApplicationDefBase
    {
        /// <summary>
        /// 名前空間はSample.Application
        /// </summary>
        public override string BaseNamespace
        {
            get { return "Sample.Application"; }
        }

        /// <summary>
        /// ModelはModelsという名前空間
        /// </summary>
        public override string ModelNamespace
        {
            get { return "Models"; }
        }

        /// <summary>
        /// Modelのクラスの定義
        /// </summary>
        public override ClassDef[] Models
        {
            get 
            {
                return new[]
                {
                    // INotifyPropertyChangedを実装したNotificationObjectクラス
                    new ClassDef
                    {
                        ImplementsNotifyPropertyChanged = true,
                        Name = "NotificationObject"
                    },
                    // NotificationObjectを継承したPersonクラス
                    // PropertyChangedイベントを発行してプロパティは以下の2つを持つ
                    // ・string型のNameプロパティ
                    // ・int型のAgeプロパティ
                    new ClassDef
                    {
                        BaseClassName = "NotificationObject",
                        RaisePropertyChanged = true,
                        Name = "Person",
                        Properties = new
                        {
                            Name = default(string),
                            Age = default(int)
                        }
                    },
                    // Personを継承したEmployeeクラス
                    // PropertyChangedイベントを発行してプロパティは以下の3つを持つ
                    // ・int型のSalaryプロパティ
                    // ・Employee型のSuperiorプロパティ(上司)
                    // ・IList<string>型のTasksプロパティ
                    new ClassDef
                    {
                        RaisePropertyChanged = true,
                        BaseClassName = "Person",
                        Name = "Employee",
                        Properties = new
                        {
                            Salary = default(int),
                            Superior = "Employee",
                            Tasks = default(IList<string>)
                        }
                    }
                };
            }
        }
    }
}

定義内容はコメントにある通りです。この定義は、インテリセンスが効く中で出来るので割と快適です。

コード生成を行うプロジェクトを作成

アプリケーションの定義が終わったので、ついにコードを生成する対象のプロジェクトを作成します。とりあえずApplicationという名前でコンソールアプリケーションのプロジェクトを作成します。
そしてApplicationプロジェクトの下にGeneratedCodeというフォルダを作成します。

MetadataプロジェクトとMetadataDefプロジェクトの設定変更

次に先ほど作成したMetadataプロジェクトとMetadataDefプロジェクトの設定を変更します。ビルド後に生成されるdllをGeneratedCodeフォルダに出来るようにします。
プロパティのビルドの出力を以下のように「..\Application\GeneratedCode」に変更します。

そして、ビルドをすると以下のように2つのdllがGeneratedCodeフォルダに作成されます。とりあえず気分的に見えたほうが個人的に嬉しかったのでソリューションエクスプローラからGeneratedCodeフォルダに生成されたdllをプロジェクトに追加してみました。

T4 Templateの作成

ここまで来たら、ついに待ち焦がれたT4 Templateの作成です。

PropertyDef.ttinclude

まずは、プロパティを生成するテンプレートを作成します。こいつは、プロパティの変更前や変更後に呼び出されるpartialメソッドの定義や、PropertyChangedイベント発行のためのRaisePropertyChangedの呼び出しなどがあるので今回つくるT4 Templateの中では一番複雑です。でも、基本はPropertyDefクラスにある情報からプロパティを定義しているだけです。

<#+
// プロパティの生成
void GenerateProperty(PropertyDef propertyDef)
{
#>
		#region <#= propertyDef.Name #>プロパティ
		partial void <#= propertyDef.Name #>PropertyChanging(<#= propertyDef.PropertyType #> value, ref bool canWrite);
		partial void <#= propertyDef.Name #>PropertyChanged();
		private <#= propertyDef.PropertyType #> _<#= propertyDef.Name #>;
		public <#= propertyDef.PropertyType #> <#= propertyDef.Name #>
		{
			get
			{
				return this._<#= propertyDef.Name #>;
			}
				
			set
			{
				if (this._<#= propertyDef.Name #> == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.<#= propertyDef.Name #>PropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._<#= propertyDef.Name #> = value;
<#+
	if (propertyDef.RaisePropertyChanged)
	{
#>
				this.RaisePropertyChanged("<#= propertyDef.Name #>");
<#+
	}
#>
				this.<#= propertyDef.Name #>PropertyChanged();
			}
		}
		#endregion
<#+
}
#>
ClassDef.ttinclude

次にクラスを定義するためのT4 Templateです。これはClassDefを受け取ってクラス定義を生成します。分岐とかでちょっと嫌な感じになっているところはINotifyPropertyChangedの実装のところでしょうか。そこさえ気を付ければ上で定義したPropertyDefを呼んでるだけなので、そんなに難しいテンプレートではないと思います。

<#+
// クラスのコードの生成
void GenerateClass(ClassDef classDef)
{
#>
	public partial class <#= classDef.Name #> : <#= classDef.BaseClassName #> <#= classDef.ImplementsNotifyPropertyChanged ? ", System.ComponentModel.INotifyPropertyChanged" : "" #>
	{
<#+
	// INotifyPropertyChangedを実装する場合はコードを生成する
	if (classDef.ImplementsNotifyPropertyChanged)
	{
		GenerateNotifyPropertyChanged();
	}
	// プロパティのコードを生成する
	foreach (var propertyDef in classDef.PropertyDefinitions)
	{
		GenerateProperty(propertyDef);
	}
#>
	}
<#+
}
#>

<#+
// NotifyPropertyChangedの実装
void GenerateNotifyPropertyChanged()
{
#>
		public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
		protected virtual void RaisePropertyChanged(string propertyName)
		{
			var h = this.PropertyChanged;
			if (h != null)
			{
				h(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
			}
		}
<#+
}
#>
最後にコードを生成するT4 Template

部品が出そろったので、最後にコード生成を実際に行うT4 Templateを作成します。こいつはGenerator.ttと言う名前で作成してみました。
最初に、必要なdllへの参照を追加しています。絶対パスで指定する必要があるのですがVisual Studio環境変数が使えるので、とりあえず環境依存にはなっていません。

そして、includeで先ほど作成した部品のT4 Templateをincludeしています。次にMetadataプロジェクトで作成したSampleApplicationクラスのインスタンスを生成して、そのクラスのデータを読み取りながらコード生成を行っています。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".generated.cs" #>
<#@ assembly name="$(ProjectDir)\GeneratedCode\Metadata.dll" #>
<#@ assembly name="$(ProjectDir)\GeneratedCode\MetadataDef.dll" #>
<#@ import namespace="MetadataDef" #>

<#@ include file="ClassDef.ttinclude" #>
<#@ include file="PropertyDef.ttinclude" #>

// <generated>
// これはツールによって自動生成されたコードです。
// </generated>
<#
// Metadataプロジェクトで作成したアプリケーション定義のクラスをインスタンス化
var metadata = new Metadata.SampleApplicationDef();
GenerateApplication(metadata);
#>

<#+
// アプリケーションコードの生成
void GenerateApplication(ApplicationDefBase appDef)
{
#>
namespace <#= appDef.FullModelNamespace #>
{
<#+
	foreach (var classDef in appDef.Models)
	{
#>
	#region <#= classDef.Name #>クラス
<#+
		GenerateClass(classDef);
#>
	#endregion
<#+
	}
#>
}
<#+
}
#>
プロジェクトの状態

ここまで出来たらプロジェクトは以下のような状態になってます。

Generator.generated.csが今回生成されたコードになります。長いですが、全部のコードをのせてみます。

// <generated>
// これはツールによって自動生成されたコードです。
// </generated>
namespace Sample.Application.Models
{
	#region NotificationObjectクラス
	public partial class NotificationObject : System.Object , System.ComponentModel.INotifyPropertyChanged
	{
		public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
		protected virtual void RaisePropertyChanged(string propertyName)
		{
			var h = this.PropertyChanged;
			if (h != null)
			{
				h(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
			}
		}
	}
	#endregion
	#region Personクラス
	public partial class Person : NotificationObject 
	{
		#region Nameプロパティ
		partial void NamePropertyChanging(System.String value, ref bool canWrite);
		partial void NamePropertyChanged();
		private System.String _Name;
		public System.String Name
		{
			get
			{
				return this._Name;
			}
				
			set
			{
				if (this._Name == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.NamePropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._Name = value;
				this.RaisePropertyChanged("Name");
				this.NamePropertyChanged();
			}
		}
		#endregion
		#region Ageプロパティ
		partial void AgePropertyChanging(System.Int32 value, ref bool canWrite);
		partial void AgePropertyChanged();
		private System.Int32 _Age;
		public System.Int32 Age
		{
			get
			{
				return this._Age;
			}
				
			set
			{
				if (this._Age == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.AgePropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._Age = value;
				this.RaisePropertyChanged("Age");
				this.AgePropertyChanged();
			}
		}
		#endregion
	}
	#endregion
	#region Employeeクラス
	public partial class Employee : Person 
	{
		#region Salaryプロパティ
		partial void SalaryPropertyChanging(System.Int32 value, ref bool canWrite);
		partial void SalaryPropertyChanged();
		private System.Int32 _Salary;
		public System.Int32 Salary
		{
			get
			{
				return this._Salary;
			}
				
			set
			{
				if (this._Salary == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.SalaryPropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._Salary = value;
				this.RaisePropertyChanged("Salary");
				this.SalaryPropertyChanged();
			}
		}
		#endregion
		#region Superiorプロパティ
		partial void SuperiorPropertyChanging(Employee value, ref bool canWrite);
		partial void SuperiorPropertyChanged();
		private Employee _Superior;
		public Employee Superior
		{
			get
			{
				return this._Superior;
			}
				
			set
			{
				if (this._Superior == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.SuperiorPropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._Superior = value;
				this.RaisePropertyChanged("Superior");
				this.SuperiorPropertyChanged();
			}
		}
		#endregion
		#region Tasksプロパティ
		partial void TasksPropertyChanging(System.Collections.Generic.IList<System.String> value, ref bool canWrite);
		partial void TasksPropertyChanged();
		private System.Collections.Generic.IList<System.String> _Tasks;
		public System.Collections.Generic.IList<System.String> Tasks
		{
			get
			{
				return this._Tasks;
			}
				
			set
			{
				if (this._Tasks == value)
				{
					return;
				}
				
				bool canWrite = true;
				this.TasksPropertyChanging(value, ref canWrite);
				if (!canWrite)
				{
					return;
				}
				
				this._Tasks = value;
				this.RaisePropertyChanged("Tasks");
				this.TasksPropertyChanged();
			}
		}
		#endregion
	}
	#endregion
}

結構な量のコードが生成されました。

まとめ

ということで、なるべくインテリセンスの恩恵を受けながらT4 Templateを使おうと考えてた時に思いついた方法を書いてみました。今回はModelを生成するだけでしたが、これを発展させれば純粋にModelをラップするだけのViewModelとかまで自動生成できるようになると思います。その気になればViewの雛形も吐けるかも???
とまぁ、考えれば夢ひろがりんぐなのです。ApplicationDefBaseを継承したクラスがExcelで定義されたドキュメントを読み取ってとかいう作りにするとExcelからコードってことも考えられます。がっつり作りこんで使いまわせばそれなりにメリットが出てくるかもしれません。

コードのダウンロード

以下のページからダウンロードできます。