一個簡易版的T4程式碼生成

youou發表於2021-09-09

對於企業開發來說,程式碼生成在某種意義上可以極大地提高開發效率和質量。在眾多程式碼生成方案來說,T4是一個不錯的選擇,今天花了點時間寫了一個簡易版本的T4程式碼生成的“框架”,該框架僅僅是定義了一些基本的基類以及其他與VS整合相關功能的型別而已。[原始碼從這裡下載]

目錄      
一、T4模版的定義和程式碼檔案的生成      
二、TransformationContext與TransformationContextScope      
三、Template      
四、Generator      
五、擴充套件方法RunCodeGenerator

一、T4模版的定義和程式碼檔案的生成

我們先來看看最終的程式碼生成需要定義那些東西,以及T4模版應該如何定義。對於這個框架來說,程式碼結構的生成是透過繼承自我們自定義基類Template的自定義型別實現的。作為演示,我們定義瞭如下一個DemoTemplate。從程式碼可以看出,DemoTemplate僅僅用於生成一個空類,型別名稱在建構函式中指定。

   1:  public class DemoTemplate: Template

 

   2:  {

 

   3:      public string ClassName { get; private set; }

 

   4:      public DemoTemplate(string className)

 

   5:      {

 

   6:          this.ClassName = className;

 

   7:      }

 

   8:      public override string TransformText()

 

   9:      {

 

  10:          this.WriteLine("public class {0}",this.ClassName);

 

  11:          this.WriteLine("{");

 

  12:          this.WriteLine("}");

 

  13:          return this.GenerationEnvironment.ToString();

 

  14:      }

 

  15:  }

程式碼的生成最終透過執行相應的Generator來實現,為此我們定義瞭如下一個DemoGenerator。DemoGenerator最終會生成三個.cs檔案,而每個檔案的程式碼最終由上面定義的DemoTemplate來生成。如下程式碼片段所示,繼承自Generator的DemoGenerator重寫了CreateTemplates方法,返回一個字典物件。字典的Key代表生成的檔名,而Value則表示相應的Template物件。

   1:  public class DemoGenerator : Generator

 

   2:  {

 

   3:      protected override IDictionary CreateTemplates()

 

   4:      {

 

   5:          Dictionary templates = new Dictionary();

 

   6:          templates.Add("Foo.cs", new DemoTemplate("Foo"));

 

   7:          templates.Add("Bar.cs", new DemoTemplate("Bar"));

 

   8:          templates.Add("Baz.cs", new DemoTemplate("Baz"));

 

   9:          return templates;

 

  10:      }

 

  11:  }

最後我們在T4檔案中以如下的方式執行DemoGenerator來生成我們需要的三個.cs檔案。

   1:  

 

   2:  

 

   3:  

 

   4:  

 

   5:  

 

   6:  this.RunCodeGenerator(this.Host, new DemoGenerator());

 

   7:  #>

三個.cs檔案(Foo.cs、Bar.cs和Baz.cs)最終會以如下的方式生成出來。

二、TransformationContext與TransformationContextScope

接下來我們來簡單看看Generator最終是如何利用Template生成相應的文字檔案的,不過在這之前我們先來了解一下TransformationContext與TransformationContextScope這兩個型別。顧名思義,TransformationContext用於儲存T4文字轉換的上下文資訊,而TransformationContextScope用於限制TransformationContext的作用範圍,這與Transaction/TransactionScope的關係一樣。

TransformationContext定義如下,靜態屬性Current表示當前的TransformationContext,透過它可以得到當前的TextTransformation (即T4檔案本身對應的那個TextTransformation 物件),當前的TextTemplatingEngineHost,以及針對T4檔案的DTE和ProjectItem。

   1:  public class TransformContext

 

   2:  {

 

   3:      public static TransformContext Current { get; internal set; }

 

   4:      public TextTransformation Transformation{get; private set;}

 

   5:      public ITextTemplatingEngineHost Host {get; private set;}

 

   6:      public DTE Dte { get; private set; }

 

   7:      public ProjectItem TemplateProjectItem { get; private set; }

 

   8:

 

   9:      internal TransformContext(TextTransformation transformation, ITextTemplatingEngineHost host)

 

  10:      {

 

  11:          this.Transformation = transformation;

 

  12:          this.Host = host;

 

  13:          this.Dte = (DTE)((IServiceProvider)host).GetService(typeof(DTE));

 

  14:          this.TemplateProjectItem = this.Dte.Solution.FindProjectItem(host.TemplateFile);

 

  15:      }

 

  16:

 

  17:      public static void EnsureContextInitialized()

 

  18:      {

 

  19:          if (null == Current)

 

  20:          {

 

  21:              throw new TransformationException("TransformContext is not initialized.");

 

  22:          }

 

  23:      }

 

  24:  }

