|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Table of contentsIntroductionThis 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:
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. 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 Embedded resourcesThe 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 compressionThe 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 projectsThe 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 applicationsUsing the page classes in your own applications is fairly straightforward. Just follow these steps:
If you decide to make use of the
No styles in design viewOne problem when using the For .NET 2.0, a better solution is to use a master page instead of The BasePage classThis article presents a class called
Enabling and disabling form controlsEnabling and disabling controls is a simple matter of setting their
Setting control focusPrior 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 /// <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 The second version is passed the control ID to give the focus as a To clear the focus, pass // Clear the focus
this.SetFocus((string)null);
The 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 // 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 // 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 typeThe 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 The class also contains a Enhanced error informationWhen 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 Normally, detailed error information is not available by the time you reach the custom error page. To overcome this limitation, the class overrides the 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 <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 classThere 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 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 Note that the .NET 2.0 version of the Generation of common header and footer tagsThe The
Adding additional controls to the form at runtimeAdding additional controls to the form that are created dynamically at runtime is quite simple. The The MenuPage and VerticalMenuPage classesThe The bulk of the work takes place in the overridden 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 ConclusionI have used the Revision history
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||