Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Common Web Page Class Library - Part 1

0.00/5 (No votes)
6 Apr 2006 3  
A set of common, reusable page classes for ASP.NET applications.

Table of contents

Introduction

This is the first in a series of articles on a class library for ASP.NET applications that I have developed. It contains a set of common, reusable page classes that can be utilized in web applications as-is to provide a consistent look, feel, and set of features. New classes can also be derived from them to extend their capabilities. The features are all fairly modular, and may be extracted and placed into your own classes too. The classes in the assembly include:

  • BasePage - Described in this article, continued in Part 2 [^] which covers the data change checking features of the class, and in Part 3 [^] which covers the features that allow it to e-mail its rendered content.
  • RenderedPage, MenuPage, and VerticalMenuPage - Described in this article. These provide a way for .NET 1.1 applications to create pages that automatically render all the common header and footer HTML tags.
  • PageUtils - A utility class for the library described in Part 4 [^]. This also covers a few methods found in the BasePage class related to converting validation messages into clickable links.

The download contains a small demo application in C# and VB.NET that makes use of these classes. To try out the demo applications, create two virtual directories in IIS and point them at the two demo folders:

  • Use \DotNet\Web\EWSWeb\EWSWebDemoCS with a virtual directory name of EWSWebDemoCS11 and \DotNet\Web\EWSWeb\EWSWebDemoVB with a virtual directory name of EWSWebDemoVB11 for the .NET 1.1 version.
  • Use \DotNet20\Web\EWSWeb\EWSWebDemoCS with a virtual directory name of EWSWebDemoCS20 and \DotNet20\Web\EWSWeb\EWSWebDemoVB with a virtual directory name of EWSWebDemoVB20 for the .NET 2.0 version.

The startup page in each application is Default.aspx. The demo projects are set up to compile and run on a development machine that has Visual Studio .NET and IIS running on it. If you are using a remote server, you will need to set up the virtual directories, build the projects, and copy them to the server location. When opening the pages in design view the very first time, you may get an error stating that they cannot be viewed. If this occurs, rebuild the projects so that the assembly containing the base classes exists.

For the e-mail part of the demo, you will need the SMTP service on the web server or access to a separate SMTP server. The error page demos use an e-mail address stored in the Web.config file that is currently set to a dummy address. You should modify the address specified by the ErrorRptEMail key in the appSettings section to make it valid. The e-mail page feature can also use an optional configuration option to control the name of the SMTP server (EMailPage_SmtpServer).

Embedded resources

The library contains some client-side script files. Rather than distributing and installing them separately, they are embedded in the assembly as resources that are extracted and returned to the client browser at runtime. For more information on how this is implemented, see the included help file and the following Code Project article: A Resource Server Handler Class For Custom Controls [^].

For the .NET 1.1 version, the embedded resources require that an HTTP handler entry be added to the Web.config file. This is a simple procedure and requires nothing more than copying and pasting a definition from the demo into your own project's Web.config file. Refer to the supplied help file and the article noted above, for more information. .NET 2.0 provides a built-in method of serving embedded resources so this step is not necessary for applications using the .NET 2.0 version of the assembly.

Script compression

The scripts are also compressed during the build step for the project using the JavaScript compressor described in the article A JavaScript Compression Tool for Web Applications [^]. This reduces the size of the scripts by removing comments and extraneous whitespace so that they take up less space. If you'd prefer to not use script compression, you can remove it from the pre-build step by opening the project, right click the project name in the Solution Explorer, select Properties, expand the Common Properties folder, and select the Build Events sub-item. Click in the Pre-build Event Command Line option and delete the command line that you see there. Copy the scripts from the ScriptsDev folder to the Scripts folder to replace the existing compressed versions distributed with the library. The ScriptsDev folder can be deleted from the project if not using the compressor.

Using the assembly in your projects

The BasePage class and the others in the library are included in the sample project. They are compiled into an assembly that you can reference in your own projects. The sections below describe each of the main features of the class and the methods and properties that are used to implement them. If you are already using a custom page class in your applications, you can simply extract the parts that interest you for inclusion in your own projects.

An HTML help file is included in the source code download that contains more extensive documentation on the classes in the assembly. It was generated using NDOC [^] from the XML comments within the source code. The first page of the help file titled Usage Notes describes how to install and use the assembly in your own projects. Please refer to it for more information.

The code is written in C#. However, because .NET is language-neutral, the assembly is perfectly useable as-is in projects utilizing other languages such as VB.NET. The class documentation below presents the properties and methods using their C# declarations. The download file contains a C# and a VB.NET version of the demonstration application. The help file mentioned above shows the class declarations and example code in C# and VB.NET.

Using BasePage and its derived classes in your own applications