TransformationContext的建構函式是Internal的,所以不能在外部直接構建,我們透過具有如下定義的TransformationContextScope來建立它並將其作為當前的TransformationContext。TransformationContextScope實現了IDisposable介面,在實現的Dispose方法中當前的TransformationContext被設定為Null。

   1:  public class TransformContextScope: IDisposable

 

   2:  {

 

   3:      public TransformContextScope(TextTransformation transformation, ITextTemplatingEngineHost host)

 

   4:      {

 

   5:          TransformContext.Current = new TransformContext(transformation, host);

 

   6:      }

 

   7:

 

   8:      public void Dispose()

 

   9:      {

 

  10:          TransformContext.Current = null;

 

  11:      }

 

  12:  }

 

三、Template

程式碼生成的邏輯實現在繼承自具有如下定義的Template型別中,而它是TextTransformation的子類。Template的核心是Render和RenderToFile方法,前者指將生成的程式碼寫入T4檔案對應的生成檔案中,後者則將內容寫入某個指定的檔案之中。Template生成的程式碼內容都是透過呼叫TransformText獲取,在Render方法中直接透過當前TransformContext獲取T4檔案本身代表的TextTransformation物件,並呼叫其Wirte方法進行內容的寫入。

而RenderToFile方法由於涉及到生成新的檔案,邏輯就相對複雜一些。它先透過當前TransformContext得到TextTemplatingEngineHost並計算出T4所在的目錄,並最終解析出生成檔案最終的路徑。檔案的建立和內容的寫入透過呼叫CreateFile方法實現,如果涉及到Source Control,還需要執行Check Out操作。新建立的檔案最終透過代表T4檔案的ProjectItem物件新增到Project之中。

   1:  public abstract class Template: TextTransformation

 

   2:  {

 

   3:      private bool initialized;

 

   4:      public override void Initialize()

 

   5:      {

 

   6:          base.Initialize();

 

   7:          initialized = true;

 

   8:      }

 

   9:      internal void EnsureInitialized()

 

  10:      {

 

  11:          if (!initialized)

 

  12:          {

 

  13:              this.Initialize();

 

  14:          }

 

  15:      }

 

  16:      public virtual void Render()

 

  17:      {

 

  18:          TransformContext.EnsureContextInitialized();

 

  19:          string contents = this.TransformText();

 

  20:          TransformContext.Current.Transformation.Write(contents);

 

  21:      }

 

  22:      public virtual void RenderToFile(string fileName)

 

  23:      {

 

  24:              TransformContext.EnsureContextInitialized();

 

  25:              string directory = Path.GetDirectoryName(TransformContext.Current.Host.TemplateFile);

 

  26:              fileName = Path.Combine(directory, fileName);

 

  27:              string contents = this.TransformText();

 

  28:              this.CreateFile(fileName, contents);

 

  29:              if (TransformContext.Current.TemplateProjectItem.ProjectItems.Cast().Any(item => item.get_FileNames(0) != fileName))

 

  30:              {

 

  31:                  TransformContext.Current.TemplateProjectItem.ProjectItems.AddFromFile(fileName);

 

  32:              }

 

  33:      }

 

  34:      protected void CreateFile(string fileName, string contents)

 

  35:      {

 

  36:          if (File.Exists(fileName) && File.ReadAllText(fileName) == contents)

 

  37:          {

 

  38:              return;

 

  39:          }

 

  40:          SourceControl sourceControl = TransformContext.Current.Dte.SourceControl;

 

  41:          if (null != sourceControl && sourceControl.IsItemUnderSCC(fileName) && !sourceControl.IsItemCheckedOut(fileName))

 

  42:          {

 

  43:              sourceControl.CheckOutItem(fileName);

 

  44:          }

 

  45:          File.WriteAllText(fileName, contents);

 

  46:      }

 

  47:  }

 

四、Generator

T4檔案中最終是透過執行Generator物件來生成程式碼的,如下是這個抽象型別的定義。它定義了兩個虛方法,其中CreateTemplates方法一組基於獨立檔案的Template物件,返回字典的Key代表生成檔名稱;CreateTemplate返回直接生成在當前T4檔案對應生成檔案的Template物件。程式碼生成透過呼叫Run方法來完成,而最終的邏輯定義在虛方法RunCore中。

