This is the first. The classes in the assembly include:
BasePage
RenderedPage
MenuPage
VerticalMenuPage
PageUtils
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:
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. feature can also use an optional configuration option to control the name of the SMTP server (EMailPage_SmtpServer).
ErrorRptEMail
appSettings
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:
using EWSoftware.Web;
Imports EWSoftware.Web
System.Web.UI.Page
EWSoftware.Web
Page_Load
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 ...
}
}
If you decide to make use of the RenderedPage class, you will also need to do the following:
<!DOCTYPE>
<html>
</html>
<head>
<body>
</body>
<%@ Page>
<form>
</form>
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.
<link>
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:
head
runat="server"
form
EditItemTemplate
DataGrid
AuthType
OnError
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.
Enabled
true
false
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.
BasePage.DisabledCssName
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.
TextBox
DropDownList
ListBox
DisabledCssClass
public void SetEnabledState(WebControl ctl, bool enabled, string normalClass) 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.
foreach
public void SetEnabledState(string normalClass, bool enabled, params WebControl[] ctlList) 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:
Panel
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 [^].
MultiPage
PageView
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:
Focus
focus()
Page
SetFocus
SetFocusExtended
/// <span class="code-SummaryComment"><summary></span>
/// This sets the control that should have the focus when the
/// page has finished loading by control reference.
/// <span class="code-SummaryComment"></summary></span>
public void SetFocusExtended(WebControl ctl)
{
if(ctl != null)
{
focusedControl = ctl.ClientID;
findControl = false;
}
else
focusedControl = null;
}
/// <span class="code-SummaryComment"><summary></span>
/// This sets the control that should have the focus when the
/// page has finished loading by control ID.
/// <span class="code-SummaryComment"></summary></span>
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.
focusedCtl
findCtrl
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.
string
focusedCtrl
txtName
dgGrid:_ctl5:txtName
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:
null
Nothing
// 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.
OnPreRender()
ConvertValMsgsToLinks()
ValidationSummary
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:
BP_funSetFocus()
document.getElementByID()
// 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.
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".
HTTP_AUTHORIZATION
AUTH_TYPE
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.
CurrentUser
User.Identity.Name
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:
customErrors
defaultRedirect
mode
On
RemoteOnly
<system.web>
<!--<span class="code-comment"> 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.
--></span>
<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.
PageDescription
PageKeywords
PageTitle
Robots
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.
DOCTYPE
html
meta
link
PageStyleSheet
title
body
class
PageBodyStyle
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.
Render
virtual
OnInit
PageForm
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.
StringBuilder
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.
Controls
Form
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.
MenuControlFile
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:
verticalMenu
BasePage.PageForm
table
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.
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.
Non-breaking changes in this release:
httpHandlers
Breaking changes:
BasePage.BypassPromptIds
BasePage.DisabledCssClass
BasePage.MsgLinkCssClass
BasePage.MsgLinkCssName
BasePage.SkipDataCheckIds
MenuPage.MenuCtrlFileName
PageUtils.HtmlEncode
RenderedPage.PageBodyCssClass
RenderedPage.CssFileName
BasePage.SetFocus
BasePage.SetFocusExtended
Changes in this release:
NAME
META
RobotOptions
Description
Keywords
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
<%@ Register TagPrefix="EWSoftware" Namespace="EWSoftware.Web" Assembly="EWSoftware.Web" %>
PageHeader.LinkRegEx = New Regex("<link.+?>", RegexOptions.IgnoreCase Or RegexOptions.Compiled)<br />
PageHeader.ScriptRegEx = New Regex("<script.+?</script>", RegexOptions.IgnoreCase Or RegexOptions.Compiled)<br />
PageHeader.TitleRegEx = New Regex("<title>.+?</title>", RegexOptions.IgnoreCase Or RegexOptions.Compiled)<br />
<br />
Dim mc As MatchCollection<br />
Me.Scripts = New StringCollection<br />
Me.Stylesheets = New StringCollection<br />
<br />
_TopContainer = New PlaceHolder<br />
<br />
mc = PageHeader.LinkRegEx.Matches(htmlSource)<br />
For Each m As Match In mc<br />
If m.Success Then<br />
Me.Stylesheets.Add(m.Value)<br />
End If<br />
Next<br />
<br />
mc = PageHeader.ScriptRegEx.Matches(htmlSource)<br />
For Each m As Match In mc<br />
If m.Success Then<br />
Me.Scripts.Add(m.Value)<br />
End If<br />
Next<br />
<br />
mc = PageHeader.TitleRegEx.Matches(htmlSource)<br />
If mc.Count = 1 AndAlso mc(0).Success Then<br />
_Title = mc(0).Value.Substring(7, mc(0).Value.Length - 15)<br />
End If
General News Suggestion Question Bug Answer Joke Praise Rant Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.