Using the page classes in your own applications is fairly straightforward. Just follow these steps:

  • If you have not done so already, add a reference to the EWSoftware.Web assembly in your project.
  • For .NET 1.1 applications, add the necessary settings to your Web.config file. See the supplied help file for more information.
  • Add a new Web Form to your project.
  • Open the code-behind module for the form.
  • Add a using EWSoftware.Web; statement to the code module (Imports EWSoftware.Web for VB.NET).
  • On the class declaration, replace the reference to System.Web.UI.Page as the base class with a reference to one of the page classes in the EWSoftware.Web namespace (BasePage, RenderedPage, MenuPage, or VerticalMenuPage) or one of your own classes derived from them.
  • Normally, you will add code to the Page_Load event to set the page title and other properties and set the initial focus to the first control on the page. This should be done only on the initial page load and not on postback as other events may have changed the page title or set the focus to some other control. For example:
    private void Page_Load(object sender, System.EventArgs e)
    {
        if(!Page.IsPostBack)
        {
            this.PageTitle = "My Page Title";
            this.PageDescription = "My Test Page";
            this.Robots = RobotOptions.Index |
                RobotOptions.Follow;
            this.SetFocusExtended(txtFirstField);
    
            ... Do other stuff for initial page load ...
        }
    }
  • Return to design view and add controls to the form in the normal fashion.

If you decide to make use of the RenderedPage class, you will also need to do the following:

  • Open the new form in design view and switch to HTML view.
  • Delete the <!DOCTYPE> tag, the opening <html> and closing </html> tags, the <head> section, and the opening <body> and closing </body> tags. All that should be present in the new page are the <%@ Page> directive, the opening <form> tag, and the closing </form> tag.

No styles in design view

One problem when using the RenderedPage class and those derived from it is that you lose the style settings normally present when you have the entire supporting header HTML in the ASPX page. A solution for this is to temporarily add a <link> tag to the top of the page that references the application style sheet while you are designing the initial layout of the page. Just be sure to remove it when you are done designing the page.

For .NET 2.0, a better solution is to use a master page instead of RenderedPage. This allows you to have the same functionality as the RenderedPage class but with a lot more flexibility. However, you can still derive your pages from BasePage to gain the extra functionality that it provides.

The BasePage class

This article presents a class called BasePage that is derived from System.Web.UI.Page. It can be used as the base class for the pages in any ASP.NET application as well as be used to derive new classes that contain additional features common to the pages in your web application. The BasePage class contains the following useful features:

  • Properties are provided to customize or alter the common header tags if necessary (i.e. page title, description, keywords, style sheet, robot options, additional header tags, etc). For the .NET 1.1 version, these properties are used by the RenderedPage class. For the .NET 2.0 version, they are used by BasePage as long as there is a head control with a runat="server" tag.
  • Properties are provided to more easily allow the insertion of user controls and supporting structure into the page's form control in classes derived from the BasePage class.
  • Methods are provided that allow you to enable and disable one or more controls in a single call. Support is provided to allow the setting of a CSS class to better show the disabled state.
  • Server-side methods and client-side code are provided to allow you to set the focus to any control on the page. The controls can be regular controls on the page itself or those embedded in other controls such as those in the EditItemTemplate of a DataGrid that may not exist until the page is rendered.
  • For data entry forms, properties and client-side code are provided that allow you to automatically track the dirty state of the form. For Internet Explorer, the client-side code can also prompt the user to save their changes before performing an action such as leaving the page, closing the browser, etc. that could cause loss of their data. This is covered in a separate article as noted at the start of this one.
  • An AuthType property is provided to allow you to get the authentication method in effect for the application (Anonymous, Basic, NTLM, or Kerberos).
  • The OnError method is overridden to save more context information about the cause of the error to the application cache so that it can be passed on to a custom error page.

Enabling and disabling form controls