在RunCore方法中,先便利透過CreateTemplates方法返回的Template物件並呼叫其RenderToFile進行獨立檔案的程式碼生成,然後呼叫CreateTemplate方法返回的Template物件的Render方法將程式碼生成於預設的程式碼檔案中。最終執行RemoveUnusedFiles用於生成無用的檔案。比如T4檔案原來生成Foo.cs檔案,現在修改T4檔案內容使之生成Bar.cs檔案,之前的檔案應該在T4檔案執行之後被刪除。

   1:  public abstract class Generator

 

   2:  {

 

   3:      protected virtual IDictionary CreateTemplates()

 

   4:      {

 

   5:          return new Dictionary();

 

   6:      }

 

   7:      protected virtual Template CreateTemplate()

 

   8:      {

 

   9:          return null;

 

  10:      }

 

  11:      public void Run()

 

  12:      {

 

  13:          this.RunCore();

 

  14:          this.RemoveUnusedFiles();

 

  15:      }

 

  16:      protected virtual void RunCore()

 

  17:      {

 

  18:          List files = new List();

 

  19:          string directory = Path.GetDirectoryName(TransformContext.Current.Host.TemplateFile);

 

  20:          foreach (var item in this.CreateTemplates())

 

  21:          {

 

  22:              files.Add(Path.Combine(directory, item.Key));

 

  23:              item.Value.EnsureInitialized();

 

  24:              item.Value.RenderToFile(item.Key);

 

  25:          }

 

  26:

 

  27:          Template template = this.CreateTemplate();

 

  28:          if (null != template)

 

  29:          {

 

  30:              template.EnsureInitialized();

 

  31:              template.Render();

 

  32:          }

 

  33:      }

 

  34:      protected virtual void RemoveUnusedFiles()

 

  35:      {

 

  36:          List files = new List();

 

  37:          string directory = Path.GetDirectoryName(TransformContext.Current.Host.TemplateFile);

 

  38:          foreach (var item in this.CreateTemplates())

 

  39:          {

 

  40:              files.Add(Path.Combine(directory, item.Key));

 

  41:              item.Value.EnsureInitialized();

 

  42:              item.Value.RenderToFile(item.Key);

 

  43:          }

 

  44:

 

  45:          if (null != this.CreateTemplate())

 

  46:          {

 

  47:              string defaultCodeFileName = Directory.GetFiles(directory)

 

  48:                  .Except(new string[] { TransformContext.Current.Host.TemplateFile })

 

  49:                  .FirstOrDefault(path => Path.GetFileNameWithoutExtension(path)

 

  50:                      == Path.GetFileNameWithoutExtension(TransformContext.Current.Host.TemplateFile));

 

  51:              if (!string.IsNullOrEmpty(defaultCodeFileName))

 

  52:              {

 

  53:                  files.Add(defaultCodeFileName);

 

  54:              }

 

  55:          }

 

  56:

 

  57:          var projectItems = TransformContext.Current.TemplateProjectItem.ProjectItems.Cast().ToArray();

 

  58:          foreach (ProjectItem projectItem in projectItems)

 

  59:          {

 

  60:              string fileName = projectItem.get_FileNames(0);

 

  61:              if (!files.Contains(fileName))

 

  62:              {

 

  63:                  projectItem.Delete();

 

  64:              }

 

  65:          }

 

  66:      }

 

  67:  }


五、擴充套件方法RunCodeGenerator

在我們的例項演示中,T4檔案中執行Generator是透過呼叫方法RunCodeGenerator來實現的,這是一個針對TextTransformation的擴充套件方法。如下面的程式碼片段所示,方法先根據指定的TextTransformation 和TextTemplatingEngineHost 建立當前TransformContext,對Generator 的Run方法的呼叫是在當前TransformContext中完成的。

   1:  public static class TextTransformationExtensions

 

   2:  {

 

   3:      public static void RunCodeGenerator(this TextTransformation transformation, ITextTemplatingEngineHost host, Generator generator)

 

   4:      {

 

   5:          using (TransformContextScope contextScope = new TransformContextScope(transformation, host))

 

   6:          {

 

   7:              generator.Run();

 

   8:          }

 

   9:      }

 

  10:  }

 

相關閱讀:    
與VS整合的若干種程式碼生成解決方案[博文彙總(共8篇)]    
透過CodeDOM定義生成程式碼的結構    
透過Visual Studio的Custom Tool定義程式碼生成器    
不同於CodeDOM的程式碼生成機制——T4    
透過T4模板實現單檔案的程式碼生成    
透過T4模板實現多檔案的程式碼生成    
解決T4模板的程式集引用的五種方案    
編寫T4模板進行程式碼生成無法避免的兩個話題:"Assembly Locking"&"Debug"    
透過自定義BuildProvider為ASP.NET提供程式碼生成

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4729/viewspace-2805195/,如需轉載,請註明出處,否則將追究法律責任。

相關文章