As the holiday season rolls around, there are family traditions that we dust
off from the previous years. Mine is baking. Holiday cookies are a big hit with
my family and friends so I find myself baking 50 dozen or so every year to help
spread the joy of the season. It certainly doesn't hurt that I used to be a
baker, so churning out 50 dozen cookies is a short afternoon for me. But the
pleasure of popping one of those bad oscars in and tasting that familiar once-a-year
flavor is really a treat, even after your 15th one (I've heard).
One of the things that make holiday cookies holiday cookies is the fact that
they're all cut out in holiday shapes like Christmas trees, ornaments, wreaths,
and the like. No matter how many I do, there are dozens of cookies that look
exactly the same. That's the glory of cookie cutters. You're guaranteed to get
consistently great-looking cookies.
My father uses the cookie cutter analogy for master-planned communities as
well. As a buyer you can walk in to a development, choose from one of the handful
of blueprints and then within a few short months you get your house with a customized
option here and there. When I lived in Las Vegas we came very close to buying
a house in one of these developments, but decided to leave the area instead.
A year later we went back to visit and, sure enough, all of the houses were
complete. If you were blindfolded and dropped off on one of the streets in the
community, you'd have no idea where you were because (you guessed it) all the
houses look exactly the same (not that there's anything wrong with that).
If we could take the same approach with the code we write, we can see where
this same type of effect could be extremely beneficial. In this article, we'll
create a powerful class that will serve as the base for all of the Web forms
on our site and then we'll put it to work.
Suppose we had a cookie cutter that created Web pages. By the time we were
done creating them, all of the pages would have the same appearance and features.
This is quite powerful. Let's assume we're working on an online commerce site.
We'll call it www.BuySomeStuff.com. There are a few basic requirements for this
site that we'll deal with in this article:
- Keep track of the products the visitor viewed. Have you noticed that area
on Amazon.com where you see the most recent products you viewed so it's easy
to view them again?
- Persist the shopping cart across visits. Many times I'll go to a site and
add stuff to my cart, but I'm not immediately ready to check out. I find it
very frustrating if I return the follow day (or week) and have no items in
my cart.
- Remember visitors when they come back without the necessity of a sign-in.
In today's world, "privacy" is a major buzz word. I don't want to have to
give someone my e-mail address and other personal information just so I can
browse around and add things to my cart.
- Provide a consistent look and feel for each page.
Setting the database aside, we are faced with one initial task — identifying
the visitor. If we know who they are (regardless of the page they're viewing),
we can add personalization wherever we want. Some sites identify visitors by
way of the session ID. That might work within a visit, but it leaves us short
if we think about spanning multiple visits. With that in mind, we'll need to
be able to find out who our visitor is on essentially any page within our entire
site. Furthermore, we don't want to incur any overhead involved with identifying
the user if we don't happen to need to know who they are on any given page.
Building the BasePage Class
The first thing we need to do is create our Base Page. To do that, start a
new Web Application and then add a new class. Call your class BasePage.cs
or BasePage.vb depending on the language with which you're working. We'll
need to inherit from System.Web.UI.Page, so your BasePage
class should contain the following code:
[C#]
using System;
using System.Web.UI;
namespace BasePagesCS {
public class BasePage : Page {
public BasePage() {
}
}
}
|
[VB]
Imports System
Imports System.Web.UI
Namespace BasePagesVB
Public Class BasePage
Inherits Page
End Class
End Namespace
|
Once your class has been saved, build your Web application and then open the code-behind
for WebForm1. Change the inheritance from System.Web.UI.Page to BasePage
so we can take advantage of the functionality that we'll add to our new class.
Since identification is the driving force for each of the first three requirements
outlined above, it stands to reason that we need to write a method in our BasePage
class that will return a shopper ID when we ask for one. Our method will first
look in a cookie called "shopperid". If that cookie contains a value, we'll
return it. If not, we'll need to generate a new shopper ID, store that in a
cookie, and then return the new ID. Add the following method to your BasePage
class:
[C#]
protected string ShopperID() {
string id = Request.Cookies["shopperid"].Value;
if (id == "") {
id =
Guid.NewGuid().ToString();
HttpCookie ck = new HttpCookie("shopperid", id);
ck.Expires = DateTime.MaxValue;
Response.Cookies.Add(ck);
return id;
} else {
return id;
}
}
|
[VB]
Protected Function ShopperID() As String
Dim id As String =
Request.Cookies("shopperid").Value
If id = "" Then
id =
Guid.NewGuid().ToString()
Dim ck As New HttpCookie("shopperid", id)
ck.Expires = DateTime.MaxValue
Response.Cookies.Add(ck)
Return id
Else
Return id
End If
End Function
|
By adding this single method, we have given ourselves the ability to easily handle
the first three requirements. Our product detail page can save the shopper ID
and product ID to a database to keep track of the products that each visitor has
viewed. By storing the shopper ID, product ID, and quantity in a ShoppingCart
table, we can persist the visitor's shopping cart between visits (since their
cookie does not expire any time soon). We can also figure out who the visitor
is by the cookie we store on their machine. Any page in our entire online commerce
site can identify the visitor.
It should be noted that the function we just created could just as easily
have been scoped as Public and placed inside a public module or as a shared
method on a class. I chose to implement it this way merely to illustrate the
concept of Base Page classes.
The fourth requirement is to provide a consistent look and feel for our
site and that can also be accomplished easily using our new BasePage
class. The "inverted L" layout is quite common, so that's what we'll use. In
order to implement this, we override the OnInit method
of the Page class in our BasePage.
We'll use OnInit to add the Table to our page's Controls
collection. This Table will contain two rows, and the second row will contain
two columns as depicted in FIGURE 1.
FIGURE 1: An "inverted L" table
Handling the BasePage Class Properties
Let's deal with all of the new properties in our BasePage
class. Because we have decided to go with an "inverted L" layout, we'll need
to give the page developer the ability to work with the three areas of content
that the inverted L offers. To that end, we'll create three properties in our
BasePage that will return TableCell
objects. TableCells contain a Controls collection, which makes it very easy
for developers to manipulate any of the controls in the title bar, navigation
area, or content area. Here's the code:
[C#]
private TableCell baseTitleArea;
protected TableCell TitleBar {
get { return baseTitleArea; }
}
private TableCell baseNavArea;
protected TableCell NavigationBar {
get {return baseNavArea; }
}
private TableCell baseContentArea;
protected TableCell ContentArea {
get { return baseContentArea; }
}
|
[VB]
Private baseTItleArea As TableCell
Protected ReadOnly Property TitleBar As TableCell
Get
Return baseTitleArea
End Get
End Property
Private baseNavArea As TableCell
Protected ReadOnly Property NavigationBar As TableCell
Get
Return baseNavArea
End Get
End Property
Private baseContentArea As TableCell
Protected ReadOnly Property ContentArea As TableCell
Get
Return baseContentArea
End Get
End Property
|
The next thing we'll do is expose the HtmlForm that
is common to all Web forms. The page developer might want to change various
properties of the form (such as the Enctype property
to allow for file uploads) and the last thing we want to do is limit any developer's
abilities. Add the following code to your BasePage
class:
[C#]
private HtmlForm baseForm;
protected HtmlForm Form {
get { return baseForm; }
}
|
[VB]
Private baseForm As HtmlForm
Protected ReadOnly Property Form As HtmlForm
Get
Return baseForm
End Get
End Property
|
There is one property that we'll add to our BasePage
class that will make life a great deal easier: Title.
Anyone who has tried knows that it's not easy to set the title of a page programmatically
unless you alter the <title> tag on each page so that it runs at the server.
We'll take care of this for everyone who subclasses our BasePage
by exposing an HtmlGenericControl to represent our
<title> tag and we'll let the user set or retrieve it as a property, just
like ID or IsPostBack. Something
like this is particularly useful on our online commerce site when we'll want
to change the page's title for each product the visitor browses. Since we won't
have one page for each product, our product page must use a product ID to retrieve
the product's details from the database. The developer can now set the page's
title along with all the other content programmatically. Add the following code
to your BasePage class (notice that the property sets
or retrieves the InnerText property of the HtmlGenericControl):
[C#]
private HtmlGenericControl baseTitle;
protected string Title {
get { return baseTitle.InnerText; }
set { baseTitle.InnerText = value; }
}
|
[VB]
Private baseTitle As HtmlGenericControl
Protected Property Title As String
Get
Return baseTitle.InnerText
End Get
Set
baseTitle.InnerText = value
End Set
End Property
|
Keeping in mind that our BasePage class will be responsible
for generating ALL of the HTML for a page, we need a method that will take care
of this for us. The method will create our <html>,
<head>, and other generic tags that are required
as well as the ones we need for our inverted L. Call the method BuildFrame
and add the code below:
[C#]
private void BuildFrame() {
// Create our opening tags
LiteralControl baseHtml = new LiteralControl("<html>");
baseHtml.ID = "baseHtml";
this.Controls.Add(baseHtml);
LiteralControl baseHead = new LiteralControl("<head>");
baseHead.ID = "baseHead";
this.Controls.Add(baseHead);
// Page Title
baseTitle = new HtmlGenericControl("title"); baseTitle.ID =
"baseTitle";
this.Controls.Add(baseTitle);
LiteralControl baseHead2 = new LiteralControl("</head>"); baseHead2.ID =
"baseHead2";
this.Controls.Add(baseHead2);
LiteralControl baseBody = new LiteralControl("<body>");
baseBody.ID = "baseBody";
this.Controls.Add(baseBody);
// Add the form baseForm =
new HtmlForm();
baseForm.ID = "baseForm";
this.Controls.Add(baseForm);
// Set up our table
Table tbl = new Table(); tbl.Width =
Unit.Percentage(100);
// Title Bar Row
TableRow rw = new TableRow();
baseTitleArea = new TableCell(); baseTitleArea.ColumnSpan =
2; rw.Height =
Unit.Pixel(40);
rw.Cells.Add(baseTitleArea);
tbl.Rows.Add(rw);
// Navigation and Content
rw = new TableRow();
baseNavArea = new TableCell(); baseNavArea.Width =
Unit.Pixel(120);
rw.Cells.Add(baseNavArea);
baseContentArea = new TableCell();
rw.Cells.Add(baseContentArea);
tbl.Rows.Add(rw);
baseForm.Controls.Add(tbl);
//Close our tags
LiteralControl baseBody2 = new LiteralControl("</body>"); baseBody2.ID =
"baseBody2";
this.Controls.Add(baseBody2);
LiteralControl baseHtml2 = new LiteralControl("</html>"); baseHtml2.ID =
"baseHtml2";
this.Controls.Add(baseHtml2);
}
|
[VB]
Private Sub BuildFrame()
' Create our opening tags
Dim baseHtml As New LiteralControl("<html>")
baseHtml.ID = "baseHtml"
Me.Controls.Add(baseHtml)
Dim baseHead As New LiteralControl("<head>")
baseHead.ID = "baseHead"
Me.Controls.Add(baseHead)
' Page Title
baseTitle = New HtmlGenericControl("title")
baseTitle.ID = "baseTitle"
Me.Controls.Add(baseTitle)
Dim baseHead2 As New LiteralControl("</head>")
baseHead2.ID = "baseHead2"
Me.Controls.Add(baseHead2)
Dim baseBody As New LiteralControl("<body>")
baseBody.ID = "baseBody"
Me.Controls.Add(baseBody)
' Add the form baseForm =
New HtmlForm()
baseForm.ID = "baseForm"
Me.Controls.Add(baseForm)
' Set up our table
Dim tbl As New Table() tbl.Width =
Unit.Percentage(100)
' Title Bar Row
Dim rw As New TableRow()
baseTitleArea = New TableCell() baseTitleArea.ColumnSpan =
2 rw.Height =
Unit.Pixel(40)
rw.Cells.Add(baseTitleArea)
tbl.Rows.Add(rw)
' Navigation and Content
rw = New TableRow()
baseNavArea = New TableCell() baseNavArea.Width =
Unit.Pixel(120)
rw.Cells.Add(baseNavArea)
baseContentArea = New TableCell()
rw.Cells.Add(baseContentArea)
tbl.Rows.Add(rw)
baseForm.Controls.Add(tbl)
'Close our tags
Dim baseBody2 As New LiteralControl("</body>")
baseBody2.ID = "baseBody2"
Me.Controls.Add(baseBody2)
Dim baseHtml2 As New LiteralControl("</html>")
baseHtml2.ID = "baseHtml2"
Me.Controls.Add(baseHtml2)
End Sub
|
Moving the Controls
As you can see, this is fairly straightforward. All we're doing is creating
controls and adding them where they're supposed to go. Once this is done we
need to think about the developers who will be adding their controls at design
time. Anyone who does this will find out (when they build and try to browse
to the page) that they'll receive an error message telling them that their ASP.NET
controls need to be contained within a server-side form. As it stands now, the
controls added at design time would render themselves outside the HtmlForm
that we are creating so we'll need to move them inside of it.
If you didn't already notice, any control that we added in our BuildFrame
method has an ID that begins with the word "base". Because of this, we can determine
quickly whether it needs to be moved. The only caveat to this lies in the possibility
that a developer could give an ID that starts with this same word to one of
THEIR controls. Let's just pretend that'll never happen.
All we have to do here is set up an integer as a place holder. We'll loop
through all the controls on the form, but not move all of them. We'll only move
the control to the content area TableCell if:
- The control has no ID.
- The control has an ID less than four characters in length.
- The control has an ID of at least four characters,
but the first four characters are not "base".
Those that are worried about any kind of performance hit this causes can simply
place all of their controls inside a Panel control, and only have a single
control to be moved by the MoveControls method:
[C#]
private void MoveControls() {
int ph = 0; while
(this.Controls.Count > ph) { if
(this.Controls[ph].ID ==
null || this.Controls[ph].ID.Length < 4 || (this.Controls[ph].ID.Length > 3 && this.Controls[ph].ID.Substring(0, 4).ToLower() != "base")) {
baseContentArea.Controls.Add(this.Controls[ph]);
} else {
ph += 1;
}
}
}
|
[VB]
Private Sub MoveControls()
Dim ph As Integer = 0 While
Me.Controls.Count > ph If
IsNothing(Me.Controls(ph).ID) OrElse Me.Controls(ph).ID.Length < 4 _
OrElse
(Me.Controls(ph).ID.Length > 3 And _ Me.Controls(ph).ID.Substring(0,
4).ToLower() <> "base") Then
baseContentArea.Controls.Add(Me.Controls(ph))
Else
ph += 1
End If
End While
End Sub
|
As discussed above, we'll override the OnInit method
of System.Web.UI.Page. We'll call BuildControls first,
followed by MoveControls. Then we'll call the OnInit
of the base class to make sure we're playing nicely
in it's sandbox. Add the following code to your BasePage
class:
[C#]
protected override void OnInit(EventArgs e) {
BuildFrame();
MoveControls();
base.OnInit (e);
}
|
[VB]
Protected Overrides Sub OnInit(ByVal e As EventArgs)
BuildFrame()
MoveControls()
MyBase.OnInit(e)
End Sub
|
Testing the BasePage Class
Our BasePage class is now complete, which means we
can now test this out. Switch over to WebForm1 in your project, go to HTML view,
and remove every line of code you see there with the exception of any directives.
There should be no HTML code remaining. Then press F7 to switch to code view
and change the inheritance of WebForm1 to use our new BasePage
class.
Let's try out some of the new features by adding content to each of the three
page areas and setting the page title all from code. We'll set our page title
using our new Title property and add some controls
to the main content and navigation areas. We'll also throw a nice large, bold
label in the title bar to serve as the company's logo. Your Page_Load
procedure should contain the following code:
[C#]
private void Page_Load(object sender, System.EventArgs e)
{
// Set page title
Title = "Hello World";
// Content area Label lbl =
new Label(); lbl.Text =
"Hello World";
ContentArea.Controls.Add(lbl);
// Navigation bar
Button btn = new Button(); btn.Text =
"Push Here";
NavigationBar.Controls.Add(btn);
// Title bar
lbl = new Label(); lbl.Text =
"mycompany.com";
lbl.Font.Bold = true;
lbl.Font.Name = "Verdana"; lbl.Font.Size =
FontUnit.Large;
TitleBar.Controls.Add(lbl);
}
|
[VB]
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Set page title
Title = "Hello World"
' Content area
Dim lbl As New Label()
lbl.Text = "Hello World"
ContentArea.Controls.Add(lbl)
' Navigation bar
Dim btn As New Button() btn.Text =
"Push Here"
NavigationBar.Controls.Add(btn)
' Title bar
lbl = new Label() lbl.Text =
"mycompany.com"
lbl.Font.Bold = True
lbl.Font.Name = "Verdana" lbl.Font.Size =
FontUnit.Large
TitleBar.Controls.Add(lbl)
End Sub
|
No site with a navigation bar and title bar would be complete without places to
navigate to and a logo. Most of the time these exist as user controls (ascx files)
that you can add to your page. Using our new BasePage
is no different. Using Page.LoadControl in our BuildFrame()
method, we can pop a user control into any of our page areas that satisfies the
common look and feel requirement.
Summary
After this journey we are left with a class that makes it simple to give all
of your Web forms a common look and feel. We added some functionality not native
to System.Web.UI.Page that allows us to identify the visitor and set a page's
title. You can follow the same logic to add <meta> tags so you can programmatically
add keywords and a page description as well (which can help your presence on
search engine results). There are a slew of different way to implement functionality
like this, including the implementation of something called Master Pages, which
will make more sense after Whidbey is released to the masses. No single solution
holds all of the keys to the door that leads to best practices.