Wednesday, June 27, 2007

ASP.NET Development Strategies

By: Nick Hodges

Abstract: Strategies, tips, and techniques for building Web sites with ASP.NET and Delphi. We discuss how to manage entire sites to make them easy to maintain and enhance. Rather than focusing on lower-level techniques, we emphasize how to architect and build a whole site solution.�

Nick Hodges has been a Delphi developer from the very beginning. He started programming with Turbo Pascal for Windows when it came out and has been hooked on Object Pascal ever since. He served as a Naval Intelligence Officer for twelve years, receiving a Masters of Science in Information Technology Management from the Naval Postgraduate School in 1995. After leaving the Navy, He worked at Xapware Technologies and in 2001 struck out on his own, eventually helping to form Lemanix. Nick is a frequent speaker, trainer, and author. You can find out more about Nick at www.nickhodges.com and read his blog at http://www.lemanix.com/nick.

nick@lemanix.com
http://www.lemanix.com/nick

 

ASP.NET Development Strategies

by Nick Hodges

Table of Contents

Introduction

Strategies:
Manage users to provide a customized experience
Manage custom application settings
Handle errors in both a developer and user-friendly manner
Give your site a consistent look with a “master pages” approach
Improve performance via caching

Conclusion

Introduction

ASP.NET is cool, and it gives you the power you need to start easily building the types of web sites that you really want to build. The cool thing about it is that it is quite easy to get up and running with the basics. Putting controls on a page, and displaying data is pretty simple, and setting up a web site doesn't take a lot of effort.

There are tons of articles out there outlining these basics, but most of them seem to delve into a lot of the mundane, lower-level tasks. But what of bigger picture issues? What about handling and managing users and their preferences? What about error handling? What about setting up page templates and customizing the content?

This paper will cover those topics, and more. The point of this paper will be to take a more strategic, 10.000 foot view of ASP.NET with Delphi, and see some of the bigger picture things that you can to do build power, dynamic, user-preference driven web sites. ASP.NET has a lot of power under the hood, and I aim to reveal some of that power in the sections below.

Assumptions

This paper assumes that you are already familiar with how ASP.NET works. I won't be discussing the mechanics of using the designer, managing controls and ASPX pages, etc. The idea here is to discuss ways to make things work better once you know all that basic stuff.

A Few Terms

Throughout the paper, I'll refer to the ASP.NET Framework, the FCL, just “ASP.NET”, the Framework, and other similar terms. They all refer to the Framework Class Library, specifically the classes in the System.Web namespace. I'll try to be consistent, but I apologize if I'm not. I hope that you understand what I mean.

Strategy: Manage users to provide a customized experience.

Authentication and Authorization

Probably the first thing that any self-respecting web site will want to do is to enable user login. Whether you need to manage the site at the user level for security or for simply providing a custom user experience, being able to accept a user's login information and grant access and custom content is a basic function that almost all web sites should provide. And lucky for us, ASP.NET provides a powerful infrastructure for doing just that. Of course, enabling user login means being able to authenticate a user, and then authorizing access to functionality based on that authenticated state. ASP.NET provides an infrastructure for doing these two functionalities in a number of different ways, utilizing the existing operating system, the .Net Framework, or your own implementation.

In an ASP.NET application, authorization occurs at two basic levels – first at the IIS level, and then at the ASP.NET level. If IIS isn't configured to allow access to certain directories, files, or other resources, then IIS will reject a user request before ASP.NET or your application ever gets a hold of it. But we aren't interested in that – what we are interested in is what happens when user requests a URL that you want them to get to.

A typical web application will likely have sections that can be viewed by any user, and other sections that can only be viewed by authorized users. Examples might be the administration portion of an application that is accessible only to the site administrators, or a membership portion of the site that is available only to users who have paid a membership fee or otherwise been blessed to access that portion of the site. ASP.NET allows you to easily handle all these situations either by using the functionality built into the framework, or by using functionality that you provide yourself.

Authentication Methods

ASP.NET provides three main ways that a user can be authenticated. They are outlined in the table below:

Authentication Type

Description

Forms Authentication

Uses a user defined interface and authentication method to verify users

Windows Authentication

Uses the Windows Security scheme to authenticate users. Requires that users be running a Windows Client

Passport Authentication

Uses Microsoft's Passport system to authenticate users.


In all likelihood, you aren't going to want to use either Passport Authentication or Windows authentication in your web applications. Passport authentication requires that you buy and implement a Passport server, and Windows authentication basically requires that your clients all be running Windows.

More likely, you'll probably want to do what most web sites out there do and that is provide Forms Authentication. Forms Authentication allows you to provide your own login form via an ASP.NET page, and then authenticate the user with criteria you code yourself. Most websites you see out there do something like this – very view of them use the HTTP authentication schemes anymore, mostly because they are very hard to customize and control. Chances are, you'll want to do the authentication against a database, and Forms Authentication makes that very easy.

Forms Authentication Example

The demo application for this section uses Forms Authentication to both authenticate and authorize users. It begins with a home page in the root directory that has two links on it – one to a page merely requiring authentication, and another that requires authorization. The application defines a user 'nick', who has the role 'admin' and a user 'frank' who doesn't have any role assigned.

