The Power of Base Pages
By Russ Nemhauser
Published: 1/19/2004
Reader Level: Beginner Intermediate
Rated: 4.40 by 25 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

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 == "") {
        // 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.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help