
Introduction
ASP.NET provides us with many easy ways to build our web system, especially the code-behind technique which amazingly allows a separation of layout and code. However, ASP.NET also offers some mechanisms to allow you to build a custom programming model more than that offered by code-behind. One mechanism is HTTP Handler which gives you a means of interacting with the low-level request and response services of the IIS Web server, and provides functionality much like ISAPI extensions but with a simpler programming model. Great! This mechanism is just what I do like most, because it gives me a nice feeling that everything is under my own control and I'm free.
But when you are writing custom HTTP handlers, hard-coding the page layout is boring and error-prone. We do need a way to separate layout from code. Thus, class TmplParser, TemplatePool etc. were born. The main job of class TmplParser is to parse a layout template file with some tags and labels whose rules are simple and defined by myself :-). The class TemplatePool is used to buffer a set of templates, which can reduce the I/O operations with less template file reading, and improve the performance. I will illustrate how to use them to separate layout from code, later in this article.
Notes: It's the first time that I programmed ASP.NET, the first time that I programmed in C#, the first time that I touched IIS, the first time that I submitted an article to Code Project. So, there must be some problems or something not good enough in the article and code. And I do welcome any feedback. Thanks!
Using the code
First, I will tell you the rules of using the parser and some basic information as well.
- A template file consists of two and only two basic elements. They are
blockandlabel. A block is defined with begin-flag<!--BEGIN: BLOCKNAME -->and end-flag<!--END: BLOCKNAME -->. A label is defined as{LABELNAME}. Let's look at an example template file example.html to make the concepts more clear to you.example.html
<html> <head> <title> Example </title> </head> <body> <table> <!--BEGIN: FORMAT1 --> <tr><th>{THEAD1}</th></tr> <!--BEGIN: ROW1 --> <tr><td>{VALUE1}</td></tr> <!--END: ROW1 --> <!--END: FORMAT1 --> <!--BEGIN: FORMAT2 --> <tr><th>{THEAD1}</th><th>{THEAD2}</th></tr> <!--BEGIN: ROW2 --> <tr><td>{VALUE1}</td><td>{VALUE2}</td></tr> <!--END: ROW2 --> <!--END: FORMAT2 --> </table> </body> </html>In the template file example.html, there are four blocks:
FORMAT1,ROW1,FORMAT2andROW2. BlockFORMAT1has one label:THEAD1; blockROW1has one label:VALUE1; blockFORMAT2has two labels:THEAD1andTHEAD2; blockROW2has two labels:VALUE1andVALUE2.FORMAT1andFORMAT2are parent blocks ofROW1andROW2respectively. Actually, there is one more block. It's theroot block, namely the template file itself. Let's call itDOCUMENT-BLOCK. - Rules of Template File Definition
- A block name must be unique in one template file. It's not only for code readability but also for easy use. And my principle is: the simpler, the better.
- A block can be a parent by enclosing other blocks. But any two blocks can not overlap. And there is no sibling relation, but parent-children relation is maintained between blocks.
- A Deep Look
When the template file is parsed by
TmplParseras the following code:// tmplDir is the path of the directory in which example.html is placed. // It's in the format like this: E:/Demo/tmpl/ // 50 is the capacity of the pool. // The pool uses LRU algorithm for template replacement. TemplatePool.Singleton(tmplDir, 50); // What GetTemplate does is to load the template file, // create and initialize a new instance // of TmplParser for this template file, // do the parsing and return it if the filename // passed is not found in the pool, // otherwise just return a clone // of the instance found in pool. // It's thread-safe. // ITemplate is an interface inherited by TmplParser. ITemplate tmpl = TemplatePool.Singleton().GetTemplate("example.html"); // the second time, just return a cloned instance // for template example.html // The code below is just for instruction. ITemplate tmpl2 = TemplatePool.Singleton().GetTemplate("example.html");
Then five
BlockParser(a private class nested inTmplParser) instances will be built and maintained inTmplParser. The contents of the five blocks are below:ROW1<tr><td>{VALUE1}</td></tr>ROW2<tr><td>{VALUE1}</td><td>{VALUE2}</td></tr>FORMAT1<tr><th>{THEAD1}</th></tr> <tag:ROW1/>FORMAT2<tr><th>{THEAD1}</th><th>{THEAD2}</th></tr> <tag:ROW2/>DOCUMENT-BLOCK<html> <head> <title> Example </title> </head> <body> <table> <tag:FORMAT1/> <tag:FORMAT2/> </table> </body> </html>Once a child block (example:
ROW1) is Out, its current content will be placed just before the tag<tag:BLOCKNAME/>(example:<tag:ROW1/>) in its parent block and it will return to the original raw content.After the following code is executed, the content of
DOCUMENT-BLOCKwill be ...ITmplBlock tmplROW1 = tmpl.ParseBlock("ROW1"); ITmplBlock tmplFORMAT1 = tmpl.ParseBlock("FORMAT1"); //- 1: child block(ROW1) is not Out // Replace the label {THEAD1} in block FORMAT1 with OS. tmplFORMAT1.Assign("THEAD1", "OS"); // Replace the label {VALUE1} in block ROW1 with WinXP. tmplROW1.Assign("VALUE1", "WinXP"); // Be careful, the code below is commented deliberately. // So block ROW1 content won't be embeded into its parent block FORMAT1. // tmplROW1.Out(); tmplFORMAT1.Out(); //- 2: parent block(FORMAT1) is Out before child block(ROW1) // you won't see the ROW1 content in the result either. tmplFORMAT1.Out(); tmplROW1.Assign("VALUE1", "WinXP"); tmplROW1.Out(); //- 3: Replace the child block's label in the parent // block by outing child block first // It does work. tmplROW1.Out(); tmplROW1.Out(); tmplFORMAT1.Assign("THEAD1", "Tools"); // {VALUE1} is a label in block ROW1. tmplFORMAT1.Assign("VALUE1", "Visual Studio .NET"); //- 4: General usage ITmplBlock tmplROW2 = tmpl.ParseBlock("ROW2"); ITmplBlock tmplFORMAT2 = tmpl.ParseBlock("FORMAT2"); tmplFORMAT2.Assign("THEAD1", "Country"); tmplFORMAT2.Assign("THEAD2", "City"); tmplROW2.Assign("VALUE1", "China"); tmplROW2.Assign("VALUE2", "Pekin"); tmplROW2.Out(); tmplROW2.Assign("VALUE1", "China"); tmplROW2.Assign("VALUE2", "Shanghai"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Tianjin"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Chongqing"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Shenzhen"); tmplROW2.Out(); tmplFORMAT2.Assign("VALUE1", "China"); tmplFORMAT2.Out(); // Calling the method ParseBlock, the one without // parameters, can get DOCUMENT-BLOCK. ITmplBlock tmplDoc = tmpl.ParseBlock(); tmplDoc.Out(); // the next step will usually be like the code below // which just sends the result content to the client. Response.Write(tmplDoc.BlockString);
Then the result content of
DOCUMENT-BLOCKis shown as follows:<html> <head> <title> Example </title> </head> <body> <table> <!-- 1 --> <tr><th>OS</th></tr> <!-- 2 --> <tr><th>{THEAD1}</th></tr> <!-- 3 --> <tr><th>Tools</th></tr> <tr><td>Visual Studio .NET</td></tr> <tr><td>Visual Studio .NET</td></tr> <!-- 4 --> <tr><th>Country</th><th>City</th></tr> <tr><td>China</td><td>Pekin</td></tr> <tr><td>China</td><td>Shanghai</td></tr> <tr><td>China</td><td>Tianjin</td></tr> <tr><td>China</td><td>Chongqin</td></tr> <tr><td>China</td><td>Shenzhen</td></tr> </table> </body> </html>The comments in the result content are added just for your convenience to contrast to the C# code above. They don't exist in the real result content. As you all see, the presentation code (HTML) can be reused much with this technique.
Now, I believe that you have known much about this technique, which really will makes me happy :-). One point should be accentuated. That is: with one block's Out method being not called, its content won't be placed in its parent.
Second, I will illustrate how to use the code with a simple demo project. Actually, it's a framework shown with the template parser and pool more than a usage instruction. However, I'll just list the main code. Details should be viewed in the source code by yourself. All the source code can be downloaded through the link above.
- Build a web site called Demo whose virtual directory is the one that you extract the demo project's source files to. Below I will use $DEMO to refer to this virtual directory.
- In the IIS, disable all the access privilege of the directory $DEMO/tmpl under which our web page template files are placed, and $DEMO/src under which our source code files are placed. It just prevents clients from accessing to any resources under the two directories in any way.
- View the config file web.config. Each request whose path matches *do.aspx will be handled by
Demo.Handler.Controller. $DEMO is the absolute path of your website virtual directory. For example, if your virtual directory is E:/MyWebsite/demo, then the config below should be:<add key="tmpldir" value="E:/MyWebsite/demo/tmpl/"/>
<configuration> <system.web> ... <httpHandlers> <add verb="*" path="*do.aspx" type="Demo.Handler.Controller, demo"/> </httpHandlers> ... </system.web> <appSettings> <add key="tmpldir" value="$DEMO/tmpl/"/> <add key="capacity" value="50"/> </appSettings> </configuration> - The following code in the file Global.asax.cs is to create a single instance of
TemplatePoolwith the singleton pattern.public class Global : System.Web.HttpApplication { ... // // application's initialization // protected void Application_Start(Object sender, EventArgs e) { TemplatePool.Singleton( ConfigurationSettings.AppSettings.Get("tmpldir"), Int32.Parse(ConfigurationSettings.AppSettings.Get("capacity"))); } ... }
What
TemplatePool.Singletondoes is shown below. As you all see, it is thread-safe and uses the double-check trick to improve the performance. And the pool instance will exist through the process life.public sealed class TemplatePool : ITmplLoader { ... public static TemplatePool Singleton(string tmplDir, int capacity) { if(pool == null) { lock(objLock) { if(pool == null) pool = new TemplatePool(tmplDir, capacity); } } return pool; } ... private static TemplatePool pool = null; private static object objLock = new object(); }
- View class
Demo.Handler.Controllerto see how the handler deals with the request.public class Controller : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { ... // A more sophisticated way is to put the info, // such as Demo.Application.MenuDealer, menupanel.html etc, // into a config(an XML file or a table in db or others). IApplication theApp = (IApplication) Activator.CreateInstance(Type.GetType("Demo.Application.MenuDealer")); theApp.Init("menupanel.html", "login.html"); theApp.Session = context.Session; theApp.DoProcess(context.Request.Params); context.Response.Write(theApp.Out); } ... }
- View class
Demo.Application.MenuDealerto see what methodDoProcessdoes. It just composites the data with the template to generate a string to be responded.public class MenuDealer : Dealer { ... // generate menus string oldModName = ""; string modname = null; string username = reqParams.Get("username"); Hashtable htMenu = null; ITmplBlock tmplMenuFrm = OutPageTmpl.ParseBlock("MENUFRM"); ITmplBlock tmplMenu = OutPageTmpl.ParseBlock("MENU"); int modIdx = 0; Operation operation = new Operation(); operation.GetMenuStart(); string action = "do.aspx?opname="; while((htMenu = operation.GetMenuNext()) != null) { modname = (string)htMenu["modname"]; if(!modname.Equals(oldModName)) { if(modIdx > 0) tmplMenuFrm.Out(); tmplMenuFrm.Assign("IDIDX", modIdx.ToString()); tmplMenuFrm.Assign("MODULE", modname); oldModName = modname; modIdx++; } string opname = (string)htMenu["opname"]; tmplMenu.Assign("REQUEST", opname + action + opname); tmplMenu.Assign("MENU", (string)htMenu["opvalue"]); tmplMenu.Out(); } if(modIdx > 0) tmplMenuFrm.Out(); tmplDoc = OutPageTmpl.ParseBlock(); tmplDoc.Assign("USERNAME", username); ... }
- View class
Demo.Application.Dealerto see what the propertiesOutandOutPageTmpldo.public abstract class Dealer : IApplication { ... public string Out { get { if(tmplDoc == null) return null; tmplDoc.Out(); return tmplDoc.BlockString; } } protected ITemplate GetTemplate(string tmplFileName) { return (ITemplate) (TemplatePool.Singleton().GetTemplate(tmplFileName)); } protected ITemplate OutPageTmpl { get { if(outPageTmpl == null) outPageTmpl = GetTemplate(outPage); return outPageTmpl; } } ... }
At last, build the system with Visual Studio .NET, and you will see what the image above shows. And I believe you will find more useful code in the demo project's source code. As I already said, the demo project is just a framework for a small project. May you like it!
Points of Interest
- When I implement class
TemplatePool, I need a class for linked list. But, I can't find one in the namespaceSystem.Collections. There may be someone shouting why not useArrayList. Good question! But I guess thatArrayListwill do the elements copying to move their positions when operationsInsertandRemoveare being done, which does raise performance penalty when the two operations are done frequently in LRU algorithm. So, I implemented a simple double linked listAgemo.Utility.DoubleLinkedListto meet my needs. It's really simple. Again, the simpler, the better. - The non-recursive DFS algorithm is used to disassemble the template into blocks.
- The
HttpHandlerclass must inherit the interfaceIRequiresSessionStatewhich is just a marker interface if it will use the session object. I was harassed for a long time by this problem.
Agemo
| Click here to view Agemo's online profile. |
本文介绍了ASP.NET构建Web系统的方法,着重讲述了HTTP Handler机制。编写自定义HTTP处理程序时,为分离布局与代码,引入了相关类。还说明了模板文件的规则、解析过程,以及使用单例模式创建模板池等内容,最后提及实现中的链表使用和DFS算法等。
9450

被折叠的 条评论
为什么被折叠?