To get the application to use Forms Authentication, we need to make an additional entry into the root web.config file as follows:

<authentication mode="Forms">
<forms loginUrl="login.aspx">
</forms>
</authentication>

The <authentication> tag uses the 'mode' attribute to determine the type of authentication used, in this case “Forms”. (Note that these tags and attributes are case-sensitive.) The <forms> tag uses the loginURL attribute to provide the page that unauthenticated users should be sent to when they need to login. This entry will then ensure that any attempt to access content by unauthenticated or unauthorized users will result in the user being sent to the login page.


Partitioning Off Sections of an Application


Managing users and user access in an ASP.NET application is really more about the process of managing directories for the application than specifically managing users per se. Pages that requires special access – authentication, authorization, or both – need to be placed in their own directories beneath the applications main directory. Each directory is then given its own web.config file that defines who can see the content found in that directory. It's actually pretty straight forward.


Therefore, I suggest that your application be designed with two levels of directories for your ASPX pages. The root directory will contain the Home/Main page and any other content that can be viewed by all users without any authentication required. One level below this directory should be directories that contain content that requires authentication and authorization.


For instance, you may have your application's virtual directory point to the c:myapp directory. In that directory will be home.aspx and generalcontent.aspx, and other pages that all users can view. Your application may have content viewable only by registered members, and that might be found in the c:myappregusers directory. Then, you may have some administration functionality for the site that only is accessible to site administrators – that content would be found in c:myappadmin. Both of the subdirectories, regusers and admin, would have customized web.config files that limit access to registered users and administrators respectively.


In the case of our demo application, there are two sub-directories authenticated and authorized. The first allows users who are properly logged in to see its content. The second only allows logged in users with the proper role settings to see the content within it. Ensuring that that these two scenarios is done with custom web.config files.


Settings in the Web.Config File


In order to limit access to subdirectories, place a customized web.config file in the directory in question. Web.config files us a sort of inheritance model, in that all of them “inherit” their settings from the machine.config file in the root directory of the framework code. Each ASP.NET web application must have a web.config file in its root directory that inherits all the settings from machine.config. All the settings in the root web.config file augment or override the machine.config file. Web.config files place in subdirectories inherit all the settings from their “parent' web.config file, but any settings in the sub-directory based web.config files override those of the “parent”.


Thus, in order to limit access to the content of a sub-directory to anonymous users – and thus giving access only to authenticated users, you can place a web.config file in that directory that looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>

The key line here is the <deny users=”?”/>, which means “deny access to all anonymous users”. If you wanted to deny access to all users altogether, then you could use <deny users=”*”/>. The previous can be used in conjunction with the 'allow' attribute, as follows:

<authorization>
<allow users=”nick' />
<deny users="*" />
</authorization>

which would allow access to the user named 'nick', and then deny access to everyone else.


In order to limit access to authorized users, you have to provide a web.config file that defines what roles are allowed to access a given directory. For example, to limit access to only those users who have the 'admin' role defined, then use a web.config file like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<authorization>
<allow roles="admin"/>
<deny users="*" />
</authorization>
</system.web>
</configuration>

This version uses the “allow/deny” pattern shown above, first allowing all users with the role 'admin', and then denying everyone else access.


The Code to Make All This Work


Of course, none of this can happen without some code to make things work as desired. The first thing that must happen is that users are asked to login. In the case of the demo application, a user can click on either of the two links provided, and because both links require authentication, the user is automatically redirected to the login page. He can then type in the proper credentials (in this case 'nick' and 'borcon') and press the login button. The code for the click handler for the login button looks like this:

procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs);
var
TempUsername: string;
TempPassword: string;
TempAuthenticated: Boolean;
begin
TempUserName := UserNameTextBox.Text;
TempPassword := PasswordTextBox.Text;
TempAuthenticated := AuthenticateUser(TempUsername, TempPassword);
if TempAuthenticated then
begin
ErrorMessageLabel.Visible := False;
FormsAuthentication.RedirectFromLoginPage(TempUsername, False);
end else
begin
ErrorMessageLabel.Visible := True;
end;
end;

This code is fairly straight-forward. Upon clicking the button, the data entered into the two edit boxes is grabbed, and passed to the AuthenticateUser function (See below). If the user is authenticated, then the FormAuthentication class is used to redirect the user from the login page to whatever page they were going to when they were sent to the login page in the first place. If the login information is not valid, then an error message is displayed.


Note the URL that is used when a user is redirected to the login page – it might look something like this:


http://localhost:8080/WebApplication15/login.aspx?ReturnUrl=%2fWebApplication15%2fauthenticated%2fAuthenticated.aspx


Note that it has the ReturnURL value in the query string:


FormsAuthentication is a class whose job it is to manage users using the Forms Authentication system. It provides a number of methods to manage users, the cookie for identifying the user, and for controlling login and logout. An instance of this object will always exist within the context of your application.


The AuthenticateUser function is implemented as follows:

