T4 Text TemplateによるViewModelクラスの生成 その5

さて少し空いてしまったが、残った処理をテンプレートに実装していこう。

前回はクラスの定義部までを生成した。

MyViewModel_Generated.cs (自動生成結果)
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;

using Kazzz.MVVM.ViewModel;

namespace DataBindApp1.ViewModel
{
    /// <summary>
    /// このクラスはE:\.Kazzz\Kazzz\CodeGen\ViewModelGenerate.ttにより
    /// 2011/09/27 18:58:51に自動生成されました。
    /// このファイルをエディタで直接編集しないでください
    /// </summary>
    public partial class MyViewModel: Kazzz.MVVM.ViewModel.ViewModel
    {
    }
}

残りはクラスに記述されたいたのPropertyDecl属性からプロパティのフィールドとアクセサを生成するだけだ。
今回はテンプレートの全てのソースコードを掲載する

ViewModelGenerate.tt (全て)
<#@ template language="C#" debug="true" hostspecific="true" #>
<#@ output extension="cs"#>
<#@ import namespace="System"#>
<#@ include file="Util.tt" #>
<#
    //ソリューションのプロジェクトを全て列挙
    var projects = FindAllProjects();
    foreach (var project in projects)
    {
       foreach (var projectItem in GetProjectItems(project)) 
       {
           //プロジェクト中、属性PropertyDeclで修飾されているクラスのみ抽出する
           var classes = GetCodeElements(projectItem)
               .Where(el => el.Kind == vsCMElement.vsCMElementClass).Cast<CodeClass>()
               .Where(cl => Attributes(cl).Any(at => at.Name.StartsWith("PropertyDecl")));
           foreach (var clazz in classes)
           {
              GenerateClass(clazz);
            
              //ファイルに出力                   
              int lastDot = projectItem.FileNames[0].LastIndexOf(@".");
              string filePath = projectItem.FileNames[0].Substring(0, lastDot);
              string generatedFileName = filePath + "_Generated.cs";
              SaveOutput(generatedFileName, project);
           }
       }
    }
#>
<#+
    /// <summary>
      /// PropertyDecl属性を元にクラスを生成します
    /// </summary>
    private void GenerateClass(CodeClass clazz)
    {
        //テンプレートのパスを取得
        string T4TemplatePath = Path.GetDirectoryName(Host.TemplateFile);
        string fileName = Path.GetFileName(Host.TemplateFile);
        //対象クラスと同じ名前空間として定義する
        string classNamespace = clazz.Namespace.Name;
        string className =  clazz.Name;
#>
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;

using Kazzz.MVVM.ViewModel;

namespace <#= classNamespace #>
{
    /// <summary>
    /// このクラスは<#=T4TemplatePath+"\\" + fileName#>により
    /// <#=DateTime.Now.ToString()#>に自動生成されました。
    /// このファイルをエディタで直接編集しないでください
    /// </summary>
    public partial class <#= className #>: Kazzz.MVVM.ViewModel.ViewModel
    {
<#+
        //PropertyDecl属性を抽出
        var attributes = Attributes(clazz).Where(att => att.Name=="PropertyDecl");
        foreach(CodeAttribute attribute in attributes)
        {
            var argDic = new Dictionary<string, string>();
            var attributeArgs 
                = CodeAttributeArgumentInCodeAttribute(attribute).ToArray<EnvDTE80.CodeAttributeArgument>();
            for (var i = 0; i < attributeArgs.Length; i++)
            {
                var arg = attributeArgs[i];
                var argValue = arg.Value.Replace("\"", "").Replace("typeof", "").Replace("(", "").Replace(")", "").Trim(); 
                if (string.IsNullOrEmpty(arg.Name))
                {
                    if (i == 0) 
                    {
                        argDic.Add("name", argValue);
                    } else
                    if (i == 1)
                    {
                        argDic.Add("type", argValue);
                    } else
                    if (i == 2)
                    {
                        argDic.Add("defaultValue", argValue);
                    } else
                    if (i == 3)
                    {
                        argDic.Add("converterType", argValue);
                    } else
                    if (i == 4)
                    {
                        argDic.Add("notifyPropertyChanged", argValue);
                    } 
                }                              
                else
                {
                    argDic.Add(arg.Name, argValue);
                }                                       
            }

            //文字列になっている属性値をプロパティ名、型、デフォルト値、コンバータ、変更通知に振り分ける
            string propertyName;
            argDic.TryGetValue("name", out propertyName);

            string propertyType;
            argDic.TryGetValue("type", out propertyType);

            string summary = null;
            //string metadata = null;

            string defaultValue;
            argDic.TryGetValue("defaultValue", out defaultValue);

            string typeConverter;
            argDic.TryGetValue("converterType", out typeConverter);
                                       
            string notifyPropertyChangedStr;
            bool notifyPropertyChanged = true;
            if ( argDic.TryGetValue("notifyPropertyChanged", out notifyPropertyChangedStr))
            {
                bool.TryParse(notifyPropertyChangedStr, out notifyPropertyChanged);
            }
#>
        #region <#= propertyName #>

<#+        
            GenerateField(typeConverter, propertyType, propertyName, defaultValue);
            GenerateCLRAccessor(typeConverter, propertyType, propertyName, summary, notifyPropertyChanged);
#>

        #endregion
<#+
        } // end foreach dps
#>
    }
}
<#+
    } // end generate class
