Code reusuability is (or at least should be) one of the major goals of any good
object-oriented programmer. Reusuable code cuts development time and also
minimizes the chances of introducing new bugs into an application. Fortunately,
the ASP.NET framework has made code reusuability easier and much more elegant
than it was in classic ASP. I, personally, am very grateful for all of the
wonderful techniques that we have at our disposal for code reusuability
(inheritance, ASP.NET server controls, user controls, etc..).
One area though, where I feel that ASP.NET still leaves a bit to be desired is
reusuability for ASP.NET pages at the UI level. Several techniques exist to
combat this problem, one of the most common being a technique that is based on
overriding a base page's OnRender() method. By inheriting all of the pages in
your web application from the base page, you can ensure a consistent look and
feel across the entire application.
For me though, there is two major drawbacks to this approach. A base page's
OnRender() method is nested pretty far into your application, so what happens if
you decide you want to change something about the UI once your application has
been deployed? Well, you have to edit the base page's OnRender() method, and
editing code means recompiling, and recompiling means retesting and redeploying,
none of which sound much like a good time to me.
Another stike against this approach is that is 100% reliant upon a code file (be
it C# or VB). In other words, there is no ASP.NET page (or Code-Front, as I like
to call it) associated with the base page. Any UI components that you want to
exist across your web app, must be programatically built and added to the base
page, and I think this is inconvienent (at best).
What would be nice, is if there were a technique whereby every page in your web
application could inherit not only the *functionality* of a base page, but also
the UI of that base page. Furthermore, the UI of the base page would be as
simple to modify as editing a basic HTML/ASP.NET file.
Well believe it or not, it *is* possible, and in this article, I'm going to show
you my technique for doing just that.
First things first, we must decide what that "basic look & feel" for our web
application is going to be. I wanted to keep things simple for this article, so
I whipped up this HTML and corresponding CSS that creates a header, footer, and
left-side navbar for a ficticious web site about cooking.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Cooking: it's more fun than you think!</title>
<style type = "text/css">
body
{
font-family: verdana; font-size: 11px; color: #676767;
margin: 0px; padding: 0px; background-color: #cccccc;
}
#Container
{
position: relative;
width: 760px;
margin: auto;
margin-top: 10px;
background-color: #003366;
}
#Header
{
height: 80px; line-height: 80px;
background-color: #336699;
font-size: 18px; font-weight: bold; color: #ffffff;
text-align: center;
border-bottom: solid 1px #cccccc;
}
#NavBar
{
position: absolute; top: 80px; left: 0px;
width: 130px; height: 300px;
padding: 5px;
color: #ffffff;
}
#PageContent
{
background-color: #ffffff;
position: relative; top: 0px; left: 140px;
padding: 10px;
width: 600px;
height: 300px;
}
#Footer
{
padding-left: 10px;
height: 30px; line-height: 30px;
background-color: #336699;
font-weight: bold; color: #ffffff;
text-align: center;
border-top: solid 1px #cccccc;
border-bottom: solid 1px #cccccc;
clear: both;
}
</style>
</head>
<body>
<div id = "Container">
<div id = "Header">
Cooking: it's more fun than you think!
</div>
<div id = "NavBar">
<b>Navigation:</b>
<ul>
<li></li>
<li>Searing Chicken</li>
<li>Roasting Pork</li>
<li>Onion Soup</li>
</ul>
</div>
<div id = "PageContent">
<asp:Label id = "lblBaseLabel" runat = "server">Hi, I'm the Base Label!</asp:Label>
<br />
<asp:PlaceHolder id = "phMainContent" runat = "server" />
</div>
<div id = "Footer">
Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
</div>
</div>
</body>
</html>
Of course, the above would be better if the CSS were placed in an external stylesheet like so:
But I wanted the reader to be able to get a glimpse of the CSS before I hid it
in an external file.
The next step in building our Page Template is to place the above ASP.NET/HTML
in an XML file. That's right, an XML file. Something like this would work just
fine:
<?xml version="1.0" encoding="utf-8" ?>
<PageTemplate>
!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Cooking: it's more fun than you think!</title>
<link rel = "stylesheet" type = "text/css" href = "./_stylesheets/layout.css" />
</head>
<body>
<div id = "Container">
<div id = "Header">
Cooking: it's more fun than you think!
</div>
<div id = "NavBar">
<b>Navigation:</b>
<ul>
<li></li>
<li>Searing Chicken</li>
<li>Roasting Pork</li>
<li>Onion Soup</li>
</ul>
</div>
<div id = "PageContent">
<asp:Label id = "lblBaseLabel" runat = "server">Hi, I'm the Base Label!</asp:Label>
<br />
<asp:PlaceHolder id = "phMainContent" runat = "server" />
</div>
<div id = "Footer">
Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
</div>
</div>
</body>
</html>
]]>
</PageTemplate>
You can see that I haven't done anything spectacular here. Just wrapped the
whole thing inside of an XML tag <PageTemplate>. You'll also notice the CDATA block. I use this just to ensure that I won't get any XML errors later on when
I'm reading the file programatically (of course, this means, it's up to *you* to
ensure your HTML is XHTML compliant -- but that's another article).
Finally, notice the <asp:PlaceHolder id = "phMainContent" runat = "server" />
control. This is critical. This PlaceHolder represents the *exact spot* that any
non-templated content will be displayed. In other words, the phMainContent PlaceHolder will hold all of the controls that you will eventually define on the
inheriting page.
We've now got our XML template file written and ready to go. You can think of
this file as the corresponding ASP.NET page (or Code-Front) to the base page
class that we're going to write shortly.
[ Part 2: Creating the base page class ]
The concept I'm going to use to create this Page Template is as follows:
1. Grab all of the controls from the inheriting page (remove them), and place
them in a temporary control array.
2. Read the HTML from the XML template file into a string variable.
3. Parse string variable (which contains HTML) into series of ASP.NET Literal
Controls, and/or HtmlControls, and/or WebControls, and (if applicable) 3rd party
controls, and add the newly parsed control to the Page's ControlCollection.
4. Take all of the controls that were placed in the temporary control array
(from Step 1) and add them to the PlaceHolder, phMainContent.
I know this may seem a bit convoluted, but I'm confident that it will become
clearer as we go, so let's get started by first creating a BasePge class and
then implementing Step 1.
The basic shell of the BasePage class should look something like this:
using System;
using System.Xml;
using System.Web.UI;
using System.Reflection;
using System.Web.UI.WebControls;
namespace sstchur.web.Pages
{
public class BasePage : System.Web.UI.Page
{
// Protected Variables (UI Components)
protected Label lblBaseLabel;
// Private Variables
private string m_strTemplateFile;
// Public Properties
public string TemplateFile
{
set { m_strTemplateFile = value; }
}
// Constructor
public BasePage()
{
// Initialize the TemplateFile in case one is not specified
m_strTemplateFile = "~/_templates/standardpage.xml";
}
protected override void OnInit(EventArgs e)
{
// Load the XHTML Template
InvokePageTemplate();
// Initialize any components/variables specific to this class (or its base)
InitializeComponent();
// Let the base class do its thing
base.OnInit (e);
}
private void InitializeComponent()
{
// Wireup Page_Load Event
this.Load += new EventHandler(Page_Load);
}
private void Page_Load(object sender, EventArgs e)
{
// Put any needed Page_Load functionality here
}
// Here is where all the tricky stuff happens
private void InvokePageTemplate()
{
// Here is where the bulk of the BasePage's implementation will eventually go
}
}
}
One of the first things you'll notice is a few non-orthodox using statements:
namely, using System.Xml and using System.Reflection. You might have already
guessed that we'd need access to Xml functionality since our TemplateFile is
written in XML, but the System.Reflection namespace should be a bit of a
surprise. The reason it's needed won't become clear until a little later on, so
I'll postpone its discussion until we get to the code that actually make use of
it.
Beyond that, there's nothing earth-shattering in the BasePage class. You can see
that we inherit from the standard System.Web.UI.Page class, and we go ahead and
wire up the Page_Load method using the default technique that VS.NET implements
for you when you create a new WebForm (overriding the OnInit() method and
calling InitializeComponent()).
You'll notice however, that prior to calling InitializeComponent(), we have a
call to a custom, private, method: InvokePageTemplate() (which we have yet to
implement). If you remember back to the 5 steps I outlined for creating the BasePage class, step 1 was grabbing all of the controls from the inheriting page
(removing them), and placing them in a temporary control array.
private void InvokePageTemplate()
{
// Copy off inheriting page's control into a temporary Control[] array
Control[] controls = new Control[this.Controls.Count];
this.Controls.CopyTo(controls, 0);
this.Controls.Clear();
}
Step 2 was reading the HTML/XHTML from the XML template file into a string
variable:
private void InvokePageTemplate()
{
// Copy off inheriting page's control into a temporary Control[] array
Control[] controls = new Control[this.Controls.Count];
this.Controls.CopyTo(controls, 0);
this.Controls.Clear();
// Load the XML tempalte file into an XmlDocument object
doc = new XmlDocument();
doc.Load(Server.MapPath(m_strTemplateFile));
// Place the content's of the tag into a string variable
XmlElement root = doc.DocumentElement;
XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
string strTemplate = nodeTemplate.InnerText;
}
Step 3 was parsing the string variable (which contains HTML) into series of ASP.NET Literal Controls, and/or HtmlControls, and/or WebControls, and (if
applicable) 3rd party controls, and adding the newly parsed control to the
Page's ControlCollection. This is surprisingly easy to do with the Page.ParseControl() method. What surprised me the most about this method, was
that it does not need to be called repeatedly. Rather, any nested controls in
the string to be parsed, will become child controls of their containing controls
(essentially the same way .NET parses your ASP.NET page!).
private void InvokePageTemplate()
{
// Copy off inheriting page's control into a temporary Control[] array
Control[] controls = new Control[this.Controls.Count];
this.Controls.CopyTo(controls, 0);
this.Controls.Clear();
// Load the XML tempalte file into an XmlDocument object
doc = new XmlDocument();
doc.Load(Server.MapPath(m_strTemplateFile));
// Place the content's of the tag into a string variable
XmlElement root = doc.DocumentElement;
XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
string strTemplate = nodeTemplate.InnerText;
// Parse the HTML/XHTML contained in the strTemplate string; add the parsed controls to the Page's ControlCollection.
Control ctrlTemplate = Page.ParseControl(strTemplate);
this.Controls.Add(ctrlTemplate);
}
Step 4 was taking all of the controls that were placed in the temporary control
array (from Step 1) and adding them to the PlaceHolder, phMainContent. Before we
can do this though, we need a reference to the phMainContent PlaceHolder. This
is straight-forward enough, so let's tackle everything at once:
private void InvokePageTemplate()
{
// Copy off inheriting page's control into a temporary Control[] array
Control[] controls = new Control[this.Controls.Count];
this.Controls.CopyTo(controls, 0);
this.Controls.Clear();
// Load the XML tempalte file into an XmlDocument object
doc = new XmlDocument();
doc.Load(Server.MapPath(m_strTemplateFile));
// Place the content's of the tag into a string variable
XmlElement root = doc.DocumentElement;
XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
string strTemplate = nodeTemplate.InnerText;
// Parse the HTML/XHTML contained in the strTemplate string; add the parsed controls to the Page's ControlCollection.
Control ctrlTemplate = Page.ParseControl(strTemplate);
this.Controls.Add(ctrlTemplate);
// Get a reference to the phMainContent PlaceHolder
PlaceHolder phMainContent = (PlaceHolder)ctrlTemplate.FindControl("phMainContent");
// Add each control from the inheriting page (saved from Step 1) to the phMainContent's ControlCollection
foreach (Control c in controls)
phMainContent.Controls.Add(c);
}
At this point, we *could* be done. If you create a new ASP.NET page with
literally NOTHING in it (save for an <%@ Page ... %> directive) which inherits
from sstchur.web.Pages.BasePage, you'll have a working ASP.NET page that looks
just like the template file we created way back at the beginning of this
article. You can go ahead an try it if you want (don't forget to compile
everything though).
The only problem at this point, is that we don't have access to any of the base
page's components. You may have noticed that I added a Label to the template
file, whose ID was lblBaseLabel and whose default Text was "Hi, I'm the Base
Label!" I did this specifically for demonstration purposes. We obviously want to
be able to access the base page's components (like lblBaseLabel), and with our
BasePage class in its current state, we can't do that. ASP.NET will let you try
alright, but you'll get a NullReferenceException, and that's never good.
There are two solutions that I can think of, both are similar, but one is much
more convenient. The first way, is to simply use FindControl() to obtain a
reference to a control with a given ID. In the case of our lblBaseLabel, the
code (which would be placed in the inheriting page) would look something like
this:
Label baseLabel = (Label)Page.FindControl("lblBaseLabel);
baseLabel.Text = "Overridden value";
However, having to call FindControl EVERY time we want to access one of the base
page's controls, is sloppy, repetitive, and I dunno... just all around bad.
A much better solution, would be one that would allow us to access the base
page's components via base.{ComponentId} where {ComponentId} is the Id of the
component we want to access (in our case, base.lblBaseLabel).
How do we do this? Why, with Reflection of course! While a detailed discussion
of Reflection is far, far beyond the scope of this article, I'm going to try to
touch on just the bare minimum required to understand the reflection-based code
in our BasePage class. A good start would be to try to obtain a thorough
understanding of just what it is we're trying to accomplish. Let's recap:
- We have a BasePage class which all of our ASP.NET pages will inherit from.
- Inheriting from the BasePage class mean inheriting not only functionality, but
also UI components contained within the XML template file.
- If we want to be able to access any of the components from XML template file,
in the BasePage, then we need to add a protected member variable to the
BasePage, whose variable name is the same as the ID of the ASP.NET component in
the XML template file.
- We also want each protected member variable of the BasePage to be available to
inheriting pages (and not throw back NullReferenceExceptions when we try to
access them).
- Finally, we already know that in order to avoid a NullReferenceException, one
solution is a call to FindControl().
If we take all of the requirements that I've outlined above, the logic to
implement it can be summarized as follows:
For each protected member variable in the BasePage, do the following:
-- Determine the variable's name (as far as the compiler is concerned).
-- Call FindControl(variableName) to get a reference to the control whose Id is
the same as that variableName.
-- Set the value of the protected member variable equal to the control that was
returned with the call to FindControl()
Sounds complicated doesn't it? Well, to be truthful, is kind of is. Fortunately
though, the .NET framework wraps up a good portion of the complexity in the
System.Reflection namespace, which makes what we're trying to do a lot less
daunting that it might at first seem.
The first step is to determine what protected member variables have been defined
in our BasePage. We can do this by using the FieldInfo class. In our case, I'm
going to create an array of FieldInfo objects to hold information about each
variable defined in our base page. Then, I'm going to create a Type object which
is of the type sstchur.web.Pages.BasePage. Finally, I'll call GetFields(...) on
my type object and assign the resulting array to the FieldInfo array I created a
moment earlier. It looks something like this:
FieldInfo[] fieldInfo;
Type myType = typeof(sstchur.web.Pages.BasePage);
fieldInfo = myType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
The BindingFlags that were passed into the GetFields(...) method are just a way
of specifying which types of variables we want to retrieve information about. In
our case, we're only interested in NonPublic (protected actually, but we'll get
to that in a minute), instance variables that have been explicitely declared.
Once we've got our fieldInfo array populated, we can loop through it and take a
crack at those few remaining tasks:
FieldInfo[] fieldInfo;
Type myType = typeof(sstchur.web.Pages.BasePage);
fieldInfo = myType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
for (int i = 0; i < fieldInfo.Length; i++)
{
if (!fieldInfo[i].IsPrivate)
{
string id = fieldInfo[i].Name;
Control c = ctrlTemplate.FindControl(id);
if (c != null)
fieldInfo[i].SetValue(this, c);
}
}
In the above for loop, we simply check to make sure the current variable is not
private (we pulled back NonPublic fields with our call to GetFields. So a
variable that isn't public and isn't private, much be protected). Next, we
retrieve the variable Name, and assign it to the string variable id. Finally, we
attempt to get a reference to a Control object by calling FindControl(id). If
the control that's returned is not null, we assign it to the current variable
(field) in our for loop.
And that's all there is too it. See, now that wasn't so bad was it?
Now, I know what a lot of you much be thinking. Doesn't this add a tremendous
amount of overhead to your pages? I would suspect that the additional overhead
probably is significant. However, in my testing (which was far from extensive)
the pages seemed to perform pretty darned well. Infact, they performed well
enough, that the company I work for agreed to use this concept in a web
application we're working on.
The only thing I can say with regards to overhead is to do some benchmarking of
your own. If the numbers don't satisfy you, you can always elect to use a
different technique for page templates. At the very least, you'll have gotten to
read a stunningly interesting article by yours truely :-)
[ Part 3 - Putting it all together ]
So now we've got pretty much everything we need. Let's take a look at each of
the completed files that would go into an example page:
<?xml version="1.0" encoding="utf-8" ?>
<PageTemplate>
!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Cooking: it's more fun than you think!</title>
<link rel = "stylesheet" type = "text/css" href = "./_stylesheets/layout.css" />
</head>
<body>
<div id = "Container">
<div id = "Header">
Cooking: it's more fun than you think!
</div>
<div id = "NavBar">
<b>Navigation:</b>
<ul>
<li></li>
<li>Searing Chicken</li>
<li>Roasting Pork</li>
<li>Onion Soup</li>
</ul>
</div>
<div id = "PageContent">
<asp:Label id = "lblBaseLabel" runat = "server">Hi, I'm the Base Label!</asp:Label>
<br />
<asp:PlaceHolder id = "phMainContent" runat = "server" />
</div>
<div id = "Footer">
Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
</div>
</div>
</body>
</html>
]]>
</PageTemplate>
-- CSS file -- (note that I put all of my CSS files in a subdirectory ./_stylesheets)
body
{
font-family: verdana; font-size: 11px; color: #676767;
margin: 0px; padding: 0px; background-color: #cccccc;
}
#Container
{
position: relative;
width: 760px;
margin: auto;
margin-top: 10px;
background-color: #003366;
}
#Header
{
height: 80px; line-height: 80px;
background-color: #336699;
font-size: 18px; font-weight: bold; color: #ffffff;
text-align: center;
border-bottom: solid 1px #cccccc;
}
#NavBar
{
position: absolute; top: 80px; left: 0px;
width: 130px; height: 300px;
padding: 5px;
color: #ffffff;
}
#PageContent
{
background-color: #ffffff;
position: relative; top: 0px; left: 140px;
padding: 10px;
width: 600px;
height: 300px;
}
#Footer
{
padding-left: 10px;
height: 30px; line-height: 30px;
background-color: #336699;
font-weight: bold; color: #ffffff;
text-align: center;
border-top: solid 1px #cccccc;
border-bottom: solid 1px #cccccc;
clear: both;
}
using System;
using System.Xml;
using System.Web.UI;
using System.Reflection;
using System.Web.UI.WebControls;
namespace sstchur.web.Pages
{
public class BasePage : System.Web.UI.Page
{
// Protected Variables (UI Components)
protected Label lblBaseLabel;
// Private Variables
private string m_strTemplateFile;
// Public Properties
public string TemplateFile
{
set { m_strTemplateFile = value; }
}
// Constructor
public BasePage()
{
// Initialize the TemplateFile in case one is not specified
m_strTemplateFile = "~/_templates/standardpage.xml";
}
protected override void OnInit(EventArgs e)
{
// Load the XHTML Template
InvokePageTemplate();
// Initialize any components/variables specific to this class (or its base)
InitializeComponent();
// Let the base class do its thing
base.OnInit (e);
}
private void InitializeComponent()
{
// Wireup Page_Load Event
this.Load += new EventHandler(Page_Load);
}
private void Page_Load(object sender, EventArgs e)
{
// Put any needed Page_Load functionality here
}
// Here is where all the tricky stuff happens
private void InvokePageTemplate()
{
Control[] controls = new Control[this.Controls.Count];
this.Controls.CopyTo(controls, 0);
this.Controls.Clear();
XmlDocument doc;
doc = new XmlDocument();
doc.Load(Server.MapPath(m_strTemplateFile));
XmlElement root = doc.DocumentElement;
XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
string strTemplate = nodeTemplate.InnerText;
Control ctrlTemplate = Page.ParseControl(strTemplate);
this.Controls.Add(ctrlTemplate);
PlaceHolder phMainContent = (PlaceHolder)ctrlTemplate.FindControl("phMainContent");
foreach (Control c in controls)
phMainContent.Controls.Add(c);
FieldInfo[] fieldInfo;
Type myType = typeof(sstchur.web.Pages.BasePage);
fieldInfo = myType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
for (int i = 0; i < fieldInfo.Length; i++)
{
if (!fieldInfo[i].IsPrivate)
{
string id = fieldInfo[i].Name;
Control c = ctrlTemplate.FindControl(id);
if (c != null)
fieldInfo[i].SetValue(this, c);
}
}
}
}
}
One more file... err, actually 2 more files remain. An ASPX example file and its
corresponding code-behind. You're not going to believe how absolutely simple
these files are to create:
-- Example.aspx --
<%@ Page language = "C#" AutoEventWireup = "false" Codebehind = "example.aspx.cs" Inherits = "sstchur.web.ExamplePage" %>
using System;
namespace sstchur.web
{
public class ExamplePage : sstchur.web.Pages.BasePage
{
private void Page_Load(object sender, System.EventArgs e)
{
base.lblBaseLabel.Text = "My value has been overridden by the example.aspx page";
}
override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
private void InitializeComponent()
{
this.TemplateFile = "~/template.xml";
this.Load += new System.EventHandler(this.Page_Load);
}
}
}
Now, create a new web project in Visual Studio, and add each of the files that
we created above to it. Compile the project, and browse to the file
Example.aspx.
You should see something like this :
Of course, just for grins and giggles, it might be interesting to add a few
additional components to our Example.aspx file, just to see if they do indeed
act the way we'd expect them to in a normal ASP.NET page.
<%@ Page language = "C#" AutoEventWireup = "false" Codebehind = "example.aspx.cs" Inherits = "sstchur.web.ExamplePage" %>
<form id = "theForm" runat = "server">
<p><asp:TextBox id = "txt" runat = "server" /></p>
<p><asp:Button id = "btn" Text = "Click Me!" runat = "server" />
</form>
It's worth noting that if you happen to have any components in your base page's
XML template file that require a <form runat = "server"> tag, then you'll want
to make sure you put the server-side form in the XML template file, rather than
in the Code-Front of the inheriting page. And of course, you then won't need the
<form runat = "server"> in the Code-Front of your inheriting page anymore.
Caveats: There is two caveats I'd like to point out:
1. EVERY control you use MUST be given an ID. I suspect this has to do with
timing and reflection, but it's just a good habit to name your controls, so do
it!
2. Accessing the base page's components can't be done any earlier than in the
Page_Load method of the inheriting page. If you try to do it in
InitializeComponent() (or OnInit()) you'll be attempting to access the controls
before the reflection has had a chance to works its magic. The result? A
NullReferenceException.
Well, I hope this article has intrigued you and given you the confidence to
start exploring the possibilities of Reusuable Page Templates using XML and
reflection. The examples we've seen in this article are very very basic, but
rest assured, they can be as complex as you like. The most recent web
application I'm working on for example, makes use of this Page Template concept,
and we're using 3rd party navigation components, and several user controls all
within our page template! We even exposed method whereby certain navigational
components that might not be applicable to every page, can be shown or hidden at
run time.
So I encourage you to give this concept a try. At best, it will save you a ton
of time, and at worst, you'll learn something!
Happy programming.
Top 
About
Stephen Stchur
Click here if you want to know more about
Stephen Stchur.
Other articles that may interest you
|