function TWebForm1.AuthenticateUser(aUsername, aPassword: string): Boolean;
begin
Result := ((aUsername = 'nick') and (aPassword = 'borcon'))
or ((aUsername = 'frank') and (aPassword = 'borland'));
end;

This code is obviously a very simple way of authenticating users. Of course, this method could be implemented to authenticate the data against a database, but for simplicities sake here, I've just authorized two users, 'nick' and 'frank'. The function returns 'True' if the proper credentials are presented. Pretty simple.


And of course, once the user is authenticated, then he is returned to the page in question.


Role Handling


The application also contains content in the authorized directory which, as shown above, is only accessible to users who have the 'admin' role specified.


To give certain roles to specific users, you should write an event handler for the TGlobal.Application_AuthenticateRequest in the global.pas unit of your application. This event fires whenever a user is authenticated, and thus it can be used to assign credentials to a user as he logs in. In our demo application, the code for the event handler looks like this:

procedure TGlobal.Application_AuthenticateRequest(sender: System.Object; e: EventArgs);
var
RoleString: System.string;
Roles: array of string;
begin
if not Request.IsAuthenticated then
begin
Exit;
end;
RoleString := GetRole(HttpContext.Current.User.Identity.Name);
Roles := RoleString.Split([',']);
Context.User := GenericPrincipal.Create(User.Identity, Roles);
end;

First, it checks to see if the user is authenticated, and if not, then it exits. But if the user is authenticated, it calls GetRole (defined below) and determines what the roles of the given user are. (Roles are typically defined in a comma-delimited string). It then “splits” that string into an array of strings, and uses that, as well as the IIdentity of the user, to create a new instance of the GenericPrincipal class and assign it to the current user. In this way, the roles for the current user are set.


GetRole is implemented as follows:

function TGlobal.GetRole(aUser: string): string;
begin
Result := '';
if Context.User.Identity.Name = 'nick' then
begin
Result := 'admin';
end;
end;

Once again, this code is very simple yet illustrative. You'd very likely look the data up in a database rather than simply assigning the values manually as is done here.


Providing Custom Content Based on Roles and User Information


There may be times when you want all authenticated users to see a page, but you only want to display specific features or controls based on specific users or user roles.


You can limit what controls are seen by specific users by setting their Visible property based on a user's credentials. For instance, on the authenticated.aspx page, there is a custom greeting message, as well content visible only to users who have the 'admin' role. That is done as follows:

procedure TWebForm2.Page_Load(sender: System.Object; e: System.EventArgs);
begin
Label1.Text := 'Welcome aboard, ' + HTTPContext.Current.User.Identity.Name;
AdminButton1.Visible := HTTPContext.Current.User.IsInRole('admin');
AdminLabel1.Visible := HTTPContext.Current.User.IsInRole('admin');
end;

The above code first sets a label to display the user's name. (Note that only the login name is available in the GenericPrincipal class. If you want to keep track of more user information than that, you can store that in the Session data, or build your own implementation of the IPrincipal interface to maintain that data.


It then shows a button and a label only if the user has the role 'admin' set. This is done by calling the IsInRole function with is part of the IIdentity interface, which is in turn provided by the IPrincipal interface. (The User class implements the IPrincipal interface).


Strategy: Manage custom application settings


Most every regular old client application stores some sort of configuration and initialization information. Sometimes that information is stored in an INI file, an XML file or the Windows registry. ASP.NET applications may very well need to store and manage application level configuration information as well. Generally, these settings will be read only, and apply to the application as a whole. To wit, I am not talking about storing user-specific settings and preferences, but instead about information about how the application as a whole is to be run and initialized. For instance, you may want to turn on or off tracing or logging in your application or you might want to store a connection string for connecting to a database and not hard-code it into your application's binary. Custom application settings allow you to determine these types of things, and even change this while the application is deployed. And I know that this will be hard for you to believe, but the Framework actually provides classes for this! Amazing, huh?


Additions to the Web.Config File


Application settings for an ASP.NET application are typically stored in the web.config file. They are placed in the <configuration> section and should use the <appSettings> tag. (Note the case. Unfortunately, almost all the stuff in web.config files is case-sensitive. Yuk.) A sample web.config might look like this:

<configuration>
<system.web>
...
</system.web>
<appSettings>
<add key="connectionstring" value="Server=localhost;Database=NorthWind;User ID=sa;
Password=;Trusted_Connection=False" />
<add key="SomeSetting" value="Off"/>
<add key="AnotherSetting" value="1"/>
<add key="YetAnotherSetting" value="yes"/>
</appSettings>
</configuration>

You can add as many keys as you like to the <appSettings> section by simply defining a key and a value. Then, you can use the System.Configuration.ConfigurationSettings.AppSettings class to retrieve the values.


I've included with the code that accompanies this paper a unit called Lemanix.Web.AppSettingsUtils which has the two following functions in it:

function GetAppSetting(aKey: string): string;
begin
Result := System.Configuration.ConfigurationSettings.AppSettings[aKey];
end;

function FeatureIsSet(aKey: string): Boolean;
begin
Result := (GetAppSetting(aKey).ToLower = 'on')
or (GetAppSetting(aKey).ToLower = 'true')
or (GetAppSetting(aKey).ToLower = 'yes')
or (GetAppSetting(aKey)[1] in ['1', 't', 'y', 'T', 'Y']);
end;

