常々、C# では C/C++ のようにマクロが使えん…と思っていたのですが、実は Visual Studio 2008/2010 上であれば、T4(TextTemplatingFilePreprocessor)を使ってマクロっぽい動きができる(=コードの自動生成ができる)ってことを今頃再確認して、ああ、なるほどと思って作ってみた次第です。
コード生成と T4 テキスト テンプレート
http://msdn.microsoft.com/ja-jp/library/bb126445.aspx
T4 TemplateでViewModelの生成をするアイテムテンプレートを作りました – かずきのBlog@Hatena
http://d.hatena.ne.jp/okazuki/20110325/1301070808
宣言的にクラスを宣言しT4 Templateでコードを生成する – かずきのBlog@Hatena
http://d.hatena.ne.jp/okazuki/20110408/1302275410
を参考にして、*.tt ファイルを作って、ViewModel を作ってみます。ViewModel クラスは、Silverlight + MVVM モデルで DataGrid をバインドの落とし穴(その2) | Moonmile Solutions Blog で書いていたときのパターンを使ってみます。チープすぎて実用的ではないのですが、T4 の自動生成の確認として。
宣言的にクラスを宣言しT4 Templateでコードを生成する – かずきのBlog@Hatena なところでは、設定ファイルを作っていますが、こちらでは、
1 2 3 4 5 6 7 8 9 10 11 | /// <summary> /// ViewModel クラス /// </summary> [ViewModelTarget] public class _ViewModel { public int id { get ; set ; } public string name { get ; set ; } public int age { get ; set ; } public DateTime UpdateAt { get ; set ; } } |
な風に、クラスを定義しておけば勝手に INotifyPropertyChanged を継承した ViewModel クラスを作る、ってところを目指してみます。設定自体が生のクラスを使うってことろがミソです。
実は、テンプレートから自分のアセンブリを参照する方法が不明なので(もう少しトリッキーなことをやればできそうなんですが)、ViewModel を参照させるためだけのアセンブリを別に用意しています。
- SampleT4 に、テンプレートがあります。
- SampleT4Target は、アセンブリを参照するために用意しています。
- SampleT4 から SampleT4Target をプロジェクト参照します。
- _ViewModel.cs には、先の元になるクラスがはいっていて、2つのプロジェクトで共有(リンク)させています。
■ターゲットとなる ViewModel クラス
ちょっと無理矢理ですが、解析用のための MakeViewModel クラスを作成しています。
属性「ViewModelTarget」が付いているクラスを探すのですが、_ViewModel.cs 自体が共有されているので、無理矢理、文字列で属性値を調べています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | namespace SampleT4Target { /// <summary> /// ViewModel クラス /// </summary> [ViewModelTarget] public class _ViewModel { public int id { get ; set ; } public string name { get ; set ; } public int age { get ; set ; } public DateTime UpdateAt { get ; set ; } } public class MakeViewModel { public string className; public List<Prop> props; public MakeViewModel( Type c ) { this .className = c.Name; this .props = new List<Prop>(); foreach ( var p in c.GetProperties()) { props.Add( new Prop { name = p.Name, type = p.PropertyType }); } } public class Prop { public string name { get ; set ; } public Type type { get ; set ; } } public static List<Type> TargetTypes( string fname = "" ) { var lst = new List<Type>(); Assembly asm = (fname == "" )? Assembly.GetExecutingAssembly(): Assembly.LoadFile(fname); foreach ( var t in asm.GetTypes()) { foreach ( var at in t.GetCustomAttributes( false )) { if ( at.GetType().ToString().IndexOf( "ViewModelTarget" ) >= 0 ) // var attr = at as ViewModelTargetAttribute; // if (attr != null) { lst.Add(t); } } } return lst; } } public class ViewModelTargetAttribute : Attribute { } } |
■ViewModel のテンプレート
テンプレートファイル「ViewModel.tt」は、以下のようになります。
アセンブリを直接参照させているので、ここが嫌なんですが(プロジェクト名が変わると変更しないといけないので)ひとまず、これでコンパイルが通ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | <#@ template debug= "false" hostspecific= "false" language= "C#" #> <#@ output extension= ".cs" #> <#@ assembly name= "$(SolutionDir)\SampleT4Target\bin\Debug\SampleT4Target.dll" #> <#@ import namespace = "SampleT4Target" #> using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace SampleT4 { <# var types = MakeViewModel.TargetTypes(); foreach ( var t in types) { var mk = new MakeViewModel(t); #> /// <summary> /// <#= mk.className #> クラス /// </summary> public class <#= mk.className #> : INotifyPropertyChanged { <# foreach ( var p in mk.props ) { #> private <#= p.type.ToString() #> _<#= p.name #>; public <#= p.type.ToString() #> <#= p.name #> { get { return _<#= p.name #>; } set { if (_<#= p.name #> != value) { _<#= p.name #> = value; OnPropertyChanged( "<#= p.name #>" ); } } } <# } #> #region INotifyPropertyChanged メンバ /// <summary> /// プロパティ変更時のイベント /// </summary> public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null ) PropertyChanged( this , e); } protected virtual void OnPropertyChanged( string name) { if (PropertyChanged != null ) PropertyChanged( this , new PropertyChangedEventArgs(name)); } #endregion } <# } #> } |
テンプレートの中身自体は、インテリセンスが効かない状態なので(T4用の拡張プラグインを入れると効きます)、一度 *.cs ファイルに書き出してからちまちまと移しています。PHP か ASP.NET 風に書けば ok です。
Devart T4 Editor for Visual Studio 2010 sample
http://visualstudiogallery.msdn.microsoft.com/a42a8538-8d6e-491b-8097-5a8a00174d37
でダウンロードができます。
■自動生成された ViewModel.cs
テンプレートを保存すると、T4(TextTemplatingFileGenerator)が実行されて、ViewModel.cs ファイルが作成されます。本来は、partial にしてカスタマイズ部分と分離させると良いかなと。クラス名が元ネタのクラスと同じなので、実際は何らかのルールで変えたほうがよいでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; namespace SampleT4 { /// <summary> /// _ViewModel クラス /// </summary> public class _ViewModel : INotifyPropertyChanged { private System.Int32 _id; public System.Int32 id { get { return _id; } set { if (_id != value) { _id = value; OnPropertyChanged( "id" ); } } } private System.String _name; public System.String name { get { return _name; } set { if (_name != value) { _name = value; OnPropertyChanged( "name" ); } } } private System.Int32 _age; public System.Int32 age { get { return _age; } set { if (_age != value) { _age = value; OnPropertyChanged( "age" ); } } } private System.DateTime _UpdateAt; public System.DateTime UpdateAt { get { return _UpdateAt; } set { if (_UpdateAt != value) { _UpdateAt = value; OnPropertyChanged( "UpdateAt" ); } } } #region INotifyPropertyChanged メンバ /// <summary> /// プロパティ変更時のイベント /// </summary> public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null ) PropertyChanged( this , e); } protected virtual void OnPropertyChanged( string name) { if (PropertyChanged != null ) PropertyChanged( this , new PropertyChangedEventArgs(name)); } #endregion } } |
と、ここまで生成ができます。
本当は、別プロジェクトを作るのではなくて自前のアセンブリを参照して、対象の ViewModel クラスを見つけたいですよね。一度、「ViewModelTarget」の属性がない状態でビルドをした後に、別のところに保存。それを一時的に参照させればよいかな、と考えていますが。