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

前回は属性(アノテーション)の定義が出来たので、話をテンプレートに戻そう。次に行うのはこの属性をマーカとして使い、生成対象のクラスを見つけることだ。

1. ソリューション中のプロジェクトを列挙する
2. プロジェクト中のクラス情報を列挙する
3. PropertyDecl属性が記述されているクラスだけを抽出する
4. 抽出したクラスとPropertyDecl属性を元に"クラス名_Generted.cs"という名前のクラスを生成する

1.は既に説明したので、2以降のテンプレートを書いていく。

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);
           }
       }
    }
#>
<#+
    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
    {
    }
}
Util.tt (今回は全てのコードを掲載しておき、以降掲載しない)
<#@ template language="C#" debug="true" hostspecific="true" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ import Namespace="EnvDTE" #>
<#@ import Namespace="EnvDTE80" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#+ 
    /// <summary>
    /// ソリューション中の全てのプロジェクトを取得して返します
    /// </summary>
     /// <returns>IEnumerable<Project></returns>
    public IEnumerable<Project> FindAllProjects()
    {
        EnvDTE.DTE dte = GetDTE();
        foreach (EnvDTE.Project p in dte.Solution.Projects)
        {
            yield return p;
        }
    }
    /// <summary>
    /// ソリューション中から条件にあったプロジェクト情報を抜き出します
    /// </summary>
    /// <param name="match"></param>
    /// <returns></returns>
    public IEnumerable<Project> FindAllProjects(Predicate<Project> match)
    {
        EnvDTE.DTE dte = GetDTE();
        foreach (EnvDTE.Project p in dte.Solution.Projects)
        {
            if (match(p))
                yield return p;
        }
    }
    /// <summary>
    /// テンプレートが属しているプロジェクト情報を探して返します
    /// </summary>
    /// <returns>Project</returns>
    public Project FindProjectHost()
    {
        var dte = GetDTE();
        EnvDTE.ProjectItem containingProjectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
        Project project = containingProjectItem.ContainingProject;
        return project;
    }
    /// <summary>
    /// DTE情報を取得します
    /// </summary>
    /// <returns></returns>
    public EnvDTE.DTE GetDTE()
    {
        IServiceProvider hostServiceProvider = (IServiceProvider)Host;
        return (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
    }             
    
    /// <summary>
    /// CodeElementから文字列を取得します
    /// </summary>
    public string GetElementText(CodeElement element)
    {
        var sp = element.GetStartPoint();
        var ep = element.GetEndPoint();
        var edit = sp.CreateEditPoint();
        return edit.GetText(ep);
    }

    /// <summary>
    /// クラス情報からCodeAttributes情報を列挙します
    /// </summary>
    public IEnumerable<CodeAttribute> Attributes(CodeClass codeClass)
    {
        foreach(CodeElement element in codeClass.Attributes)
        {
            yield return (CodeAttribute)element;
        }
    }

    /// <summary>
    /// クラス情報からCodeElementを列挙します
    /// </summary>
    public IEnumerable<CodeElement> Members(CodeClass codeClass)
    {
        foreach(CodeElement element in codeClass.Members)
        {
            yield return element;
        }
    }

    /// <summary>
    /// プロジェクトからProjectItemを列挙します
    /// </summary>
    public IEnumerable<ProjectItem> GetProjectItems(Project project)
    {
        foreach(ProjectItem projectItem in project.ProjectItems)
        {
            yield return projectItem;
            foreach(ProjectItem descendantItem in GetDescendantItems(projectItem))
            {
               yield return descendantItem;
            }
        }
    }

    /// <summary>
    /// ProjectItemの子のProjectItemを列挙します
    /// </summary>
    public IEnumerable<ProjectItem> GetDescendantItems(ProjectItem projectItem)
    {    
        if (projectItem.ProjectItems != null)
        {
            foreach (ProjectItem childItem in projectItem.ProjectItems)
            {
                yield return childItem;

                foreach(ProjectItem descendantItem in GetDescendantItems(childItem))
                {
                    yield return descendantItem;
                }
            }
        }
    }

    /// <summary>
    /// ProjectItem中のCodeElementを列挙します
    /// </summary>
    public IEnumerable<CodeElement> GetCodeElements(ProjectItem projectItem)
    {
        FileCodeModel fileCodeModel = projectItem.FileCodeModel;
            
        if (fileCodeModel != null)
        {
            foreach (CodeElement codeElement in fileCodeModel.CodeElements)
            {
                foreach(CodeElement el in CodeElementDescendantsAndSelf(codeElement))
                {
                    yield return el;
                }
            }
        }
    }

    /// <summary>
    /// CodeElementの子のCodeElementを列挙します
    /// </summary>
    public IEnumerable<CodeElement> CodeElementsDescendants(CodeElements codeElements)
    {
        foreach(CodeElement element in codeElements)
        {
            foreach (CodeElement descendant in CodeElementDescendantsAndSelf(element))
            {
                yield return descendant;                
            }
        }
    }
    /// <summary>
    /// 自身の配下のCodeElementを取得します
    /// </summary>
    /// <param name="codeElement"></param>
    /// <returns></returns>
    public IEnumerable<CodeElement> CodeElementDescendantsAndSelf(CodeElement codeElement)
    {
        yield return codeElement;
    
        CodeElements codeElements;
            
        switch(codeElement.Kind)
        {        
    
            /* namespaces */
            case vsCMElement.vsCMElementNamespace:
            {
                CodeNamespace codeNamespace = (CodeNamespace)codeElement;
                codeElements = codeNamespace.Members;
                foreach(CodeElement descendant in CodeElementsDescendants(codeElements))
                {
                    yield return descendant
                }
                break;
            }
        
            /* Process classes */
            case vsCMElement.vsCMElementClass:
            {            
                CodeClass codeClass = (CodeClass)codeElement;
                codeElements = codeClass.Members;
                foreach(CodeElement descendant in CodeElementsDescendants(codeElements))
                {                
                    yield return descendant;
                }            
                break;    
            }
        
        }
    }
    /// <summary>
    /// 複数のプロジェクトアイテム中からCodeElementを列挙します
    /// </summary>
    /// <param name="projectItems"></param>
    /// <returns></returns>
    public IEnumerable<CodeElement> CodeElementsInProjectItems(ProjectItems projectItems)
    {
        foreach (ProjectItem projectItem in projectItems)
        {
            foreach (CodeElement el in CodeElementsInProjectItem(projectItem))
            {
                yield return el;
            }
        }
    }
    /// <summary>
    /// プロジェクトアイテム中からCodeElementを列挙します
    /// </summary>
    /// <param name="projectItems"></param>
    /// <returns></returns>
    public IEnumerable<CodeElement> CodeElementsInProjectItem(ProjectItem projectItem)
    {
        FileCodeModel fileCodeModel = projectItem.FileCodeModel;
        if (fileCodeModel != null)
        {
            foreach (CodeElement codeElement in fileCodeModel.CodeElements)
            {
                //WalkElements(codeElement, null);
                foreach (CodeElement el in CodeElementDescendantsAndSelf(codeElement))
                {
                    yield return el;
                }
            }
        }
        if (projectItem.ProjectItems != null)
        {
            foreach (ProjectItem childItem in projectItem.ProjectItems)
            {
                foreach (CodeElement el in CodeElementsInProjectItem(childItem))
                {
                    yield return el;
                }
            }
        }
    }
    /// <summary>
    /// CodeAttributeからCodeAttributeArgumentの列挙を取得します
    /// </summary>
    /// <param name="codeAttribute"></param>
    /// <returns>EnvDTE80.CodeAttributeArgument</returns>
    public IEnumerable<EnvDTE80.CodeAttributeArgument> CodeAttributeArgumentInCodeAttribute(
	    CodeAttribute codeAttribute)
    {
	    foreach (CodeElement child in codeAttribute.Children)
        {
            yield return (EnvDTE80.CodeAttributeArgument)child;
        }
    }	  	   
    /// <summary
    /// ファイルを保存してプロジェクトに追加します
    /// </summary>
    public void SaveOutput(string outputFileName, Project project)
    {
        //生成したコードをファイルに書き出す
        string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
        string outputFilePath = Path.Combine(templateDirectory, outputFileName);
        File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); 

        //生成したコードを一旦クリアする
        this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);

        //プロジェクトに追加する
        project.ProjectItems.AddFromFile(outputFilePath);
    }

#>

ここまででクラスのプロトタイプが出力される。
例によって私が学習した上でのポイントを解説する。

    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")));
    :
       : 
           

列挙したプロジェクト情報をトラバースして全てのコード要素を列挙しており、その中で取得したコード要素を更に列挙するがその際にWhereメソッドをネストして、クラスで且つPropertyDeclの記述があるものだけを変数classesに格納している。

この変数classes自体IEnumerableであり、CodeClassを列挙するので、その後のループ処理で対象クラスだけを処理することができる。

   foreach (var clazz in classes)
   {
      GenerateClass(clazz);
   }

取得したCodeClassオブジェクトからはクラスの情報が取得できるので、これを使ってクラスのソースコードを生成する。

<#+
    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
    {
    }
}
<#+

既に説明したが、<#+〜#>で括られた部分は出力の対象とはならない。また、<#=〜#>で括られた部分は式を記述しその結果を出力することができる。

CodeClass#Namespace.Nameで名前空間、#Nameでクラス名が取得できるのでこれをテンプレートに適用することで最終的に以下のようにクラスのプロトタイプを生成できる。

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
    {
    }
}

次回、プロパティのアクセサを生成する。