These two functions can be used to retrieve <appSettings> values. The first merely takes a given key and retrieves the value. The second is designed to retrieve boolean values. The convention for <appSettings> is “On/Off”, but FeatureIsSet is designed to be a bit more flexible.


Thus armed with the above utility functions, you can initialize your application as needed.


The accompanying code shows a very simple, one page application that reads some values from the <appSettings> in the web.config file and changes the page accordingly. The code for that is as follows:

procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs);
begin
// TODO: Put user code to initialize the page here
Label1.Text := 'The connection string would be: ' + GetAppSetting('ConnectionString');
CheckBox1.Checked := FeatureIsSet('SomeSetting');
CheckBox2.Checked := FeatureIsSet('AnotherSetting');
CheckBox3.Checked := FeatureIsSet('YetAnotherSetting');
end;

You might note that there are actually two example applications that do the same thing. One of them, in the InConfigFile directory, holds the <appSettings> keys and values right in the web.config file. The other, in the InExternalFile directory, holds them in an external file. The entry in the web.config file of the latter looks like this:

 <appSettings file="appsettings.config" />

with the contents of appsettings.config being:

 <appSettings>
<add key="connectionstring" value="Server=localhost;Database=NorthWind;User
ID=sa;Password=;Trusted_Connection=False" />
<add key="SomeSetting" value="Off"/>
<add key="AnotherSetting" value="1"/>
<add key="YetAnotherSetting" value="True"/>
</appSettings>




Both applications function almost exactly the same, with the latter merely allowing you to store the settings in an external file. I say “almost” because there is one key difference. If you make changes to the web.config file for an ASP.NET application, the application will be recompiled upon the next request. Thus, you can make changes to the settings in an <appSettings> section, and they will be reflected in the next request to your application. However, if you make changes to an external file holding the settings, the changes will not be reflected until you stop and restart the server.


Strategy: Handle errors in both a developer and user-friendly manner


Even the best of us have problems with our code. And yes, even uber-programmers like you and me sometimes have code that raises exceptions. And, good grief, we all know that those nutty users of ours can do all kinds of things to make an application go haywire. Well, what to do when a problem occurs in our ASP.NET application? How should we handle errors and exceptions? Well, probably to no one's surprise, Delphi and the ASP.NET architecture help us out here.


ASP.NET provides a generic error handling solution. By default, when an exception occurs, requests made by the local machine see a detailed error message describing the exception and including a stack trace. Users on the “outside” see some innocuous error message. Of course, this isn't really what you want them to see – you'd much rather customize a response that is more helpful than the scary, unhelpful default message.


Any error pages that your users see should be gentle and helpful, both for you and the user. Thus, the default error handling just won't do.


Handling Standard HTTP Messages


Probably the most common errors that occur are simple HTTP errors such as the “404 – Not Found” error. By default, some of these standard errors, such as the 403 code, will be handled by IIS. But some of them will trickle through to your application, and you can provide customized pages for them.


To do that, you make an entry into the web.config file for your application. The <customErrors> section determines how the application will deal with errors. If you want to provide a customized page for a 404 error, you can make the following entry into your web.config file:

    <customErrors defaultRedirect="errors/GenericError.aspx" mode="On">
<error statusCode="404" redirect="errors/NotFound.aspx" />
</customErrors>

The <customErrors> (again, note the case!) section defines how your application will handle errors. The mode attribute can be set to “On|Off|RemoteOnly”. When set to “On”, the system will always direct errors to the custom page defined in the defaultRedirect attribute, unless customized by an <error> tag within the <customErrors> tag. When set to “Off”, all users, including local users, see a 'friendly' (i.e., devoid of debug and stack trace information), default ASP.NET error page. When set to “RemoteOnly” (note the inconsistent use of case...sigh), then only remote users will see the default messages. When the error mode is set to “On”, the system assumes that you will handle displaying information about all the errors that occur in your application. Hence, you provide the content for the page named in the defaultRedirect attribute.


Anyway, the above tag will redirect 404 errors to the “NotFound.aspx” page. In this way you can provide a more friendly “not found” message. The statusCode attribute obviously denotes which standard HTTP error to deal with, and the redirect attribute tells the system where to direct the wayward user.


Handling Exceptions


Of course, HTTP errors aren't the only thing you will run across. Good old fashioned exceptions are another thing that you'll need to deal with. Once you set the <customErrors> mode to “On”, you are responsible for handling all the exceptions that propagate outside of any method. The defaultRedirect attribute of the <customErrors> tag defines a page that will be displayed when your application encounters an un-handled exception. It is then up to you to use that page to deal with the exception, usually by displaying a more user-friendly page than the default one. There are a number of ways of going about doing that.


Handling Exceptions at the Method/Code Level


Handling exceptions at the code level is what you are pretty much used to – using try..except..end blocks to trap and handle exceptions. From the ASP.NET developer perspective, this is nothing special. You should be doing this normally anyway in a lot of code, doing things like rolling back transactions, etc. But often, these exceptions are re-raised and need to be reported, logged, viewed, or otherwise acknowledged. Naturally, if you trap an exception with a method and don't let it propagate out, there's nothing further that you as an ASP.NET developer need to do.


