緣由
用Net技術生成純靜態網站目前市面上的技術貌似不是很多,要么就是一些大公司的項目。相比於Php語言來說,基於Php語言的CMS系統就有很多了,並且模板解析技術也已經比較成熟了。模板解析引擎一直是一個核心的問題,曾經我也嘗試了好多種辦法來間接的實現模板解析,但都不能完美的解決面臨的問題,相信很多使用Net做網站的朋友也希望有一套像Php那樣的CMS系統。直到有一天公司組織微軟的專家過來培訓讓我了解到了VS10在代碼生成方面所呈現出的優越表現,讓我聯想到了這套引擎能不能用於其他的方面應用。。。。(寫此文的目的為記錄日志,所以大牛的話可以飄過了。)
一、所需准備:
- 本文介紹的實現方法將以C#語言為實現。
- 實驗環境是VS10+sp1+VSsdk
- 需要引入程序集:Microsoft.VisualStudio.TextTemplating.10.0.dll 和 Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll
- 所需的Net framework平台是 4,還在用2 || 3 || 3.5的朋友趕緊的更新一下吧!
下載地址(vssdk):http://www.microsoft.com/en-us/download/details.aspx?id=2680
已知問題:如果你的VS已經打過了SP1,那么安裝VSSDK時會出現一個錯誤,需要手工更改一下注冊表,需要將注冊表中的某個鍵值1更改為0。具體的詳細設置辦法Google一下就有答案了。
二、技術實現
2.1實現思路
用T4做為模板文件的解析引擎,將數據、解析引擎、靜態文件模板、控制器分別單獨出來,這樣的話程序員只用寫一套框架程序就行了,框架寫好之后剩下的就是寫靜態文件模板了。關於T4模板解析引擎功能的強大之處,可以參考MSDN的官方資料。()像這些問題如:模板嵌套子模板、可編程等問題,早已被T4完美的解決了。
2.2實現代碼
要實現我們的自定義主機解析引擎,首先要添加一個實現了ITextTemplatingEngineHost 和 ITextTemplatingSessionHost 接口的類,代碼如下(時間長了,我也忘記是從哪里Copy過來的代碼了,應該是MSDN吧):
首先創建一個CustomCmdLineHost類,添加如下應用:
1: using System.IO;
2: using System.CodeDom.Compiler;
3: using Microsoft.VisualStudio.TextTemplating;
然后實現ITextTemplatingEngineHost 和 ITextTemplatingSessionHost 接口,代碼如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.IO;
6: using System.CodeDom.Compiler;
7: using Microsoft.VisualStudio.TextTemplating;
8:
9: namespace Smart.TextTemplating
10: {
11: public class CustomCmdLineHost : ITextTemplatingEngineHost, ITextTemplatingSessionHost
12: {
13: //the path and file name of the text template that is being processed
14: //---------------------------------------------------------------------
15: public string TemplateFileValue;
16: public string TemplateFile
17: {
18: get { return TemplateFileValue; }
19: }
20: //This will be the extension of the generated text output file.
21: //The host can provide a default by setting the value of the field here.
22: //The engine can change this value based on the optional output directive
23: //if the user specifies it in the text template.
24: //---------------------------------------------------------------------
25: private string fileExtensionValue = ".txt";
26: public string FileExtension
27: {
28: get { return fileExtensionValue; }
29: }
30: //This will be the encoding of the generated text output file.
31: //The host can provide a default by setting the value of the field here.
32: //The engine can change this value based on the optional output directive
33: //if the user specifies it in the text template.
34: //---------------------------------------------------------------------
35: private Encoding fileEncodingValue = Encoding.UTF8;
36: public Encoding FileEncoding
37: {
38: get { return fileEncodingValue; }
39: }
40: //These are the errors that occur when the engine processes a template.
41: //The engine passes the errors to the host when it is done processing,
42: //and the host can decide how to display them. For example, the host
43: //can display the errors in the UI or write them to a file.
44: //---------------------------------------------------------------------
45: private CompilerErrorCollection errorsValue;
46: public CompilerErrorCollection Errors
47: {
48: get { return errorsValue; }
49: }
50: //The host can provide standard assembly references.
51: //The engine will use these references when compiling and
52: //executing the generated transformation class.
53: //--------------------------------------------------------------
54: public IList<string> StandardAssemblyReferences
55: {
56: get
57: {
58: return new string[]
59: {
60: //If this host searches standard paths and the GAC,
61: //we can specify the assembly name like this.
62: //---------------------------------------------------------
63: //"System"
64:
65: //Because this host only resolves assemblies from the
66: //fully qualified path and name of the assembly,
67: //this is a quick way to get the code to give us the
68: //fully qualified path and name of the System assembly.
69: //---------------------------------------------------------
70: typeof(System.Uri).Assembly.Location
71: };
72: }
73: }
74: //The host can provide standard imports or using statements.
75: //The engine will add these statements to the generated
76: //transformation class.
77: //--------------------------------------------------------------
78: public IList<string> StandardImports
79: {
80: get
81: {
82: return new string[]
83: {
84: "System"
85: };
86: }
87: }
88: //The engine calls this method based on the optional include directive
89: //if the user has specified it in the text template.
90: //This method can be called 0, 1, or more times.
91: //---------------------------------------------------------------------
92: //The included text is returned in the context parameter.
93: //If the host searches the registry for the location of include files,
94: //or if the host searches multiple locations by default, the host can
95: //return the final path of the include file in the location parameter.
96: //---------------------------------------------------------------------
97: public bool LoadIncludeText(string requestFileName, out string content, out string location)
98: {
99: content = System.String.Empty;
100: location = System.String.Empty;
101:
102: //If the argument is the fully qualified path of an existing file,
103: //then we are done.
104: //----------------------------------------------------------------
105: if (File.Exists(requestFileName))
106: {
107: content = File.ReadAllText(requestFileName);
108: return true;
109: }
110: //This can be customized to search specific paths for the file.
111: //This can be customized to accept paths to search as command line
112: //arguments.
113: //----------------------------------------------------------------
114: else
115: {
116: return false;
117: }
118: }
119: //Called by the Engine to enquire about
120: //the processing options you require.
121: //If you recognize that option, return an
122: //appropriate value.
123: //Otherwise, pass back NULL.
124: //--------------------------------------------------------------------
125: public object GetHostOption(string optionName)
126: {
127: object returnObject;
128: switch (optionName)
129: {
130: case "CacheAssemblies":
131: returnObject = true;
132: break;
133: default:
134: returnObject = null;
135: break;
136: }
137: return returnObject;
138: }
139: //The engine calls this method to resolve assembly references used in
140: //the generated transformation class project and for the optional
141: //assembly directive if the user has specified it in the text template.
142: //This method can be called 0, 1, or more times.
143: //---------------------------------------------------------------------
144: public string ResolveAssemblyReference(string assemblyReference)
145: {
146: //If the argument is the fully qualified path of an existing file,
147: //then we are done. (This does not do any work.)
148: //----------------------------------------------------------------
149: if (File.Exists(assemblyReference))
150: {
151: return assemblyReference;
152: }
153: //Maybe the assembly is in the same folder as the text template that
154: //called the directive.
155: //----------------------------------------------------------------
156: string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
157: if (File.Exists(candidate))
158: {
159: return candidate;
160: }
161: //This can be customized to search specific paths for the file
162: //or to search the GAC.
163: //----------------------------------------------------------------
164: //This can be customized to accept paths to search as command line
165: //arguments.
166: //----------------------------------------------------------------
167: //If we cannot do better, return the original file name.
168: return "";
169: }
170: //The engine calls this method based on the directives the user has
171: //specified in the text template.
172: //This method can be called 0, 1, or more times.
173: //---------------------------------------------------------------------
174: public Type ResolveDirectiveProcessor(string processorName)
175: {
176: //This host will not resolve any specific processors.
177: //Check the processor name, and if it is the name of a processor the
178: //host wants to support, return the type of the processor.
179: //---------------------------------------------------------------------
180: if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
181: {
182: //return typeof();
183: }
184: //This can be customized to search specific paths for the file
185: //or to search the GAC
186: //If the directive processor cannot be found, throw an error.
187: throw new Exception("Directive Processor not found");
188: }
189: //A directive processor can call this method if a file name does not
190: //have a path.
191: //The host can attempt to provide path information by searching
192: //specific paths for the file and returning the file and path if found.
193: //This method can be called 0, 1, or more times.
194: //---------------------------------------------------------------------
195: public string ResolvePath(string fileName)
196: {
197: if (fileName == null)
198: {
199: throw new ArgumentNullException("the file name cannot be null");
200: }
201: //If the argument is the fully qualified path of an existing file,
202: //then we are done
203: //----------------------------------------------------------------
204: if (File.Exists(fileName))
205: {
206: return fileName;
207: }
208: //Maybe the file is in the same folder as the text template that
209: //called the directive.
210: //----------------------------------------------------------------
211: string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
212: if (File.Exists(candidate))
213: {
214: return candidate;
215: }
216: //Look more places.
217: //----------------------------------------------------------------
218: //More code can go here...
219: //If we cannot do better, return the original file name.
220: return fileName;
221: }
222: //If a call to a directive in a text template does not provide a value
223: //for a required parameter, the directive processor can try to get it
224: //from the host by calling this method.
225: //This method can be called 0, 1, or more times.
226: //---------------------------------------------------------------------
227: public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
228: {
229: if (directiveId == null)
230: {
231: throw new ArgumentNullException("the directiveId cannot be null");
232: }
233: if (processorName == null)
234: {
235: throw new ArgumentNullException("the processorName cannot be null");
236: }
237: if (parameterName == null)
238: {
239: throw new ArgumentNullException("the parameterName cannot be null");
240: }
241: //Code to provide "hard-coded" parameter values goes here.
242: //This code depends on the directive processors this host will interact with.
243: //If we cannot do better, return the empty string.
244: return String.Empty;
245: }
246: //The engine calls this method to change the extension of the
247: //generated text output file based on the optional output directive
248: //if the user specifies it in the text template.
249: //---------------------------------------------------------------------
250: public void SetFileExtension(string extension)
251: {
252: //The parameter extension has a '.' in front of it already.
253: //--------------------------------------------------------
254: fileExtensionValue = extension;
255: }
256: //The engine calls this method to change the encoding of the
257: //generated text output file based on the optional output directive
258: //if the user specifies it in the text template.
259: //----------------------------------------------------------------------
260: public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
261: {
262: fileEncodingValue = encoding;
263: }
264: //The engine calls this method when it is done processing a text
265: //template to pass any errors that occurred to the host.
266: //The host can decide how to display them.
267: //---------------------------------------------------------------------
268: public void LogErrors(CompilerErrorCollection errors)
269: {
270: errorsValue = errors;
271: }
272: //This is the application domain that is used to compile and run
273: //the generated transformation class to create the generated text output.
274: //----------------------------------------------------------------------
275: public AppDomain ProvideTemplatingAppDomain(string content)
276: {
277: //This host will provide a new application domain each time the
278: //engine processes a text template.
279: //-------------------------------------------------------------
280: return AppDomain.CreateDomain("Generation App Domain");
281: //This could be changed to return the current appdomain, but new
282: //assemblies are loaded into this AppDomain on a regular basis.
283: //If the AppDomain lasts too long, it will grow indefintely,
284: //which might be regarded as a leak.
285: //This could be customized to cache the application domain for
286: //a certain number of text template generations (for example, 10).
287: //This could be customized based on the contents of the text
288: //template, which are provided as a parameter for that purpose.
289: }
290:
291: public ITextTemplatingSession CreateSession()
292: {
293: return Session;
294: }
295:
296: public ITextTemplatingSession Session
297: {
298: get;
299: set;
300: }
301: }
302: }
ITextTemplatingEngineHost 毫無疑問是模板解析引擎的主機;實現ITextTemplatingSessionHost 接口可以讓我們往模板中傳遞變量,它采用了asp.net 中的Session概念。如果我們不需要往模板中傳遞Session數據話,可以不實現這個接口。
調用實例一:
1: Smart.TextTemplating.CustomCmdLineHost host = new Smart.TextTemplating.CustomCmdLineHost();
2: Engine engine = new Engine();
3: host.Session = new TextTemplatingSession();
4: host.Session["count"] = 5;
5: host.TemplateFileValue = "tmp.tt";
6: string input = File.ReadAllText(host.TemplateFileValue);
7: string output = engine.ProcessTemplate(input, host);
8: File.WriteAllText(host.TemplateFileValue + ".txt", output);
模板文件內容(tmp.tt):
1: <#@ template debug="true" #>
2: <#@ parameter name="data" type="System.Object" #>
3:
4: <#
5: int count = Convert.ToInt32(data);
6: for (int i=0; i<count; i++)
7: {
8: WriteLine(i.ToString());
9: }
10: #>
調用實例二:
此種方法我封裝了一個單獨的類,加入了我的代碼收藏夾,實現了多文件生成,向模板文件傳遞數據等:
調用示例:
1: List<object> data = new List<object>();
2: for (int i = 0; i < 5; i++)
3: {
4: data.Add(i.ToString());
5: }
6:
7: Smart.TextTemplating.ParseTextTemplating parse = new Smart.TextTemplating.ParseTextTemplating(
8: "",
9: "view_",
10: ".txt",
11: data,
12: "tmp.tt");
13: parse.Parse();
我封裝的代碼(以后想用了直接調用就可以了):
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.CodeDom.Compiler;
6: using System.IO;
7: using Microsoft.VisualStudio.TextTemplating;
8:
9: namespace Smart.TextTemplating
10: {
11: public class ParseTextTemplating
12: {
13: #region 私有變量
14: private string _templateContent;
15: private string _saveRootPath;
16: private string _startFlag;
17: private string _extension;
18: private List<object> _data = new List<object>();
19: #endregion
20:
21: public ParseTextTemplating(string saveRootPath, string startFlag, string extension, List<object> data, string templatefilePath)
22: {
23: this._saveRootPath = saveRootPath;
24: this._startFlag = startFlag;
25: this._extension = extension;
26: this._data = data;
27:
28: if (string.IsNullOrEmpty(_saveRootPath))
29: {
30: _saveRootPath = AppDomain.CurrentDomain.BaseDirectory;
31: }
32: if (string.IsNullOrEmpty(_extension))
33: {
34: _extension = ".html";
35: }
36: if (string.IsNullOrEmpty(templatefilePath) || !File.Exists(templatefilePath))
37: {
38: this._templateContent = "";
39: }
40: else
41: {
42: this._templateContent = File.ReadAllText(templatefilePath);
43: }
44: }
45:
46: public void Parse()
47: {
48: foreach (object key in _data)
49: {
50: SmartTextTemplatingEngineHost host = new SmartTextTemplatingEngineHost();
51: host.Session = new TextTemplatingSession();
52: host.Session["data"] = key;
53: Engine engine = new Engine();
54: string content = engine.ProcessTemplate(_templateContent, host);
55: if (!string.IsNullOrEmpty(content) && host.Errors.Count == 0)
56: {
57: string filePath = string.Format("{0}{1}{2}{3}",
58: _saveRootPath,
59: _startFlag,
60: DateTime.Now.ToString("yyyyMMddHHmmss") + DateTime.Now.Millisecond.ToString("d4"),
61: _extension);
62: File.WriteAllText(filePath, content, Encoding.UTF8);
63: }
64:
65: foreach (CompilerError er in host.Errors)
66: {
67: File.AppendAllText(_saveRootPath + "error.txt",
68: er.ToString() + "\r\n\r\n",
69: Encoding.UTF8);
70: }
71: }
72: }
73: }
74: }
模板文件傳遞變量:<#@ parameter name="data" type="System.Object" #> 可以接口主程序傳遞過來的onject類型的變量數據。
2.3運行結果
生成的結果文件:
文件內容:
三、總結和展望
T4模板引擎是非常強大的,至於功能都強大到何處還需要我們深入的仔細了解。細心的同學可能已經發現了,T4模板引擎雖然強大,但是寫模板時卻沒有一個像VS那樣的帶智能感知的代碼提示工具啊?放心吧!這個問題已經不是問題了,安裝一個VS擴展就行了,看截圖:
下載地址我就不貼了,文件名字是“tangibleT4EditorPlusModellingToolsSetup.msi”,相信有了Google和文件的名字找到官方網站的主頁和下載地址對你已經不是問題了,如果從VS擴展管理器安裝的話,名字是(推薦了2個):t4 editor 和visual t4。
什么?T4模板的語法太古怪!寫起來太麻煩!!寫起來太累!程序員對代碼總是這么苛刻,好吧,滿足你的苛刻要求,不過要下次才能向你介紹了:
下次向你介紹的基於Net的模板解析引擎名字是:Razor
調用示例:
http://www.codeplex.com/上有個項目,名字是:RazorEngine 地址:http://razorengine.codeplex.com/
Razor的語法預覽:
Razor語法詳細介紹:http://weblogs.asp.net/scottgu/archive/2010/07/02/introducing-razor.aspx





