- Download demo project - 27.4 Kb
- Download source - 5.09 Kb

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 `
and end-flag
. 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
1<html>
2<head>
3<title> Example </title>
4</head>
5<body>
6<table>
7<!--BEGIN: FORMAT1 -->
8<tr><th>{THEAD1}</th></tr>
9<!--BEGIN: ROW1 -->
10<tr><td>{VALUE1}</td></tr>
11<!--END: ROW1 -->
12<!--END: FORMAT1 -->
13<!--BEGIN: FORMAT2 -->
14<tr><th>{THEAD1}</th><th>{THEAD2}</th></tr>
15<!--BEGIN: ROW2 -->
16<tr><td>{VALUE1}</td><td>{VALUE2}</td></tr>
17<!--END: ROW2 -->
18<!--END: FORMAT2 -->
19</table>
20</body>
21</html>
In the template file example.html , there are four blocks: FORMAT1 , ROW1 , FORMAT2 and ROW2 . Block FORMAT1 has one label: THEAD1 ; block ROW1 has one label: VALUE1 ; block FORMAT2 has two labels: THEAD1 and THEAD2 ; block ROW2 has two labels: VALUE1 and VALUE2 . FORMAT1 and FORMAT2 are parent blocks of ROW1 and ROW2 respectively. Actually, there is one more block. It's the root block , namely the template file itself. Let's call it DOCUMENT-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 TmplParser as 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 in TmplParser ) instances will be built and maintained in TmplParser . The contents of the five blocks are below:
ROW1
1<tr><td>{VALUE1}</td></tr>
ROW2
1<tr><td>{VALUE1}</td><td>{VALUE2}</td></tr>
FORMAT1
1<tr><th>{THEAD1}</th></tr>
1<tag:row1></tag:row1>
FORMAT2
1<tr><th>{THEAD1}</th><th>{THEAD2}</th></tr>
1<tag:row2></tag:row2>
DOCUMENT-BLOCK
1<html>
2<head>
3<title> Example </title>
4</head>
5<body>
6<table>
7<tag:format1></tag:format1>
8<tag:format2></tag:format2>
9</table>
10</body>
11</html>
Once a child block (example: ROW1 ) is Out, its current content will be placed just before the tag `
1<tag:blockname></tag:blockname>
(example:
1<tag:row1></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-BLOCK will 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-BLOCK is shown as follows:
1<html>
2<head>
3<title> Example </title>
4</head>
5<body>
6<table>
7<!-- 1 -->
8<tr><th>OS</th></tr>
9<!-- 2 -->
10<tr><th>{THEAD1}</th></tr>
11<!-- 3 -->
12<tr><th>Tools</th></tr>
13<tr><td>Visual Studio .NET</td></tr>
14<tr><td>Visual Studio .NET</td></tr>
15<!-- 4 -->
16<tr><th>Country</th><th>City</th></tr>
17<tr><td>China</td><td>Pekin</td></tr>
18<tr><td>China</td><td>Shanghai</td></tr>
19<tr><td>China</td><td>Tianjin</td></tr>
20<tr><td>China</td><td>Chongqin</td></tr>
21<tr><td>China</td><td>Shenzhen</td></tr>
22</table>
23</body>
24</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:
1<add key="tmpldir" value="E:/MyWebsite/demo/tmpl/"></add>
1<configuration>
2<system.web>
3 ...
4 <httphandlers>
5<add path="*do.aspx" type="Demo.Handler.Controller, demo" verb="*"></add>
6</httphandlers>
7 ...
8 </system.web>
9<appsettings>
10<add key="tmpldir" value="$DEMO/tmpl/"></add>
11<add key="capacity" value="50"></add>
12 </appsettings>
13</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.Singleton does 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.
---|---