Enabling and disabling controls is a simple matter of setting their Enabled property to true of false. However, I have found that the default visual style of disabled controls on a web page is sometimes hard to discern from enabled controls. As such, I added a property to allow the specification of an alternate style that better shows the disabled state. For my own applications, I use a CSS style that sets a silver background thus better indicating the disabled state much like a Windows Forms application. The methods described below make use of this property when disabling controls. To save some typing, overloads of the methods are provided that allow you to specify a list of two or more controls to enable or disable at once. A method is also provided that allows you to enable or disable all controls on the page in one call. This is a big time saver when the page contains many controls or controls such as panels that contain nested controls.

  • public string DisabledCssClass

    This property is used to get or set the CSS class for disabled controls. The CSS class name should appear in the style sheet file associated with the application. If not set or set to null, the property will use the style name defined by the BasePage.DisabledCssName constant. Currently this is set to the style name Disabled.

  • public void SetEnabledState(WebControl ctl, bool enabled)

    This method is used to enable or disable a single control. If disabled and the control is a TextBox, DropDownList, or ListBox (or ones derived from them), it sets the style class to the one specified by the DisabledCssClass property. When enabling a control, this method calls the following overload with an empty string as the normal style.

  • public void SetEnabledState(WebControl ctl, bool enabled, string normalClass)

    This method is the same as the one above but it allows you to specify the normal style class name for TextBoxes, DropDownLists, and ListBoxes. It can be used if you have explicitly specified a style for the enabled state and need it restored when enabling the control. Instead of clearing the style with an empty string as in the prior method, you can use this version to replace it with the specified style:

    public void SetEnabledState(WebControl ctl, bool enabled,
      string normalClass)
    {
        if(ctl == null)
            throw new ArgumentNullException("ctl",
                "The control cannot be null");
    
        ctl.Enabled = enabled;
    
        if(ctl is System.Web.UI.WebControls.TextBox ||
          ctl is System.Web.UI.WebControls.DropDownList ||
          ctl is System.Web.UI.WebControls.ListBox)
            if(enabled)
                ctl.CssClass = normalClass;
            else
                ctl.CssClass = this.DisabledCssClass;
    }
  • public void SetEnabledState(bool enabled, params WebControl[] ctlList)

    This method is used to enable or disable multiple controls in one step. Simply pass it the state to set and a list of the controls to enable or disable. When disabling a TextBox, DropDownList, or ListBox control (or ones derived from them), it sets the style class to the one specified by the DisabledCssClass property. When enabling such controls, it clears the style class. The code is identical to the single control methods above except that it is wrapped in a foreach loop that iterates over the passed array of controls.

  • public void SetEnabledState(string normalClass, bool enabled, params WebControl[] ctlList)

    This method is the same as the one above, but it allows you to specify the normal style class name for TextBoxes, DropDownLists, and ListBoxes. It can be used if you have explicitly specified a style for the enabled state and need it restored when enabling the controls.

  • public void SetEnabledAll(bool enabled, System.Web.UI.Control ctlPageForm)

    This can be used to disable or enable all edit controls on a web page, form, panel, or tab control. The method will call itself recursively if it encounters other container controls such as Panels to enable or disable controls contained within them too. Note that buttons and links are not disabled by this method as it is quite likely that you will want them enabled to perform an action such as exiting the page. If you do want certain buttons disabled, you will have to make separate calls to the methods above. Since all controls are disabled, the lack of a distinct disabled style to distinguish them from enabled controls is not an issue, so this method will not alter their style in any way:

    public void SetEnabledAll(bool enabled, Control ctlPageForm)
    {
        Control form = null;
        string controlType;
    
        // If null, default to the current page
        if(ctlPageForm == null)
            ctlPageForm = this.PageForm;
    
        // Yes, I could add a reference to the MS IE Web
        // Controls, but I don't want this library to have a
        // dependency on it so we'll just check for IE Web
        // Controls by type name string instead.
        controlType = ctlPageForm.ToString();
    
        // If passed a form, panel, multi-page, or page view,
        // use it directly. If passed a page, see if it
        // contains a form. If so, use that form. If not, use
        // the page.
        if(ctlPageForm is System.Web.UI.HtmlControls.HtmlForm ||
           ctlPageForm is System.Web.UI.WebControls.ContentPlaceHolder ||
           ctlPageForm is System.Web.UI.WebControls.Panel ||
           ctlPageForm is System.Web.UI.WebControls.MultiView ||
           ctlPageForm is System.Web.UI.WebControls.View ||
           controlType.IndexOf("MultiPage") != -1 ||
           controlType.IndexOf("PageView") != -1)
        {
            form = ctlPageForm;
        }
        else
            if(ctlPageForm is System.Web.UI.Page &&
              ctlPageForm != this.PageForm)
                form = BasePage.FindPageForm((Page)ctlPageForm);
    
        // Ignore anything unexpected
        if(form == null)
            return;
    
        // Disable each edit control on the page
        foreach(Control ctl in form.Controls)
            if(ctl is System.Web.UI.WebControls.TextBox ||
               ctl is System.Web.UI.WebControls.DropDownList ||
               ctl is System.Web.UI.WebControls.ListBox ||
               ctl is System.Web.UI.WebControls.CheckBox ||
               ctl is System.Web.UI.WebControls.CheckBoxList ||
               ctl is System.Web.UI.WebControls.RadioButton ||
               ctl is System.Web.UI.WebControls.RadioButtonList)
                ((WebControl)ctl).Enabled = enabled;
            else
            {
                // As above, done this way to avoid a dependency
                controlType = ctl.ToString();
    
                if(ctl is System.Web.UI.WebControls.ContentPlaceHolder ||
                  ctl is System.Web.UI.WebControls.Panel ||
                  ctl is System.Web.UI.WebControls.MultiView ||
                  ctl is System.Web.UI.WebControls.View ||
                  controlType.IndexOf("MultiPage") != -1 ||
                  controlType.IndexOf("PageView") != -1)
                    this.SetEnabledAll(enabled, ctl); // Recursive
            }
    }

    As seen above, this method is aware of the Microsoft Internet Explorer Web Controls MultiPage and PageView, and will also enable or disable controls contained within them. Note that there is no dependency on that assembly due to the way the support for it has been implemented. Instead of checking for a type and creating a dependency on the assembly, I chose to check for them by name using a string. This keeps the assembly independent of the IE Web Control assembly and does not force developers to include it if they do not use it. It can also be extended to check for other class names and controls in a similar fashion. The only potential drawback is that it is hard coding class names as text strings. However, I feel that this is a small price to pay to keep the assembly free of dependencies and still provide a very useful service. For more information about the Internet Explorer Web Controls, see the Source Projects section of ASP.NET [^].