Handling Exceptions at the Page Level


The standard ASP.NET Page class offers an OnError event for which you can provide and event handler and process the exception within a single page. The demo application for this chapter shows how to do that. First, declare an event handler method:

  private
{ Private Declarations }
procedure Page_Error(Sender: System.Object; e: System.EventArgs);

then hook it to the actual event:

procedure TWebForm1.InitializeComponent;
begin
...
Include(Self.Error, Self.Page_Error);
...
end;

and implement it as follows:

procedure TWebForm1.Page_Error(Sender: TObject; e: System.EventArgs);
var
LastException: Exception;
begin
LastException := Server.GetLastError.GetBaseException;
if LastException is PageLevelException then
begin
Response.Write('<p>There was an exception, and it was handled at the page
level.</p>');
Response.Write('<p>The error message was: ' + LastException.Message + '</p>');
Context.ClearError;
end else
begin
if LastException <> nil then
begin
raise LastException;
end;
end;
end;

The code starts out by grabbing the exception. It makes a call to Server.GetLastError.GetBaseException to get a hold of the original exception.


As the exception moves through the stack, it may get wrapped into another exception. .Net exceptions can be “daisy-chained” together, using the InnerException property of the Exception class. It's actually a pretty cool way of doing things.


In the case of our demo application, the application artificially raises two different exceptions AppLevelException and PageLevelException, allowing the application to illustrate how you might want to handle a specific exception type on a specific page while allowing the rest to propagate up the stack. If a PageLevelException is found, then the code returns a simple response making note of the exception. If it isn't a PageLevelException, then the exception is re-raised and allow to move on. Note that if an exception reaches the Page.OnError event handler, that page will not be rendered. This is key to note, because if you want to handle an exception at the page level, you'll need to provide an alternative to displaying the page itself.


Handling Exceptions at the Application Level


Exceptions can be handle on an application-wide basis as well. The TGlobal class in the global.aspx/global.pas unit provides the Application_Error method which is fired whenever an un-handled exception bubbles up to the application object, much like the Application.OnException event found in the VCL. Code in the Application_Error method will work exactly like above in the Page_Error method. The exception can be gathered using the Server.GetLastError call, and then dealt with appropriately.


In the case of the sample application, when an exception is raised and left un-handled, the system directs the user to the page defined in the defaultRedirect attribute of the <customErrors> tag. From there, the page then displays the basics of the exception, namely the error message. Of course, in your application you could provide much more informative response than the one here, but the application illustrates the basics of how to do so.


A Simple, Application Level Exception Architecture


The code for this paper includes a very simple implementation of an error-handling methodology described in this article by Eli Robillard. The system merely provides a simple infrastructure for dealing with exceptions by providing a means of more robust exception handling than merely displaying the exception on a page. It may be that you want to log the exception or send an email to the system administrator notifying him that the error has occurred.


The first thing to note is that the application contains a number of entries in the <appSettings> section of the web.config file:

 <appSettings>
<add key="customErrorAutomaticLogging" value="Off" />
<add key="customErrorAutomaticEmail" value="Off" />
<add key="customErrorPage" value="errors/GenericError.aspx" />
<add key="customErrorBranchMethod" value="transfer" />
<add key="customErrorEmailAddress" value="errors@mySite.com" />
</appSettings>

In addition, the <customErrors> section looks like this:

    <customErrors defaultRedirect="errors/GenericError.aspx" mode="On">
<error statusCode="404" redirect="errors/NotFound.aspx" />
</customErrors>

The Application_Error event is implemented as follows:

procedure TGlobal.Application_Error(sender: System.Object; e: EventArgs);
var
LastException: Exception;

TempErrorPage: string;
TempBranchMethod: string;
TempEmail: string;
begin
// GetBaseException pulls out the *original* exception, as the
// page and application handlers wrap the Exception in their own
// exception instances.
LastException := Server.GetLastError.GetBaseException;

if ValueIsSet('customErrorAutomaticLogging') then
begin
WriteErrorToLog(LastException);
end;

if ValueIsSet('customErrorAutomaticEmail') then
begin
TempEmail := GetAppSetting('customErrorEmailAddress');
SendErrorEmail(LastException, TempEmail);
end;

Session['LastException'] := LastException;

TempErrorPage:= GetAppSetting('customErrorPage');

TempBranchMethod := GetAppSetting('customErrorBranchMethod');

if TempBranchMethod.ToLower = 'transfer' then
begin
Server.Transfer(TempErrorPage);
end else
begin
Response.Redirect(TempErrorPage);
end;
end;

This is pretty straight-forward, except perhaps for storing the Exception in the Session object. The appSettings are read, and based upon the settings, procedures are called to log the exception and email the designated person. A page is defined for responding to the exception, and a setting also determines whether the page will be shown using a call to Server.Transfer or Response.Redirect. If you call Response.Redirect, a new Context object will be created, and so all existing Response setting will be over-written. Response.Redirect also requires a round-trip to the server again, and rewrites the URL. Server.Transfer doesn't do any of this. (Note that Server.Transfer will thus not display the URL of the error page).


The other issue that may arise is the question of what to do if an exception occurs within one of the procedures called within Application_Error. You could eat all exceptions within these calls, but that isn't very helpful. Instead, it might be worthwhile to implement a very basic logging system using a simple text file to log these “Exceptions within exception” events.


Because the exception will result in the transfer of the response to another page, it is probably best to save the Exception in the Session object so that it will be available the custom error page. Thus, the Page_Load method of the custom error page looks like this:

procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs);
var
TempException: Exception;
begin
TempException := Session['LastException'] as Exception;
Label1.Text := TempException.Message;
end;

