T4模板學習心得
title: T4模板學習心得
tags: T4,學習心得,
---
近來工作比較輕松,因此可以有時間學習一些感興趣的是內容,一開始目標是研究dapper,簡單用了一下之後,感覺是介於寫sql和EF這種大型ORM框架之間的,然後就準備開始寫一個demo,就發現一個個手寫類好麻煩,就想到了T4模板,之前用過一些代碼生成器,似乎都是基於這個實現的,好吧,那就先學習一下T4模板。
T4模板是什麽
它是一個工具,可以幫我們生成一些固定格式的文件。舉個例子
using System; using System.Collections.Generic; namespace testNameSpace { public partial class testClass1 { public testClass1() { } public string name { get; set; } public int age { get; set; } public bool sex { get; set; } } }
實體類基本都是這個結構,包括最頂上的引用,然後是命名空間,類名,構造函數,屬性名,其實可以看到最上面的引用基本上是固定的,命名空間、類名、參數名和參數類型是變化的,但是結構是一致的,在DB first 的時候,我們是可以取到表名、參數名和參數類型的,那麽我們就可以直接從數據庫中讀取到表結構然後生成實體類了!
基礎語法
[T4的官方說明文檔][1]
裏面寫的比較詳細,這裏就不重復了,按照我練習的思路來吧。
第一步生成固定文件
<#@ output extension=".cs" #> using System; using System.Collections.Generic; namespace testNameSpace { public partial class testClass1 { public testClass1() { } public string name { get; set; } public int age { get; set; } public bool sex { get; set; } } }
這個代碼其實用到的語法就只有第一行,表示輸出的文件的後綴名是.cs,然後內容就是後面的這些內容作為文本寫到文件中,保存之後再路徑下面就有一個類文件,內容完全同上面一致。
第二步動態生成文件
雖然已經生成了文件,但是完全沒用,如果這種完全寫好的,何必再重復一次呢?所以我們需要動態生成。
<#@ output extension=".cs" #> <#@import namespace="System"#> <#@import namespace="System.Collections.Generic"#> <#@ template language="C#" hostspecific="True"#> using System; using System.Collections.Generic; <# var model = GetModel(); #> namespace <#= model.NameSpace #> { public partial class <#= model.ClassName #> { public <#= model.ClassName #>() { } <# foreach(var key in model.KeyDic.Keys) { #> public <#= model.KeyDic[key]#> <#= key #> {get;set;} <# } #> } } <#+ private ModelClass GetModel() { var model = new ModelClass() { NameSpace = "testNameSpace", ClassName ="testClass1", KeyDic = new Dictionary<string,string>() { {"name","string"}, {"age","int"}, {"sex","bool"}, } }; return model; } private class ModelClass { public string NameSpace; public string ClassName; public Dictionary<string,string> KeyDic; } #>
這段代碼的結果和上面是一致的,但是我們是動態的對吧,現在是通過GetModel()裏面實例化的,那麽從數據庫裏面取結構也沒什麽區別。還是回來再說一下語法吧
<#@ template language="C#" hostspecific="True"#>
這行代碼是標記該目標用的語言是C#,默認就是這個,當然也可以用VB。
<#@import namespace="System"#>
<#@import namespace="System.Collections.Generic"#>
引用嘛,一看就知道,這個模板中的代碼塊需要引用。
<# #>中的就是代碼塊, 簡單的C#代碼, <#= #>這個表達式控制塊,就是把結果輸出到文本中,這個代碼也是非常簡單的,現在單個類搞定了,但是我們的數據庫一般都不是單表啊,所以我們也是需要多個,多個的代碼其實也是沒有太多區別,無非就是GetModel 改成GetList而已
<#@ output extension=".cs" #>
<#@import namespace="System"#>
<#@import namespace="System.Collections.Generic"#>
<#@ template language="C#" hostspecific="True"#>
using System;
using System.Collections.Generic;
<#
var list = GetList();
foreach(var model in list)
{
#>
namespace <#= model.NameSpace #>
{
public partial class <#= model.ClassName #>
{
public <#= model.ClassName #>()
{
}
<#
foreach(var key in model.KeyDic.Keys)
{
#>
public <#= model.KeyDic[key]#> <#= key #> {get;set;}
<# } #>
}
}
<# } #>
<#+
private List<ModelClass> GetList()
{
var list = new List<ModelClass>();
var model = new ModelClass()
{
NameSpace = "testNameSpace",
ClassName ="testClass1",
KeyDic = new Dictionary<string,string>()
{
{"name","string"},
{"age","int"},
{"sex","bool"},
}
};
list.Add(model);
model = new ModelClass()
{
NameSpace = "testNameSpace",
ClassName ="testClass2",
KeyDic = new Dictionary<string,string>()
{
{"grade","string"},
{"num","int"},
{"count","bool"},
}
};
list.Add(model);
return list;
}
private class ModelClass
{
public string NameSpace;
public string ClassName;
public Dictionary<string,string> KeyDic;
}
#>
只是多了一個list循環而已,跟上面的區別不大生成的結果如下
using System;
using System.Collections.Generic;
namespace testNameSpace
{
public partial class testClass1
{
public testClass1()
{
}
public string name { get; set; }
public int age { get; set; }
public bool sex { get; set; }
}
}
namespace testNameSpace
{
public partial class testClass2
{
public testClass2()
{
}
public string grade { get; set; }
public int num { get; set; }
public bool count { get; set; }
}
}
第三步生成多個文件
雖然剛剛已經生成了多個類,但是並不完全是我們想要的結果,我們不可能把所有的類都寫到一個文件裏面,我們需要的結果是每個類都是一個單獨的文件,但是T4在多個文件上的支持並不好,查了很多資料,幾乎所有都是用的一個大牛的寫的類,來完成這個功能
<#@ assembly name="System.Core"#>
<#@ assembly name="System.Data.Linq"#>
<#@ assembly name="EnvDTE"#>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq"#>
<#@ import namespace="System"#>
<#@ import namespace="System.CodeDom"#>
<#@ import namespace="System.CodeDom.Compiler"#>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.Data.Linq"#>
<#@ import namespace="System.Data.Linq.Mapping"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Reflection"#>
<#@ import namespace="System.Text"#>
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>
<#+
// Manager class records the various blocks so it can split them up
class Manager {
private class Block {
public String Name;
public int Start, Length;
}
private Block currentBlock;
private List<Block> files = new List<Block>();
private Block footer = new Block();
private Block header = new Block();
private ITextTemplatingEngineHost host;
private StringBuilder template;
protected List<String> generatedFileNames = new List<String>();
public static Manager Create(ITextTemplatingEngineHost host, StringBuilder template) {
return (host is IServiceProvider) ? new VSManager(host, template) : new Manager(host, template);
}
public void StartNewFile(String name) {
if (name == null)
throw new ArgumentNullException("name");
CurrentBlock = new Block { Name = name };
}
public void StartFooter() {
CurrentBlock = footer;
}
public void StartHeader() {
CurrentBlock = header;
}
public void EndBlock() {
if (CurrentBlock == null)
return;
CurrentBlock.Length = template.Length - CurrentBlock.Start;
if (CurrentBlock != header && CurrentBlock != footer)
files.Add(CurrentBlock);
currentBlock = null;
}
public virtual void Process(bool split) {
if (split) {
EndBlock();
String headerText = template.ToString(header.Start, header.Length);
String footerText = template.ToString(footer.Start, footer.Length);
String outputPath = Path.GetDirectoryName(host.TemplateFile);
files.Reverse();
foreach(Block block in files) {
String fileName = Path.Combine(outputPath, block.Name);
String content = headerText + template.ToString(block.Start, block.Length) + footerText;
generatedFileNames.Add(fileName);
CreateFile(fileName, content);
template.Remove(block.Start, block.Length);
}
}
}
protected virtual void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content))
File.WriteAllText(fileName, content);
}
public virtual String GetCustomToolNamespace(String fileName) {
return null;
}
public virtual String DefaultProjectNamespace {
get { return null; }
}
protected bool IsFileContentDifferent(String fileName, String newContent) {
return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
}
private Manager(ITextTemplatingEngineHost host, StringBuilder template) {
this.host = host;
this.template = template;
}
private Block CurrentBlock {
get { return currentBlock; }
set {
if (CurrentBlock != null)
EndBlock();
if (value != null)
value.Start = template.Length;
currentBlock = value;
}
}
private class VSManager: Manager {
private EnvDTE.ProjectItem templateProjectItem;
private EnvDTE.DTE dte;
private Action<String> checkOutAction;
private Action<IEnumerable<String>> projectSyncAction;
public override String DefaultProjectNamespace {
get {
return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
}
}
public override String GetCustomToolNamespace(string fileName) {
return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
}
public override void Process(bool split) {
if (templateProjectItem.ProjectItems == null)
return;
base.Process(split);
projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null));
}
protected override void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content)) {
CheckoutFileIfRequired(fileName);
File.WriteAllText(fileName, content);
}
}
internal VSManager(ITextTemplatingEngineHost host, StringBuilder template)
: base(host, template) {
var hostServiceProvider = (IServiceProvider) host;
if (hostServiceProvider == null)
throw new ArgumentNullException("Could not obtain IServiceProvider");
dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE));
if (dte == null)
throw new ArgumentNullException("Could not obtain DTE from host");
templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName);
projectSyncAction = (IEnumerable<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
}
private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames) {
var keepFileNameSet = new HashSet<String>(keepFileNames);
var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + ".";
foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
projectFiles.Add(projectItem.get_FileNames(0), projectItem);
// Remove unused items from the project
foreach(var pair in projectFiles)
if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix))
pair.Value.Delete();
// Add missing files to the project
foreach(String fileName in keepFileNameSet)
if (!projectFiles.ContainsKey(fileName))
templateProjectItem.ProjectItems.AddFromFile(fileName);
}
private void CheckoutFileIfRequired(String fileName) {
var sc = dte.SourceControl;
if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null));
}
}
} #>
新建一個manager.tt來保存這個類,然後把我們剛剛生成多個類的模板修改一下
<#@ output extension=".cs" #>
<#@import namespace="System"#>
<#@import namespace="System.Collections.Generic"#>
<#@ template language="C#" hostspecific="True"#>
<#@include file="Manager.tt"#>
<# var manager = Manager.Create(Host, GenerationEnvironment); #>
<#
var list = GetList();
foreach(var model in list)
{
manager.StartNewFile(model.ClassName + ".cs");
#>
using System;
using System.Collections.Generic;
namespace <#= model.NameSpace #>
{
public partial class <#= model.ClassName #>
{
public <#= model.ClassName #>()
{
}
<#
foreach(var key in model.KeyDic.Keys)
{
#>
public <#= model.KeyDic[key]#> <#= key #> {get;set;}
<# } #>
}
}
<# manager.EndBlock(); #>
<# } #>
<# manager.Process(true); #>
<#+
private List<ModelClass> GetList()
{
var list = new List<ModelClass>();
var model = new ModelClass()
{
NameSpace = "testNameSpace",
ClassName ="testClass1",
KeyDic = new Dictionary<string,string>()
{
{"name","string"},
{"age","int"},
{"sex","bool"},
}
};
list.Add(model);
model = new ModelClass()
{
NameSpace = "testNameSpace",
ClassName ="testClass2",
KeyDic = new Dictionary<string,string>()
{
{"grade","string"},
{"num","int"},
{"count","bool"},
}
};
list.Add(model);
return list;
}
private class ModelClass
{
public string NameSpace;
public string ClassName;
public Dictionary<string,string> KeyDic;
}
#>
相比之前多了一個語法<#@include file="Manager.tt"#>
即引用別的模板文件,然後功能上也就比之前多了manager類的一下方法,manager.StartNewFile(model.ClassName + ".cs");
,開始一個新的文件,<# manager.Process(true); #>
這一行代碼才是分文件,一定要加上。
總結
寫到這裏就結束了,因為在後面無非就是從數據庫讀表結構然後生成實體類,這樣的代碼有太多,而且跟這個也沒有太大關系了,至於更多的功能性的也都可以做,單其實也都只是在這個基礎上做加法,參考當年做三層的動軟的代碼生成器 bll dal model 層都可以自動生成,原理都是一樣的。
雖然我學習是以寫實體類作為方向,但是T4模板的運用方向其實是很廣的,並不僅僅是生成cs文件,txt、html、xml等等,都是可以的,具體的運用不同,方法卻是大同小異。作為VS提供一個工具,在適用的場景下用一用,真的能省不少事。
T4模板學習心得