#>

<#+
/// <summary>
/// フィールドを生成します
/// </summary>
private void GenerateField(string typeConverter, string propertyType
    , string propertyName, string defaultValue)
{
    var fieldname = "_" + Decapitalize(propertyName);
#>
        private <#= propertyType #> <#= fieldname #> <#= (!string.IsNullOrEmpty(defaultValue)) ? "= " + defaultValue : "" #> ;
<#+
}
/// <summary>
/// CLRプロパティのアクセサを生成します
/// </summary>
private void GenerateCLRAccessor(string typeConverter, string propertyType
    , string propertyName, string summary, bool notifyPropertyChanged)
{
    var fieldname = "_" + Decapitalize(propertyName);
    string typeConverterDefinition = typeConverter!= null 
                        ? "[TypeConverter(typeof(" + typeConverter + "))]" 
                        : "";
    if (!string.IsNullOrEmpty(summary))
        GeneratePropertyComment(summary);

    if (!string.IsNullOrEmpty(typeConverterDefinition))
        GenerateTypeConverterDefinition(typeConverterDefinition);
#>
        public <#= propertyType #> <#= propertyName #>
        {
            get { return <#= fieldname #> ; }
            set 
            { 
                if ( <#= fieldname #> == value ) 
                    return;
                <#= fieldname #> = value; 
<#+
    if ( notifyPropertyChanged ) 
        GenerateNotifyChanged(propertyName);       
#>
            }
        }
<#+
}
/// <summary>
/// コメントブロックを生成します
/// </summary>
private void GeneratePropertyComment(string summary)
{
#>
        /// <summary>
        /// <#= summary #>.
        /// </summary>
<#+
}
/// <summary>
/// 型コンバータ定義をを生成します
/// </summary>
private void GenerateTypeConverterDefinition(string typeConverterDefinition)
{
#>
        <#= typeConverterDefinition #>
<#+
}
/// <summary>
/// 型コンバータ定義をを生成します
/// </summary>
private void GenerateNotifyChanged(string propertyName)
{
#>
                RaisePropertyChanged(() => <#= propertyName #>);
<#+
}
/// <summary>
/// 先頭を小文字に変換した文字列を取得します(デキャピタライズ)
/// </summary>
/// <param name="name">対象の文字をセット</param>
/// <returns>先頭が小文字に変換された文字列が戻ります</returns>
private static string Decapitalize(string name)
{
    if (string.IsNullOrEmpty(name)) 
        return name;
    char[] chars = name.ToCharArray();
    chars[0] = Char.ToLower(chars[0]);
    return new string(chars);
}
#>

ポイントはCodeAttributeからCodeAttributeArgumentを抽出することと、CodeAttributeArgumentからどのようなプロパティを設定したかを読み取って、それを生成するコードに反映することだ。

//PropertyDecl属性を抽出
var attributes = Attributes(clazz).Where(att => att.Name=="PropertyDecl");
foreach(CodeAttribute attribute in attributes)
{
    var argDic = new Dictionary();
    var attributeArgs 
        = CodeAttributeArgumentInCodeAttribute(attribute).ToArray();

CodeAttribute型からは大した情報は取れないので、Util.ttの関数"CodeAttributeArgumentInCodeAttribute"を利用して取得したCodeAttributeをEnvDTE80.CodeAttributeArgumentに変換している。

EnvDTE80.CodeAttributeArgumentの列挙から実際に記述された「プロパティ名」「プロパティ値」をパースするのだが、.NET C#の属性はコンストラクタの引数で属性を指定する場合プロパティ名を省略できるため、

    [PropertyDecl("Name", typeof(String), "")]
    [PropertyDecl( name="Age", type=typeof(int), defaultValue = 20)]

このように同じプロパティ名"Name"であってもコンストラクタと同じ並びであればプロパティ名を省略することができる。なので、プロパティ名が省略されていた場合は何番目のパラメタなのかを判断して、一度<プロパティ名、プロパティ値>で辞書を作成している。

    var argDic = new Dictionary();
    var attributeArgs 
        = CodeAttributeArgumentInCodeAttribute(attribute).ToArray();
    for (var i = 0; i < attributeArgs.Length; i++)
    {
        var arg = attributeArgs[i];
        var argValue = arg.Value.Replace("\"", "").Replace("typeof", "").Replace("(", "").Replace(")", "").Trim(); 
        if (string.IsNullOrEmpty(arg.Name))
        {
            //Error("名無し = " + argValue);
            if (i == 0) 
            {
                argDic.Add("name", argValue);
            } else
            if (i == 1)
            {
                argDic.Add("type", argValue);
            } else
            if (i == 2)
            {
                argDic.Add("defaultValue", argValue);
            } else
            if (i == 3)
            {
                argDic.Add("converterType", argValue);
            } else
            if (i == 4)
            {
                argDic.Add("notifyPropertyChanged", argValue);
            } 
        }                              
        else
        {
            argDic.Add(arg.Name, argValue);
        }                                       
    }

決してスマートな書き方とはいえないが、現状これより良い方法を思いつかなかった。
このプロパティ辞書さえできれば、後はそれに従ってプロパティのアクセサを生成していくだけである。

最終的に自動生成されるクラスは以下のようになる。

MyViewModel_Generated.cs (完全)
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;

using Kazzz.MVVM.ViewModel;

namespace DataBindApp1.ViewModel
{
    /// 
    /// このクラスはE:\.Kazzz\Kazzz\CodeGen\ViewModelGenerate.ttにより
    /// 2011/09/30 18:26:56に自動生成されました。
    /// このファイルをエディタで直接編集しないでください
    /// 
    public partial class MyViewModel: Kazzz.MVVM.ViewModel.ViewModel
    {
        #region Name

        private String _name  ;
        public String Name
        {
            get { return _name ; }
            set 
            { 
                if ( _name == value ) 
                    return;
                _name = value; 
                RaisePropertyChanged(() => Name);
            }
        }

        #endregion
        #region Age

        private int _age = 20 ;
        public int Age
        {
            get { return _age ; }
            set 
            { 
                if ( _age == value ) 
                    return;
                _age = value; 
                RaisePropertyChanged(() => Age);
            }
        }

        #endregion
    }
}

ViewModelの自動生成はビルド時に自動的に行われるが、明示的に実行したい場合はソリューションエクスプローラーの右端の「すべてのテンプレートの変換」ボタンにより明示的に自動生成を開始できる。