Setting control focus

Prior to ASP.NET 2.0, I saw several requests on the newsgroups to explain how to set the focus to a control in a Web Form as the .NET 1.1 web controls lack any kind of Focus method. The lack of such a method makes sense as setting focus is a client-side rather than a server-side feature. Setting focus thus falls to the page class itself as it must generate client-side script to do it. To remedy the situation, BasePage provides two methods to handle this task. However, emitting a simple line of JavaScript to call the control's focus() method is not enough. The control to focus may be embedded within another control such as a DataGrid, and may not exist at the time the server-side request to give it focus is made, and it may not end up with the expected control ID assigned at design-time. As such, the library contains a client-side script module that contains some expanded abilities with regard to setting the control focus. It will be described shortly. The following are the two class methods that can be used to set control focus. In ASP.NET 2.0, all controls do have a Focus method. In addition, the Page class contains two SetFocus methods similar to the two in BasePage. To avoid conflicts, the two in BasePage are called SetFocusExtended. The SetFocusExtended methods are available for use in .NET 1.1 to set the focus to controls and they are available for use in the .NET 2.0 version in case you need the added capabilities that they provide:

/// <summary>
/// This sets the control that should have the focus when the
/// page has finished loading by control reference.
/// </summary>
public void SetFocusExtended(WebControl ctl)
{
    if(ctl != null)
    {
        focusedControl = ctl.ClientID;
        findControl = false;
    }
    else
        focusedControl = null;
}

/// <summary>
/// This sets the control that should have the focus when the
/// page has finished loading by control ID.
/// </summary>
public void SetFocus(string clientID)
{
    focusedControl = clientId;
    findControl = true;
}

Use the first version for controls that are children of the form control and are not embedded within other controls such as data grids (i.e., they are normal controls that appear on the form itself). The method gets the control's client ID and stores it in the private focusedCtl variable. The private findCtrl variable is set to false which is used to tell the client-side code to find the control using an exact match on the ID value. This will be explained below.

The second version is passed the control ID to give the focus as a string, and is useful for setting the focus to a control embedded in some other control such as one in a data grid's edit item template or a dynamically created control generated and added to the form at runtime. In the case of embedded controls, the control or its client-side ID does not always exist when you want to set focus to it, so this allows it to be set using the design-time control ID. As before, the focusedCtrl variable is set to the specified control ID. This time however, the findCtrl variable is set to true which is used to tell the client-side code to search for the control that has an ID that ends with the specified ID value. Containers such as the DataGrid alter the IDs of the controls within them to keep them all unique. As such, the client-side code must locate it by searching for the ID ending in the specified value. For example, if you place a TextBox in the EditItemTemplate and give it an ID of txtName, the control ID actually rendered may look something like dgGrid:_ctl5:txtName. The client-side code will search all controls on the form for the one ending in txtName and will give it the focus.

To clear the focus, pass null (Nothing in VB.NET) to either method. Due to overloading, you will need to use a cast when doing so to let the compiler pick one version or the other. It does not matter which. For example:

// Clear the focus
this.SetFocus((string)null);

The OnPreRender() method is overridden to register the script module containing the client-side focus code if one of the above methods has been called. It generates a line of startup script that calls the function in the code module passing the values from the two variables noted above as parameters and registers the script with the page. You will also see that the set focus code is rendered if the page has any validators. This is used to support the ConvertValMsgsToLinks() method which is used to convert the validator messages displayed in a ValidationSummary control into clickable links that can be used to take the user to the field that generated the validation error. That method and its related code are described in detail in another part in this series that covers the PageUtils class. See the table of contents at the start of this article for a link to it.