This code simply displays the error message of the exception, but of course could display anything you like.


Strategy: Give your site a consistent look with a “master pages” approach


I'll bet that most – if not all – of the pages on your website are about 90% the same. They probably are identical as far as headers and footers go. The menu on the left probably changes no more than to reflect the current page, and the ads at the right side are probably the same in that they are represented by the same controls that render different content on a cyclical basis. The only difference, probably, is the content in the middle section of the page. It may not be entirely like that, but chances are that much of the content on most of your pages is repetitive – that is, it is the same all the time. Naturally, you wouldn't want to have to put all that content on each ASPX page, and you certainly wouldn't want to have to make the same change for every page on your website. Instead, you'd want to make one change in one place that is then reflected on all the pages on your site.


Well, that's the idea behind “master pages” -- the concept that you manage one page that is the 'master' for all the others. ASP.NET 2.0 will incorporate the concept of master pages as a standard feature of a web application, but we don't have ASP.NET 2.0 just yet, do we (Not officially, anyway. There are beta copies of the Whidbey platform and development tools out there, but the official release hasn't happened yet.)


But of course, that doesn't deter us, does it? We can still get this functionality in the current ASP.NET platform. We just have to work a touch harder at the start, but once we get it down, it should be a piece of cake to manage your site using a master pages concept. There are a number of ways to provide this functionality out on the web. You can give these articles a look:


Master Your Site Design with Visual Inheritance and Page Templates by Fritz Onion


There is even a web control approach that you can download here. I found this a bit hard to deal with, but it is worth a look


A Simpler, More Straight-forward Approach


In the accompanying code, I've constructed a simple application that illustrates a simpler approach to master paging. It uses a single web page with a collection of user controls to provide content for the various sections of the page. The main content is determined by a query parameter in the URL. So therefore, to view a particular page, you simply refer to the home.aspx with a value for the query string of ?page=<pagename>. The cool thing about this approach is that you can cache the page based on the page parameter, and thus get easy caching of content with a single directive on the page. (Caching in general and the caching of page content in specific are discussed below) If your content doesn't update all that often, this is a straightforward way to cache all the pages in your site.


To set up the page to be a master page for the whole site, I created a single page called home.aspx, and put a simple HTML table into the page that would provide the basic layout. The table has three rows and three columns. The top and bottom rows span across all three columns. The basic code for the table looks like this:

<table>
<tr>
<td colspan=”2”>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td colspan=”2”>
</td>
</tr>
</table>

The row along the top is used to provide a header for the pages, and the row along the bottom a footer. The left middle column would normally hold a navigation control of some sort, the middle column the main content of the page, and the right column advertising information or other content.


The application then creates five user controls, one for each section of the table. These controls can then be manipulated as desired to provide the necessary content. For instance, the left middle column could build a menu based on the current page (Most menus make the current page a non-linkable item, for instance). The header and footer could be simple and remain the same for every page. The right side might present dynamic advertising. The middle section would provide the proper content for the page. For instance, the code in the ucMainContent.pas file for the user control that provides the main content looks like this:

function TWebUserControl1.GetPageName: string;
begin
Result := HTTPContext.Current.Request.QueryString['page'];
if Result = '' then
begin
Result := 'Home';
end;
end;

procedure TWebUserControl1.Page_Load(sender: System.Object; e: System.EventArgs);
var
TempPageName: string;
TempLabel: System.Web.UI.WebControls.Label;
begin
TempPageName := GetPageName;
// Here you'd get custom content based on the page name. For this
// demo, we'll just report back what the page name is
TempLabel := System.Web.UI.WebControls.Label.Create;
TempLabel.Text := System.String.Format('If this demo had a full-featured database, there would likely be some cool' +
' content for the <b>{0}</b> page here.', [TempPageName]);
Self.Controls.Add(TempLabel);
end;

Put all together, the URL

http://localhost:8080/MasterPagesDemo/Home.aspx

Results in something like this:

































My Fancy Website!


If this demo had a full-featured database, there would likely be some cool content for the Home page here.


Put some graphics or other content in here.


Main Menu


Some Link
More Info
HyperLink


Another Menu 


HyperLink
HyperLink
HyperLink


More Menus


HyperLink
HyperLink
HyperLink


This page is Copyright © 2004 by Acme Corporation
All Rights Reserved
Please report any problems with this site to the webmaster


 



Each of the sections of the above table (I've left the table borders in for illustrative purposes) is produced by a separate user control. Changing the content of a given user control will change the content of all the pages on the site. And since the content of a user control can be controlled based on almost any factor in the application, including the ?page parameter, custom content for any particular page can be programmed as well. Content can be varied based on whether or not a user is authenticated or authorized to see certain content.


All in all, a simple yet effective way to manage a site.


Strategy: Improve performance via caching


All developers are concerned about the performance of their applications. Web applications require special scrutiny in this regard because they are so dependent on many external factors – Internet speed, browser capabilities, etc. -- which determine application performance. One of the ways that an ASP.NET developer can improve performance of an application is through caching. Caching is the process of persisting data, objects, and HTML content on the web server or other locations on the way down the pipe to the end user. Caching allows you to avoid having to use processor time to produce content that is unlikely to change from request to request.


And as you might have guessed, the ASP.NET framework provides a mechanism for allowing you to manage your content by caching. It's not the easiest thing to manage and do in the Framework, but the following section will cover the basics and give some examples of caching content to improve your web application's performance. You can provide caching logic for anything, including data, images, and HTML content for your pages and user controls.


The main benefit of caching is to prevent code from being run to render your page every time a request for that page is made. Of course, this may or may not be what you want, so you'll have to determine carefully how each page is to be cached.


There are two main types of caching that you can do in your ASP.NET application: general caching and page caching. General caching involves the use of the Cache object in the .Net framework, and page caching (which actually makes use of the Cache object) allows you to easily cache pages and user controls.


General Caching


There are a number of ways to persist data in an ASP.NET application. You can use the HTTPApplicationState object, the Session object, or even the ViewState. But the most flexible of the persistent storage media is the Cache object. The Cache object not only lets you store information, but allows you to determine how long each item should remain in the cache, and even provides a mechanism for notifying you that a particular item has expired.


The Cache class is found in the System.Web.Caching namespace. Happily, the class is thread-safe, and thus can be used freely in your application. At first glance, the Cache object may seem pretty much the same as the Application object in the way it stores information across the entire application for all users. But the similarities end there. Items stored in the cache are persisted, but they can be “scavenged” at anytime. You can place items in the Cache for a specified length of time; when that time is reached, the item will be removed from the cache. You can place items in the Cache so that they will be removed after a certain interval of inactivity. You can place items in the Cache with varying priorities, so that items of less importance are “scavenged” before other more important items.


Each AppDomain will have it's own instance of a Cache object. Within your code, you can access the Cache object either via HTTPContext.Cache or Page.Cache. Part of the Cache object's functionality is to manage memory and release objects that have expired or that have a low enough priority to warrant them being removed from the cache. The Cache class functions as a Dictionary, enabling easy access to the items in the cache via string indexing. Thus, you can add items as follows:

Cache['SomeObject'] := SomeObject;

and you can retrieve items like so:

SomeString := Cache['AStringValue'];

The first example above is the simplest way to add an item to the Cache. The Cache object provides a number of overloaded Insert methods that let you control exactly how the Cache will manage its items. They are defined as follows:

procedure Cache.Insert(Key: string; 
Value: TObject);
procedure Cache.Insert(Key: string;
Value: TObject;
Dependencies: CacheDependency
);
procedure Cache.Insert(Key: string;
Value: TObject;
Dependencies: CacheDependency;
AbsoluteExpiration: DateTime;
SlidingExpiration: TimeSpan
);
procedure Cache.Insert(key: string;
Value: TObject;
Dependencies: CacheDependency;
AbsoluteExpiration: DateTime;
SlidingExpiration: TimeSpan;
Priority: CacheItemPriority;
OnRemoveCallback: CacheItemRemovedCallback
);

Since they all build on each other, I'll cover the last version, which will obviously deal with all the possibilities.


The following table discusses the parameters of the last version of the Insert method listed above:
































Parameter


Description


Key


The string used to index the item. The string is case-sensitive. An exception is thrown if you pass in a null or empty string. If you pass in a string that already exists in the Dictionary, the item will simply be replaced (Note, that if you pass in an existing string with the Add method, the call will fail)


Value


The item to be stored. Note that this is of type TObject, so you can store pretty much anything you want in the Cache. This parameter cannot be nil.


Dependencies


An object of type CacheDependency. This object tracks a connection between the cached item and an external entity such as a file, a directory, or another object in the Cache. When the connected item is modified, then the cached item is removed from the cache. This parameter can be nil if there are no dependencies.


AbsoluteExpiration


A DateTime object that defines a specific time for the cached item to be removed. You will almost always set this item by adding a certain amount of time to Now. When the given time arrives, the cached item is removed. Set this value to Cache.NoAbsoluteExpiration when you don't want the item to expire. NoAbsoluteExpiration is the default.


SlidingExpiration


This is a TimeSpan object that indicates how long the cached item should remain 'untouched' before being removed. Every time the cached item is accessed, the “clock is reset” on this item. This parameter is mutually exclusive from the AbsoluteExpiration parameter.


Priority


A value from the CacheItemPriority enumeration. Values range from Low to Normal to NotRemovable. When the Cache object decides to scavenge for memory, items with lower priorities will be removed first. The default level is Normal.


OnRemoveCallback


This is reference to a callback function that will be called with the cached item is removed from the cache. The reference should refer to a method of type CacheItemRemoveCallback.





Removing an item manually from the Cache is quite easy. Simply call:

Cache.Remove['SomeItem'];

You can read more about how the CacheItemRemoveCallback works in the MSDN Documentation.


The code that accompanies this paper gives a simple example of how caching in your ASP.NET application works. It demonstrates adding items to the cache, and allows you to determine a time interval for leaving items in the cache. Once items are placed in the cache, you can press the “Refresh” button to see as the cache deletes items once their expiration time arrives.


Page Caching


The other form of caching you can do is page caching. (In fact, Page Caching is really just a specific implementation of the Cache object by the ASP.NET Framework.) You can instruct ASP.NET to hold on to a rendering of a page for any length of time you want. The way to tell ASP.NET to cache your page is via the @OutputCache directive. By placing this directive in your page, you can control how and for how long the page, or even sections of the page are cached. The three most common attributes from the directive are Duration, Location, and VaryByParam. The Duration attribute determines how long in seconds the page is to be cached. It is a required attribute. The Location attribute determines where the page should be cached. You can set this to any of the following values:





























Location Attribute Value


Description


Any


The system can cache your page anywhere it decides to – on the client, on the server, or anywhere in between. This is the default value if the attribute is not specified.


Client


The page will cached on the client.


Downstream


The page will be cached on any HTTP 1.1 – capable device somewhere past the server


None


The page won't be cached.


Server


The page will be cached on the server


ServerAndClient


The page can be cached on either the client or the server but not on any device in between.





VarByParam is used to vary the caching for a page based on either GET or POST parameters for a given request. The values that can be set are listed below. VarByParam is a required attribute, however, it is not required by a user control if that user control's OutputCache directive has the VaryByControl attribute.


The valid values for VarByParam are as follows:




















VarByParam Attribute Value


Description


none


This setting will cause the system to ignore all parameter settings. Set this if you don't want to do any parameter caching. Use this if you want to cache the whole content of the page.


“*”


Set this value if you want to vary the output of the cache by all of the GET and POST parameters


Any valid GET or POST parameter


Set this to a semi-colon list of GET and POST parameters that you want to use to vary the output of the cache.





The code that accompanies this article has an example application that demonstrates some simple caching techniques. The main page demonstrates caching the entire page. It simply places the time on the page, and caches the page for five seconds. If you press the button to update the time, the new time isn't displayed until five seconds has past since the last time the page was updated. This is done with the following @OutputCache directive:


<%@ OutputCache Duration="5" Location="any" VaryByParam="none" %>


The Location attribute is set to “any”, so the system will cache the page anywhere that it likes. The Duration attribute ensures that the page is cached for five seconds before any new values are displayed, and the VaryByParam attribute is set to “none”, meaning that any GET or POST parameters won't play a role in the page caching process.


The code for the Button's Click event looks like this:

procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs);
begin
Label1.Text := System.String.Format('The time is now: {0}', [System.DateTime.Now.ToLongTimeString]);
end;

Caching with Parameters


The VarByParam page in the demo application shows how you can cache different versions of a given page based on a simple query parameter. The @OutputCache directive for the page looks like this:

<%@ OutputCache  Duration="120" Location="Any" VaryByParam="*" %>

The Duration attribute is set long enough so that the cache doesn't update while we are testing this out. (Hopefully this simple demo doesn't take more than two minutes to look at.....) The VaryByParam attribute is set to “*”, which means that all the parameters from the QueryString will determine a cached version of the page.


