What Will I Learn?
In this tutorial, 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.
Requirements
.NET Framework
Difficulty
Intermediate
Tutorial Contents
- Building the BasePage Class
- Handling the BasePage Class Properties
- Moving the Controls
- Testing the BasePage Class
Creating Base Pages in .NET
In this tutorial, 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 == "") {
// The shopper ID has not been generated yet. Create a new one and send it out
// in a cookie.
id = Guid.NewGuid().ToString();
HttpCookie ck = new HttpCookie("shopperid", id);
ck.Expires = DateTime.MaxValue;
Response.Cookies.Add(ck);
return id;
} else {
// Return the shopper ID that we already generated and retrieved from the cookie.
return id;
}
}
[VB]
Protected Function ShopperID() As String
Dim id As String = Request.Cookies("shopperid").Value
If id = "" Then
' The shopper ID has not been generated yet. Create a new one and send it out
' in a cookie.
id = Guid.NewGuid().ToString()
Dim ck As New HttpCookie("shopperid", id)
ck.Expires = DateTime.MaxValue
Response.Cookies.Add(ck)
Return id
Else
' Return the shopper ID that we already generated and retrieved from the cookie.
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.
Posted on Utopian.io - Rewarding Open Source Contributors