function BP_funSetFocus(strID, bFindCtrl)
{
    var nPgIdx, nIdx, nPos, ctl, ctlParent, htmlCol;

    // Do we need to find the control by partial ID?
    if(bFindCtrl == false)
    {
        ctl = document.getElementById(strID);

        // Search for the control if it was found by the
        // NAME attribute rather than by ID (i.e. the ID
        // matched a NAME attribute on a META tag).
        if(ctl != null && typeof(ctl) != "undefined" &&
          (typeof(ctl.id) != "string" || ctl.id != strID))
            bFindCtrl = true;
    }

    if(bFindCtrl == true)
    {
        // True name is unknown. Find the control ending
        // with the specified name (i.e. it's embedded in
        // a data grid).
        htmlColl = document.getElementsByTagName("*");

        for(nIdx = 0; nIdx < htmlColl.length; nIdx++)
        {
            ctl = htmlColl[nIdx];
            if(typeof(ctl.id) != "undefined")
            {
                nPos = ctl.id.indexOf(strID);
                if(nPos != -1 && ctl.id.substr(nPos) == strID)
                    break;
            }
            else
                ctl = null;
        }
    }

    // If not found, exit
    if(ctl == null || typeof(ctl) == "undefined")
        return false;

The client-side JavaScript function BP_funSetFocus() is passed the control ID to give focus and a Boolean flag indicating whether or not it should search for the control by partial name. If the find flag is false, it calls document.getElementByID() to obtain a reference to the control with the specified ID. If true, it will search all control elements on the page for one with an ID that ends with the specified value. If a control with the specified exact or partial ID cannot be found, the function will exit and nothing will happen. If a control is found, the following section of code will be executed if it is running on Internet Explorer:

// NOTE: This section is IE-specific.
// See if there is a parent element. If so, work back up the chain
// to see if the control is embedded in an PageView IE Web Control.
// If so, select that page before giving focus to the control. If
// not, it may not work as the control may not be visible.
if(typeof(ctl.parentElement) != "undefined")
{
    ctlParent = ctl.parentElement;

    while(ctlParent != null && ctlParent.tagName != "PageView")
        ctlParent = ctlParent.parentElement;

    // If found, set the page as the active one in the containing
    // MultiPage control.
    if(ctlParent != null && ctlParent.tagName == "PageView")
    {
        nPgIdx = ctlParent.PageIndex;
        ctlParent = ctlParent.parentElement;

        if(ctlParent != null && ctlParent.tagName == "MultiPage")
        {
            ctlParent.selectedIndex = nPgIdx;

            // We also have to set the index of any TabStrip
            // associated with the MultiPage.
            htmlColl = document.getElementsByTagName("TabStrip");

            for(nIdx = 0; nIdx < htmlColl.length; nIdx++)
                if(htmlColl[nIdx].targetID == ctlParent.id)
                {
                    htmlColl[nIdx].selectedIndex = nPgIdx;
                    break;
                }
        }
    }
}
// End IE-specific section

As noted, the code will check for a parent element. If there is one, it works back up the chain to find out if the control is embedded within a PageView Internet Explorer web control. If it is, it will make sure that the correct page view and tab are selected first before giving focus to the control. If this were not done, the code would generate an error if the currently selected page view were not the one containing the control to give focus. I have only been using the Internet Explorer Web Controls to provide support for tabbed pages in my applications, so they are the only ones of which it is aware. If you are using different tab and page view controls, you may be able to modify the section above to detect them and provide similar support. For more information about the Internet Explorer Web Controls, see the Source Projects section of ASP.NET [^].

// Focus the control. If it's a table, we may have been asked to
// set focus to a radio button or checkbox list. If so, select
// the control in the first cell of the table.
if(ctl.tagName == "TABLE")
{
    ctl = ctl.cells(0);
    ctl = ctl.firstChild;
}

ctl.focus();

// If it is a textbox-type control, select the text in the control
if(ctl.type == "text" || ctl.tagName == "TEXTAREA")
    ctl.select();

return false;

The final section is what actually gives focus to the control that was found in the first step. Radio button and checkbox list controls can generate their elements within a table. When asked to set focus to such a control on the server-side, you actually end up with a reference to the table containing the radio buttons or checkboxes on the client. If left to set focus to the table control, it would only work for Internet Explorer, but it would only scroll the page to make the table visible on the screen and you would not see a focus rectangle around the first checkbox or radio button. In Netscape, trying to set focus to a table element just does not work. As such, a check is first made to see if the tag name of the found control is an HTML table. If so, the control to which the focus is actually given is set to the first child of the first cell in the table. Doing this allows the focus to get set to an actual radio button or checkbox control, which works under both Internet Explorer and Netscape.

Once the control is given the focus, one final check is made to see if the control is a text box or a text area control. If so, the content is selected to mimic the behavior used when tabbing into the control. As you can see, there is actually a lot more to properly setting the focus to a control under all circumstances than originally meets the eye.

Detecting the authentication type

The AuthType property was added to the class to allow the user to find out what authentication method is being used by the application such as Anonymous, Basic, NTLM, or Kerberos:

public string AuthType
{
    get
    {
        // This prevents an exception being reported in
        // design view.
        if(this.Context == null)
            return null;

        // Figure out the authentication type
        string authType =
            Request.ServerVariables["AUTH_TYPE"];

        if(authType == "Negotiate")
        {
            // Typically, NTLM will yield a header that
            // is 300 bytes or less while Kerberos is
            // more like 5000 bytes. If blank, the best
            // we can do is return "Negotiate".
            string authorization =
                Request.ServerVariables["HTTP_AUTHORIZATION"];

            if(authorization != null)
                if(authorization.Length > 1000)
                    authType = "Kerberos";
                else
                    authType = "NTLM";
        }
        else    // If length != 0, it's probably Basic
            if(authType.Length == 0)
                authType = "Anonymous";

        return authType;
    }
}

At work, we implemented Integrated Windows Authentication using Kerberos for our intranet applications. This property proved to be quite useful in determining whether or not things were working as expected. To distinguish between NTLM and Kerberos authentication, it relies on the length of the HTTP_AUTHORIZATION server variable. As noted in the comments, NTLM headers are much shorter than Kerberos headers. Be aware that the HTTP_AUTHORIZATION variable is only available when the first page of the application is requested. On all subsequent page requests, it is not there so the best it can do is return the supplied AUTH_TYPE value of "Negotiate".

The class also contains a CurrentUser property that can be used to get the ID of the authenticated user. It returns the value of the User.Identity.Name property without the domain qualifier if one is present. For example, if it is MYDOMAIN\EWOODRUFF, this property returns EWOODRUFF. This saves you from having to check for and remove it if you do not need it.

Enhanced error information

When errors occur in your application, it is always good to have as much information as possible to help you duplicate the problem and find the source of the error. The default error page displayed by ASP.NET gives basic information about the cause of the error. It also lets you override the error handling features and specify a custom error page. There are many options available such as writing the information to the event log, writing it to a text file on the server, sending it in an e-mail to the developer, etc. Doing things like writing to the event log require special permissions on the server. As such, I decided to keep the error handling behavior of the BasePage class fairly generic. The decision about what to do with the error information is deferred to the custom error page. It can be modified based on the application or environment, to log or display the information as it sees fit. You may also find that, once written, the custom error page can be copied from one application to another without change to provide the same error handling methodology in all of your applications.

Normally, detailed error information is not available by the time you reach the custom error page. To overcome this limitation, the class overrides the OnError method, and simply packages the information up and stores it in the application cache. The custom error page can then retrieve it and complete its task of reporting the error as it sees fit:

protected override void OnError(System.EventArgs e)
{
    string remoteAddr;

    Hashtable htErrorContext = new Hashtable(5);
    SortedList slServerVars = new SortedList(9);

    // Extract a subset of the server variables
    slServerVars["SCRIPT_NAME"] =
        Request.ServerVariables["SCRIPT_NAME"];
    slServerVars["HTTP_HOST"] =
        Request.ServerVariables["HTTP_HOST"];
    slServerVars["HTTP_USER_AGENT"] =
        Request.ServerVariables["HTTP_USER_AGENT"];
    slServerVars["AUTH_TYPE"] = this.AuthType;
    slServerVars["AUTH_USER"] =
        Request.ServerVariables["AUTH_USER"];
    slServerVars["LOGON_USER"] =
        Request.ServerVariables["LOGON_USER"];
    slServerVars["SERVER_NAME"] =
        Request.ServerVariables["SERVER_NAME"];
    slServerVars["LOCAL_ADDR"] =
        Request.ServerVariables["LOCAL_ADDR"];
    remoteAddr = Request.ServerVariables["REMOTE_ADDR"];
    slServerVars["REMOTE_ADDR"] = remoteAddr;

    // Save the context information
    htErrorContext["LastError"] =
        Server.GetLastError().ToString();
    htErrorContext["ServerVars"] = slServerVars;
    htErrorContext["QueryString"] = Request.QueryString;
    htErrorContext["Form"] = Request.Form;
    htErrorContext["Page"] = Request.Path;

    // Store it in the cache with a short time limit. The
    // remote address is used as a key. We can't use the
    // session ID or store the info in the session as it's
    // not always the same session on the error page.
    Cache.Insert(remoteAddr, htErrorContext,
        null, DateTime.MaxValue, TimeSpan.FromMinutes(5));

    base.OnError(e);
}

The code creates a sorted list to contain several helpful server variables such as the authentication method that was in effect, user information, server information, and script information. Rather than store them all, I have only saved the ones that I have found useful in the past. You may wish to add to the list if necessary. The list is stored in a hash table along with the last error information, query string, form variables, and the page name. In order to pass the information to the custom error page, the hash table is stored in the application cache using the remote address as the key. A five-minute time limit is applied to the object so that it does not stay in the cache for an extended period of time holding on to resources unnecessarily. The custom error page can also delete the object from the cache once it has retrieved it. This method should work well for most applications. Unless you are expecting an extremely large number of users and there was an unexpected error that everyone got, it should not put much of a load on the server.

As noted, the error information is not stored in the session, nor does it use the session ID as a key. The reason is that on occasions, based on my experience, when the error page is reached, the session ID is completely different and thus we have no way to retrieve the information. I have noticed this most often when an error occurs on the first page loaded for the application. By using the application cache and using the remote address as the key, we can be sure that the error information is always available to the error page when it is reached.

To get ASP.NET to call your custom error page, you need to modify the customErrors tag in the Web.config file so that the defaultRedirect attribute points to your error page and the mode attribute is set to On or RemoteOnly:

  <system.web>
    <!--  CUSTOM ERROR MESSAGES
          Set customErrors mode="On" or "RemoteOnly" to
          enable custom error messages, "Off" to disable.
          Add <error> tags for each of the errors
          you want to handle.
    -->
    <customErrors mode="RemoteOnly"
        defaultRedirect="ErrorPageInternal.aspx" />
  </system.web>

The demo project in the download file contains an example that displays the information retrieved from the application cache after an error occurs.

The RenderedPage class

There have been several articles both here and on several other sites that describe the reasons for and various ways to create base page or template classes for ASP.NET Web Forms. As such, I will not rehash that information here. Instead, refer to the following Code Project articles for more background on why and how to use these methods. The RenderedPage class contains my particular implementation, and its various features are described in the remainder of this article.

ASP.NET 2.0 includes a new master page feature that lets you implement page templates with much more flexibility. As such, if you are using ASP.NET 2.0, I would recommend using master pages rather than RenderedPage. However, you can still derive your page classes from BasePage in order to gain the features described earlier and in the other articles in this series and use them in conjunction with master pages.

Note that the .NET 2.0 version of the BasePage class does support the use of the PageDescription, PageKeywords, PageTitle, and Robots properties. In order to use them, just make sure that your page or master page contains a head tag with a runat="server" attribute. When rendered, the class will add the appropriate tags to the header control for you. This allows you to modify them from page to page without any extra coding on your part.

Generation of common header and footer tags

The RenderedPage class will render all of the common header and footer tags including the DOCTYPE tag, the html opening and closing tags, the head opening and closing tags, a few common meta tags such as the description, keywords, and robot instructions, a link tag for the style sheet that can be defined via the PageStyleSheet property, a title tag containing the page title as set via the PageTitle property, and the opening and closing body tags. The body tag can be modified to include a class attribute as defined by the PageBodyStyle property to alter the style of the page body. A detailed description of each of the rendering methods can be found in the supplied help file.

The Render method is overridden to control the rendering process. Unless it is necessary, you should not override this method to alter rendering of the page content. Instead, override the following virtual methods as needed. OnInit can also be overridden to insert controls via the PageForm property. The MenuPage class contains an example of that.

  • protected virtual void RenderHeader(HtmlTextWriter writer)

    This method is called first to render the common header tags from the DOCTYPE tag to the opening body tag. The code is simple and uses a StringBuilder to generate the HTML to render. The StringBuilder is passed to the following method just before the closing head tag is added. The actual content of the page as defined in the ASPX file will be rendered immediately after the opening body tag when this method returns to the Render method.

  • protected virtual void RenderAdditionalHeaderTags(StringBuilder header)

    Override this method in derived classes to add other tags inside the head section (i.e., other meta tags for title, keywords, etc). The base class version does nothing. Additional tags generated by this method are inserted after the title tag and just before the closing head tag.

  • protected virtual void RenderFooter(HtmlTextWriter writer)

    This method can be overridden by the derived classes to add other common tags at the end of the body before the closing body tag. If overridden, call the base class method after outputting the additional tags unless you are completely replacing the rendering process for all of the closing tags normally generated by this method.

Adding additional controls to the form at runtime

Adding additional controls to the form that are created dynamically at runtime is quite simple. The PageForm property is used to obtain a reference to the form defined on the page. You can then use the Controls property of the Form object to insert additional controls anywhere within it. This is usually done in an overridden OnInit method. The MenuPage class uses this approach to insert the supporting HTML and a user control file to create pages with a menu.

The MenuPage and VerticalMenuPage classes

The MenuPage class is derived from RenderedPage and provides the layout for a page with a simple menu horizontally across the top of the page or vertically down the left side of the page. The menu is stored in the form of a user control that is loaded by the class at runtime and placed into the proper location. The user control can be changed using the MenuControlFile property. If not specified, it looks for a control by the name of MenuCtrl.ascx by default. Note that the class is not intended to compete with some of the more elaborate, full-featured menu custom controls that are available. It is just a simple class I use to get my applications up and running quickly using ASP.NET 1.0.

The bulk of the work takes place in the overridden OnInit method. Based on the setting of the verticalMenu field, it uses the BasePage.PageForm property to insert the HTML for a table control around the actual page content within the form control:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    // Insert the table and menu control at the start of
    // the page. The layout depends on whether or not
    // the menu is going to be rendered horizontally at
    // the top or vertically down the left side of the
    // page.
    if(verticalMenu)
    {
        this.PageForm.Controls.AddAt(0, new LiteralControl(
            "<table height='100%' cellpadding='0' " +
            "width='100%'>\n<tr valign='top'>\n" +
            "  <td width='15%'>\n"));
    }
    else
    {
        this.PageForm.Controls.AddAt(0, new LiteralControl(
            "<table cellpadding='0' width='100%'>\n" +
            "<tr>\n<td>\n"));
    }

    if(menuCtrl != null)
        this.PageForm.Controls.AddAt(1,
            LoadControl(menuCtrl));
    else
        this.PageForm.Controls.AddAt(1, new LiteralControl(
            "MenuControlFile property not set in derived " +
            "OnInit!"));

    if(verticalMenu)
    {
        this.PageForm.Controls.AddAt(2, new
            LiteralControl("</td><td> <" +
                "/td>\n<td>\n"));

        // Page content goes in between and this wraps it up
        this.PageForm.Controls.Add(
            new LiteralControl("</td>\n</tr>\n" +
                "</table>\n"));
    }
    else    // For horizontal, page is rendered below menu
        this.PageForm.Controls.AddAt(2,
            new LiteralControl("</td>\n</tr>\n" +
                "</table>\n"));
}