The Page_Load method's code looks like this:

procedure TWebForm2.Page_Load(sender: System.Object; e: System.EventArgs);
begin
Label1.Text := System.String.Format('{0}, your random number is : {1}',
[Request.Params['FirstName'], Random(100)]);
end;

This code will create a random number for each user of the page. So now, if you pass in two different URLs, such as

http://localhost:8080/CachingDemo/VaryByParam.aspx?FirstName=Frank
http://localhost:8080/CachingDemo/VaryByParam.aspx?FirstName=Nick

you'll notice that each page has a different value for the random number, and that random number will be remembered for two minutes.


Now here's the fun part, if you put a breakpoint on the line of code listed above, run the application, and press the refresh button, you'll notice that the debugger only stops on the code once, and the page itself is returned from the cache without the code executing. Cool, huh?


You might be thinking at this point: so what? Well, you can now provide a cached page for each user that comes to your site if you put a unique parameter in the query string of the URL. Or you can cache the content of a given page if that content is determined by what is in the query string.


Conclusion


Well, there you have it – a collection of “bigger picture” strategies for improving the power and maintainability of your web pages. You should now be able to handle users, errors, and application settings. Your applications should be able to easily maintain a consistent look and feel via a master page strategy. And you should be able to use caching to improve the overall performance of your web application. ASP.NET is a powerful framework for building web applications, and it is only going to get more powerful with the release of ASP.NET 2.0.



Published on: 10/11/2004 2:18:17 PM


0 Comments: