Title: ASP.NET Common Web Page Class Library - Part 1
Author: Eric Woodruff
Email: Eric@EWoodruff.us
Environment: Visual Studio .NET, IIS, ASP.NET, C#, VB.NET
Keywords: page, template, focus, detect, change, error, enable, disable, authentication
Level: Intermediate
Description: A set of common, reusable page classes for ASP.NET applications
Section ASP.NET
SubSection General
Table of Contents
The BasePage Class
The RenderedPage Class
Conclusion
Revision History
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 downloads contains a small demo application in C# and VB.NET that
makes use of the 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 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 the 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 www.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 to 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 the
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 www.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 some 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 through 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 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's 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 |