The VerticalMenuPage class is even simpler and only contains a constructor that sets the verticalMenu field to true so that the menu is rendered vertically by default. As noted, they are not the most sophisticated menu classes, but by deriving the ASPX pages from either class and creating a menu user control, you can get a simple application with a menu up and running in very little time.

Conclusion

I have used the BasePage class and a few derived from it in all of my ASP.NET applications to give them a consistent look, feel, and set of features. Hopefully, you will find this class and the others in the library, or parts of them, as useful as I have.

Revision history

  • 04/02/2006

    Non-breaking changes in this release:

    • The latest version of the JavaScript compressor is being used to further reduce the size of the embedded scripts.
    • The .NET 2.0 demos have been reworked to use master pages instead of RenderedPage and MenuPage.
    • For the .NET 2.0 version, the method of embedding the script resources was changed to use the .NET 2.0 method so it is no longer necessary to add the httpHandlers section for the EWSoftware.Web.aspx resource page. As such, this section can be deleted from your Web.config file.

    Breaking changes:

    • Significantly modified the BasePage class by splitting out the rendering code to its own derived class (RenderedPage). This makes it easier to move to .NET 2.0 and use master pages while still retaining the non-rendering related features of the BasePage class (data change checking, etc).
    • The EMailPage class has been removed. The e-mailing functionality has been merged with the BasePage class. This was necessary in order to move the rendering code into its own derived class.
    • Several properties and method names have been modified to conform to the .NET naming conventions with regard to casing (BasePage.BypassPromptIds, BasePage.DisabledCssClass, BasePage.DisabledCssName, BasePage.MsgLinkCssClass, BasePage.MsgLinkCssName, BasePage.SkipDataCheckIds, EMailPageEventArgs.SmtpServer, MenuPage.MenuCtrlFileName, PageUtils.HtmlEncode, RenderedPage.PageBodyCssClass, RenderedPage.CssFileName).
    • The BasePage.SetFocus methods have been renamed BasePage.SetFocusExtended. In .NET 2.0, every web control now has a Focus method. In addition, the standard Page class has two SetFocus methods somewhat equivalent to the old BasePage versions. The SetFocusExtended methods are available for use in .NET 1.1 to set the focus to controls and they are available for use in the .NET 2.0 version in case you need the added capabilities that they provide.
  • 11/26/2004

    Changes in this release:

    • Removed the hard-coded class name and cell padding from the MenuPage class' generated HTML. The menu user control should control the style and padding.
    • Fixed a bug in BP_funSetFocus() reported by Michael Freidgeim so that it finds controls by ID correctly if the ID happens to match a NAME attribute on a META tag.
    • Added RobotOptions enumerated type and a Robots property to BasePage to allow inclusion of a Robots meta tag in the page header.
    • Added PageDescription and PageKeywords properties to BasePage to allow inclusion of Description and Keywords meta tags in the page header.
  • 12/01/2003
    • Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here