Table of contents
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
).
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.
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.
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 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.
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.
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 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 TextBox
es, DropDownList
s, and ListBox
es. 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 TextBox
es, DropDownList
s, and ListBox
es. 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 Panel
s 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(ctlPageForm == null)
ctlPageForm = this.PageForm;
controlType = ctlPageForm.ToString();
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);
if(form == null)
return;
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
{
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);
}
}
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 [^].
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:
public void SetFocusExtended(WebControl ctl)
{
if(ctl != null)
{
focusedControl = ctl.ClientID;
findControl = false;
}
else
focusedControl = null;
}
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:
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;
if(bFindCtrl == false)
{
ctl = document.getElementById(strID);
if(ctl != null && typeof(ctl) != "undefined" &&
(typeof(ctl.id) != "string" || ctl.id != strID))
bFindCtrl = true;
}
if(bFindCtrl == true)
{
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(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:
if(typeof(ctl.parentElement) != "undefined")
{
ctlParent = ctl.parentElement;
while(ctlParent != null && ctlParent.tagName != "PageView")
ctlParent = ctlParent.parentElement;
if(ctlParent != null && ctlParent.tagName == "PageView")
{
nPgIdx = ctlParent.PageIndex;
ctlParent = ctlParent.parentElement;
if(ctlParent != null && ctlParent.tagName == "MultiPage")
{
ctlParent.selectedIndex = nPgIdx;
htmlColl = document.getElementsByTagName("TabStrip");
for(nIdx = 0; nIdx < htmlColl.length; nIdx++)
if(htmlColl[nIdx].targetID == ctlParent.id)
{
htmlColl[nIdx].selectedIndex = nPgIdx;
break;
}
}
}
}
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 [^].
if(ctl.tagName == "TABLE")
{
ctl = ctl.cells(0);
ctl = ctl.firstChild;
}
ctl.focus();
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.
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
{
if(this.Context == null)
return null;
string authType =
Request.ServerVariables["AUTH_TYPE"];
if(authType == "Negotiate")
{
string authorization =
Request.ServerVariables["HTTP_AUTHORIZATION"];
if(authorization != null)
if(authorization.Length > 1000)
authType = "Kerberos";
else
authType = "NTLM";
}
else
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.
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);
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;
htErrorContext["LastError"] =
Server.GetLastError().ToString();
htErrorContext["ServerVars"] = slServerVars;
htErrorContext["QueryString"] = Request.QueryString;
htErrorContext["Form"] = Request.Form;
htErrorContext["Page"] = Request.Path;
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>
<!---->
<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.
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.
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 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
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);
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"));
this.PageForm.Controls.Add(
new LiteralControl("</td>\n</tr>\n" +
"</table>\n"));
}
else
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.
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.