缘由
用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