Chapter 7. Working with the Page

“Divide and rule, a sound motto. Unite and lead, a better one.”

Wolfgang Goethe

Authoring an ASP.NET page is not simply a matter of putting together a well-organized hierarchy of server controls, literals, and JavaScript script blocks. That’s definitely a fundamental step, but it’s only the first step. First and foremost, a Web page is part of the presentation layer of a Web application. This means that the page is responsible for coordinating some user interface tasks aimed at providing end users with key information regarding bad requests and run-time anomalies, localized messages, and preferences.

Momentarily leaving aside any discussion on possible best practices for layering an ASP.NET Web Forms application, let’s examine some aspects related to ancillary page development tasks. Tasks covered in this chapter relate to error handling, error pages, tracing, localization, and personalization, as well as effective techniques to add script files and style the content of ages.

I’ll return to layers and design principles in Chapter 13.

Dealing with Errors in ASP.NET Pages

Any ASP.NET application can incur various types of errors. There are configuration errors caused by some invalid syntax or structure in one of the application’s web.config files and parser errors that occur when the syntax on a page is malformed. In addition, you can run into run-time errors that show up during the page’s execution. Finally, there are errors detected by the ASP.NET runtime infrastructure that have to do with bad requests or incorrect parameters.

Parser errors (both in configuration and markup) show up as soon as you start a debugging session, and their fix is immediate and part of the development process. What about other types of errors?

To prevent critical parts of your code from throwing exceptions at run time, you can resort to plain exception-handling practices as recommended by the Microsoft .NET Framework guidelines. To trap errors resulting from bad requests, invalid routing, or HTTP failures, you can take advantage of some of ASP.NET-specific facilities for page error handling.

Let’s attack the topic with a quick overview of exception handling as it happens in .NET.

Basics of Exception Handling

Just like other .NET applications, ASP.NET applications can take advantage of common language runtime (CLR) exceptions to catch and handle run-time errors that occur in the code. As a reminder, it’s worth mentioning here that in .NET development CLR exceptions are the recommended way of handling errors—they are the rule, not the exception!

Exceptions, though, should be taken just for what the name suggests—that is, events in the life of the application raised when something happens that violates an assumption.

Exceptions should not be used to control the normal flow of the program. If there is a way to detect possible inconsistent situations, by all means use that other method (mostly, conditional statements), and use exceptions as the last resort. The latest version of Microsoft Visual Studio 2010 (as well as many commercial products that assist you in development, such as JetBrains ReSharper and Telerik JustCode, to name a couple) offers coding tips and reminds you to check for possible null reference exceptions. That’s a huge help, isn’t it?

Although exceptions are the official tool to handle errors in .NET applications, they’re not free and should not be overused. Running any piece of code in a try/catch block will cost you at least a little in terms of performance. Protection against possible run-time failure is a sort of insurance, and you have to pay for that no matter what happens.

Exceptions in Action

To execute a piece of code with the certainty that any (or just some) exceptions it might raise will be caught, you use the following code:

try
{
    // Your regular code here
    ...
}
catch
{
    // Your recovery code for all exceptions
    ...
}

The sample code snippet can have a number of variations and extensions. You can add a finally block, which will finalize the operation and run regardless of whether the execution flow went through the try or the catch block. The snippet shown will catch any exceptions. Because of its extreme generality, you might need to lose some valuable information about what has happened. A better approach consists of listing one or more catch blocks, each trying to cache a specific exception:

try
{
    // Your regular code here
    ...
}
catch(NullReferenceException nullReferenceException)
{
    // Your recovery code for the exception
    ...
}
catch(ArgumentException argumentException)
{
    // Your recovery code for the exception
    ...
}
finally
{
    // Finalize here but DON'T throw exceptions from here
    ...
}

Exceptions will be listed from the most specific to the least specific. From a catch block, you are allowed to swallow the exception so that other topmost modules will never know about it. Alternatively, you can handle the situation gracefully and recover. Finally, you can do some work and then re-throw the same exception or arrange a new one with some extra or modified information in it.

The catch block is fairly expensive if your code gets into it. Therefore, you should use the catch block judiciously—only when really needed and without overcatching.

Guidelines for Exception Handling

When writing a module (including ASP.NET pages), you should never throw an exception as an instance of the System.Exception class. It is strictly recommended that you try to use built-in exception types such as InvalidOperationException, NullReferenceException, and ArgumentNullException whenever these types apply. You should resist the temptation of having your very own exceptions all the way through, although for program errors you should consider defining your own exceptions.

In general, you should be very specific with exceptions. ArgumentNullException is more specific than ArgumentException. An exception comes with a message, and the message must be targeted to developers and, ideally, localized.

Swallowing an exception is possible and supported, but you should consider that in this case some modules might never know what went wrong. This approach might not be acceptable in some cases, so use it with extreme care. In general, don’t be afraid to let exceptions propagate up the call stack.

When using exceptions, pay a lot of attention to cleanup code. The finally block serves exactly the purpose of ensuring that any cleanup code is always executed. Alternatively, when the cleanup code sees an object that implements IDisposable, you can resort to the using statement:

using(var someObject = new SomeDisposableObject())
{
   // Code at risk of exceptions
   ...
}

If placed in a finally block, the cleanup code is always executed. This is an important guarantee because if an unexpected exception is thrown, you might lose your cleanup code.

Finally, here are a few recommendations for situation in which you get to write your own exception classes. For a long time, Microsoft said you should derive your exception classes from System.ApplicationException. More recently, there’s been a complete turnaround on this point: the new directive says the opposite. You should ignore ApplicationException and derive your exception classes from Exception or other more specific built-in classes. And don’t forget to make your exception classes serializable.

Basics of Page Error Handling

When an exception occurs in an ASP.NET application, the CLR tries to find a block of code willing to catch it. Exceptions walk their way up the stack until the root of the current application is reached. If no proper handler shows up along the way, the exception gains the rank of unhandled exception and causes the CLR to throw a system-level exception.

At this point, ASP.NET users are shown a standard error page that some developers familiarly call the YSOD (yellow screen of death), which is a spinoff of the just as illustrious BSOD (blue screen of death) that we all have come to know after years of experience with the Microsoft Windows operating system. An unhandled exception originates an error and stops the application.

As a developer, how should you deal with unhandled exceptions in ASP.NET applications?

Default Error Pages

When an unrecoverable error occurs in an ASP.NET page, users always receive a page that, more or less nicely, informs them that something went wrong at a certain point. ASP.NET catches any unhandled exception and transforms it into a page for the user, as shown in Figure 7-1.

The error page generated by an unhandled exception (for the local user).

Figure 7-1. The error page generated by an unhandled exception (for the local user).

As you can guess from looking at the screen shot, the sample page contains a button whose click handler is bound to the following code:

protected void Button1_Click(Object sender, EventArgs e)
{
   throw new NotImplementedException();
}

More than the code itself, which is fairly trivial, the most interesting part of the story is how ASP.NET handles the exception and the machinery that ultimately produces the markup of Figure 7-1.

First and foremost, the typical error page differs for local and remote users.

By default, local users—namely, any user accessing the application through the local host—receive the page shown in Figure 7-1. The page includes the call stack—the chain of method calls leading up to the exception—and a brief description of the error. Additional source code information is added if the page runs in debug mode. For security reasons, remote users receive a less detailed page, like the one shown in Figure 7-2.

The page does not provide information about the error.

Figure 7-2. The page does not provide information about the error.

ASP.NET provides a couple of global interception points for you to handle errors programmatically, at either the page level or the application level. The Page base class exposes an Error event, which you can override in your pages to catch any unhandled exceptions raised during the execution of the page. Likewise, an Error event exists on the HttpApplication class, too, to catch any unhandled exception thrown within the application.

Page-Level Error Handling

To catch any unhandled exceptions wandering around a particular page, you define a handler for the Error event. Here’s an example:

protected void Page_Error(Object sender, EventArgs e)
{
    // Capture the error
    var exception = Server.GetLastError();

    // Resolve the error page based on the exception that occurred
    // and redirect to the appropriate page
    if (exception is NotImplementedException)
        Server.Transfer("/ErrorPages/NotImplErrorPage.aspx");
    else
        Server.Transfer("/ErrorPages/GenericErrorPage.aspx");

    // Clear the error
    Server.ClearError();
}

You know about the raised exception through the GetLastError method of the Server object. In the Error handler, you can transfer control to a particular page and show a personalized and exception-specific message to the user. The control is transferred to the error page, and the URL in the address bar of the browser doesn’t change. If you use Server.Transfer to pass control, the exception information is maintained and the error page itself can call into GetLastError and display more detailed information. Finally, after the exception is fully handled, you clear the error by calling ClearError.

Using Server.Transfer instead of Response.Redirect is also relevant from a Search-Engine Optimization (SEO) perspective because it performs a server-side redirect that is “invisible” to client applications, including Web browsers and, more importantly, Web spiders.

Important

When displaying error messages, pay attention not to hand out sensitive information that a malicious user might use against your system. Sensitive data includes user names, file system paths, connection strings, and password-related information. You can make error pages smart enough to determine whether the user is local or whether a custom header is defined, and to display more details that can be helpful to diagnose errors:

if (Request.UserHostAddress == "127.0.0.1") {
   ...
}

You can also use the Request.Headers collection to check for custom headers added only by a particular Web server machine. To add a custom header, you open the Properties dialog box of the application’s Internet Information Services (IIS) virtual folder and click the HTTP Headers tab.

Global Error Handling

A page Error handler catches only errors that occur within a particular page. This means that each page that requires error handling must point to a common piece of code or define its own handler. Such a fine-grained approach is not desirable when you want to share the same generic error handler for all the pages that make up the application. In this case, you can create a global error handler at the application level that catches all unhandled exceptions and routes them to the specified error page.

The implementation is nearly identical to page-level error handlers except that you will be handling the Error event on the HttpApplication object that represents your application. To do that, you write code in the predefined Application_Error stub of the application’s global.asax file:

void Application_Error(Object sender, EventArgs e)
{
    ...
}

You could do something useful in this event handler, such as sending an e-mail to the site administrator or writing to the Windows event log to say that the page failed to execute properly. ASP.NET provides a set of classes in the System.Net.Mail namespace for just this purpose.

void Application_Error(Object sender, EventArgs e)
{
    // Code that runs when an unhandled error occurs
    var exception = Server.GetLastError();
    if (exception == null)
       return;

    var mail = new MailMessage { From = new MailAddress("[email protected]") };
    mail.To.Add(new MailAddress("[email protected]"));
    mail.Subject = "Site Error at " + DateTime.Now;
    mail.Body = "Error Description: " + exception.Message;
    var server = new SmtpClient {Host = "your.smtp.server"};
    server.Send(mail);

    // Clear the error
    Server.ClearError();
}

If the SMTP server requires authentication, you need to provide your credentials through the Credentials property of the SmtpClient class. Figure 7-3 shows the e-mail message being sent.

The e-mail message being sent when an error is handled globally.

Figure 7-3. The e-mail message being sent when an error is handled globally.

As Figure 7-3 shows, the exception reported mentions a generic HTTP unhandled exception. Note that GetLastError returns the real exception in the context of Page_Error, but not later in the context of Application_Error. In the application context, the exception caught is a generic HTTP exception that wraps the original exception internally. To retrieve the real exception, you must go through the InnerException property, as shown here:

void Application_Error(Object sender, EventArgs e)
{
    // This is a generic HTTP failure exception
    var exception = Server.GetLastError();
    if (exception == null)
       return;

    // Put your hands on the original exception
    var originalException = exception.InnerException;
    ...
}

Essentially, when ASP.NET detects an internal application error—like it is an exception being thrown by one of the pages—it configures itself for an HTTP 500 response. The ASP.NET error-handling mechanism captures HTTP 500 errors but not other HTTP errors, such as 404. Errors other than HTTP 500 are handled by the Web server, and all that you can do is configure the ASP.NET error-handling machinery (and to some extent the routing mechanism too) to redirect automatically where you like. No full control over 404 and other HTTP errors is possible in ASP.NET Web Forms.

Note

What takes precedence if you have an application-level error handler and a page-level handler? The page handler runs first, followed by the application handler. For this reason, if you have both handlers, you should avoid calling Server.ClearError in the page handler so that you do not compromise any of the following steps.

Logging Exceptions

In addition or in alternative to sending an e-mail message, you can decide to write an entry to the Windows event log when an exception is caught. Here’s the code:

void Application_Error(Object sender, EventArgs e)
{
    // Obtain the URL of the request
    var url = Request.Path;

    // Obtain the Exception object describing the error
    var exception = Server.GetLastError();

    // Build the message --> [Error occurred. XXX at url]
    var text = new StringBuilder("Error occurred. ");
    text.Append(error.Message);
    text.Append(" at ");
    text.Append(url);

    // Write to the Event Log
    var log = new EventLog();
    log.Source = "Your Log";
    log.WriteEntry(text.ToString(), EventLogEntryType.Error);
}

The Event Log Source must exist prior to its use in an ASP.NET application—in this case, in the Application_Error method in global.asax. Typical ASP.NET account credentials are established such that the ASP.NET account does not have Event Log source creation rights. You’ll need to make sure the log is created first on each Web server your code will execute within prior to actually running your Web application.

Robust Error Handling

A good strategy for robust and effective ASP.NET error handling is based on the following three guidelines:

  • Anticipate problems by wrapping all blocks of code that might fail in try/catch/finally blocks. This alone doesn’t guarantee that no exceptions will ever show up, but at least you’ll correctly handle the most common ones.

  • Don’t leave any exceptions unhandled. By following this guideline, even if you did not anticipate a problem, at least users won’t see an exception page. You can do this both at the page and application levels. Needless to say, an application-level error handler takes precedence over page-level handlers. At the least, exceptions that are handled at the application level should be logged to feed reports and help the team to understand what went wrong and whether some bugs exist that need to be fixed.

  • Make sure that error pages don’t give away any sensitive information. If necessary, distinguish between local and remote users and show detailed messages only to the former. A local user is defined as the user that accesses the application from the Web server machine.

Outlined in this way, error handling is mostly a matter of writing the right code in the right place. However, ASP.NET provides developers with a built-in mechanism to automatically redirect users to error-specific pages. This mechanism is entirely declarative and can be controlled through the web.config file.

Mapping Errors to Pages

ASP.NET developers can also benefit from a declarative API to gain some control over the page being served to users after an unhandled exception. Such a declarative API relies on the information stored in the <customErrors> section of the application’s web.config file.

The <customErrors> Section

You turn on custom error messages for an ASP.NET application by acting on the <customErrors> section. Here’s an example:

<configuration>
    <system.web>
        ...
        <customErrors mode="RemoteOnly" />
    </system.web>
</configuration>

The mode attribute specifies whether custom error pages are enabled, disabled, or shown only to remote clients. The attribute is required. When the mode attribute is set to RemoteOnly (the default setting), remote users receive a generic error page that informs them that something went wrong on the server. (See Figure 7-2.) Local users, on the other hand, receive pages that show lots of details about the ASP.NET error. (See Figure 7-1.)

The error-handling policy can be changed at will. In particular, ASP.NET can be instructed to display detailed pages to both local and remote users. To activate this functionality, you change the value of the mode attribute to Off. For obvious security reasons, Off should not be used in production environments—it might reveal critical information to potential attackers.

Using Custom Error Pages

Overall, whatever your choice is for the mode attribute, all users have a good chance to be served a rather inexpressive and uninformative error page. To display a more professional, friendly, and apologetic page that has a look and feel consistent with the site, you set web.config as follows. Figure 7-4 gives an idea of the results you can get.

<configuration>
    <system.web>
        <customErrors mode="On"
            defaultRedirect="/GenericErrorPage.aspx" />
    </system.web>
</configuration>
A more friendly error page.

Figure 7-4. A more friendly error page.

Whatever the error is, ASP.NET now redirects the user to the GenericErrorPage.aspx page, whose contents and layout are completely under your control. This look is obtained by adding an optional attribute such as defaultRedirect, which indicates the error page to use to notify users. If mode is set to On, the default redirect takes on the standard error pages for all local and remote users. If mode is set to RemoteOnly, remote users will receive the custom error page while local users (typically, the developers) still receive the default page with the ASP.NET error information.

In most cases, the custom error page is made of plain HTML so that no error can recursively be raised. However, should the error page, in turn, originate another error, the default generic page of ASP.NET will be shown.

Note

When a default redirect is used, the browser receives an HTTP 302 status code and is invited to issue a new request to the specified error page. This fact has a key consequence: any information about the original exception is lost and GetLastError, which is called from within the custom error page, returns null.

Handling Common HTTP Errors

A generic error page invoked for each unhandled exception can hardly be context-sensitive—especially if you consider that there’s no immediate way for the page author to access the original exception. We’ll return to this point in a moment.

In addition to redirecting users to a common page for all errors, ASP.NET enables you to customize pages to show when certain HTTP errors occur. The mapping between error pages and specific HTTP status codes is defined in the web.config file. The <customErrors> section supports an inner <error> tag, which you can use to associate HTTP status codes with custom error pages.

<configuration>
  <system.web>
    <customErrors mode="On" defaultRedirect="/GenericErrorPage.aspx">
        <error statusCode="404" redirect="/ErrorPages/Error404.aspx" />
        <error statusCode="401" redirect="/ErrorPages/Error401.aspx" />
        ...
    </customErrors>
  </system.web>
</configuration>

The <error> element indicates the page to redirect the user to when the specified HTTP error occurs. The attribute statusCode denotes the HTTP error. Figure 7-5 shows what happens when the user mistypes the name of the URL and the error HTTP 404 (resource not found) is generated.

A custom page for the popular HTTP 404 error.

Figure 7-5. A custom page for the popular HTTP 404 error.

When invoked by the ASP.NET infrastructure, pages are passed the URL that caused the error on the query string. The following code shows the code-behind of a sample HTTP 404 error page:

public partial class Error404 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var errPath = "<i>No error path information is available.</i>";
        var o = Request.QueryString["AspxErrorPath"];
        if (o != null)
            errPath = o;

        // Update the UI
        ErrorPath.InnerHtml = errPath;
    }
}

If you have custom error handling and a global application handler in place, you should not clear server errors. The sequence in which handlers are invoked is this: page, application, ASP.NET runtime with configured redirects.

Important

In light of some security vulnerabilities discovered recently, returning a different output for different HTTP errors might help attackers to find out valuable information about the system. For this reason, it is recommended that you set a default redirect page and avoid adding error-specific pages.

Getting Information About the Exception

As mentioned, when you configure ASP.NET to redirect to a particular set of error pages, you lose any information about the internal exception that might have caused the error. Needless to say, no internal exception is involved in an HTTP 404 or HTTP 302 error. Unhandled exceptions are the typical cause of HTTP 500 internal errors. How do you make the page show context-sensitive information, at least to local users?

You get access to the exception in the Error event both at the page and application levels. One thing you can do is this: write a page-level error handler, capture the exception, and store the exception (or only the properties you’re interested in) to the session state. The default redirect will then retrieve any context information from the session state.

protected void Page_Error(object sender, EventArgs e)
{
    // Captures the error and stores exception data
    var exception = Server.GetLastError();

    // Distinguish local and remote users
    if (Request.UserHostAddress == "127.0.0.1")
        Session["LastErrorMessage"] = exception.Message;
    else
        Session["LastErrorMessage"] = "Internal error.";

    // Clear the error (if required)
    Server.ClearError();
}

The preceding code checks the host address and stores exception-related information (limited to the message for simplicity) only for local users. The following code should be added to the Page_Load method of the page that handles the HTTP 500 error:

var msg = "No additional information available.";
var extraInfo = Session["LastErrorMessage"];
if (extraInfo != null)
    msg = (string) extraInfo;
Session["LastErrorMessage"] = null;

// Update the UI here
ExtraInfo.InnerHtml = msg;
...

Writing context-sensitive error pages requires a page-level Error handler to cache the original exception. This means that you should write the same handler for every page that requires context-sensitive errors. You can either resort to a global error handler or write a new Page-derived class that incorporates the default Error handler. All the pages that require that functionality will derive their code file from this class instead of Page.

Error Reporting

Let’s put it down this way: fatal exceptions in software applications just happen. What do you do when such exceptions happen? Having some good exception-handling code is essential, but how would you collect any information related to the exception to study the case thoroughly?

Trapping and recovering from exceptions is only the first step, and it is largely insufficient in most cases. You need to figure out the section of the site that the user was visiting. You need to grab state information and the values currently stored in critical variables. Furthermore, you need to measure the frequency of the error to arrange a plan for bug fixing and maintenance. In a way, error reporting is the dark side of exception handling.

Features of an Error Reporting System

An effective error reporting system grabs error information and offers to report that in a variety of ways and stores. As you’ve seen, exceptions handled at the application level (that would otherwise go unhandled) should be logged and administrators should be notified.

What kind of information should be added to the log? At a minimum, the list includes values of local variables, the current call stack, and perhaps a screen shot of the failure. Is it sufficient to notify the webmaster of the failure? Although a notification is not a bad thing, an effective error reporting system reports exceptions to a centralized repository that is remotely accessible and groups them in some way—for example, by type.

Error Reporting Tools

Is such an error reporting system something you build from scratch once and adapt to any applications you write? Or is it an external framework you just plug into your solution?

In ASP.NET, there’s just one way to capture fatal exceptions—writing a handler for the Application_Error event. This can be done in two ways, however.

You can write code directly in the application’s global.asax file, or you can plug a made-to-measure HTTP module into the web.config file. The HTTP module would register its own handler for the Error application event. The two solutions are functionally equivalent, but the one based on the HTTP module can be enabled, disabled, and modified without recompiling the application. It is, in a way, less obtrusive. In the handler, you can log the exception the way you want—for example, by writing to the system’s Event Log or by adding a record to some database. Information stored in a database requires that you have some infrastructure on your end, but it provides great flexibility because the content can be extracted and manipulated to create reports and statistics. Obviously, processing the uploaded logs is up to you.

A tool that is popular among ASP.NET developers is Error Logging Modules And Handlers (ELMAH). ELMAH is essentially made of an HTTP module that, once configured, intercepts the Error event at the application level and logs it according to the configuration to a number of back-end repositories. ELMAH comes out of an open-source project (http://code.google.com/p/elmah) and includes a number of extensions, mostly in the area of repositories. ELMAH offers some nice facilities, such as a Web page to view all recorded exceptions and drill down into each of them. Any error reporting system specifically designed for ASP.NET can’t be, architecturally speaking, much different from ELMAH.

Note

You might want to take a look at some commercial products that offer a reporting mechanism for ASP.NET applications. One of these products is Red Gate’s SmartAssembly (http://www.red-gate.com/products/smartassembly/error_reporting.htm). Although it’s not specifically designed for ASP.NET, the tool can be easily adapted to add reporting capabilities to ASP.NET applications. Essentially, it takes an existing assembly and parses its compiled code adding try/catch blocks that log any possible exceptions and upload the complete information to a given Web site. The tool also has a desktop front end to help you navigate through logged exceptions.

Self-Logging Exceptions

Another handmade solution consists of employing custom exception classes that derive from a user-defined class endowed with the ability to log automatically. In this way, at the cost of using custom exceptions everywhere, you can log any exceptions you’re interested in regardless of whether the exception is fatal or not.

Page Personalization

ASP.NET pages do not necessarily require a rich set of personalization features. However, if you can build an effective personalization layer into your Web application, final pages will be friendlier, more functional, and more appealing to use. For some applications (such as portals and shopping centers), though, personalization is crucial. For others, it is mostly a way to improve visual appearance. In ASP.NET, personalization is offered through the user profile API.

ASP.NET personalization is designed for persistent storage of structured data using a friendly and type-safe API. Loading and saving personalized data is completely transparent to end users and doesn’t even require the page author to know much about the internal plumbing.

Creating the User Profile

A user profile is a plain .NET class that exposes a bunch of properties. The class can be defined in two possible ways depending on the model of Web application you are building within Visual Studio.

If your project is a Web site project, you define the user profile model declaratively through attributes in the web.config file. At run time, the ASP.NET build machinery will group these properties into a dynamically generated class. When the application runs and a page is displayed, ASP.NET dynamically creates a profile object that contains, properly typed, the properties you have defined in the data model. The object is then added to the current HttpContext object and is available to pages through the Profile property.

For a Web Application Project (WAP), instead, a bit more work is required on your part, and type-safety comes at the cost of writing the user profile class manually. You don’t use the Profile property directly from the HttpContext object but, at the end of the day, the work being done underneath is not different. The only difference is in who actually writes the code—you in a WAP scenario, or the ASP.NET runtime in a Web site project.

Any profile data is persisted on a per-user basis and is permanently stored until someone with administrative privileges deletes it. The data storage is far away from the user and, to some extent, also hidden from the programmers. The user doesn’t need to know how and where the data is stored; the programmer simply needs to indicate what type of profile provider she wants to use. The profile provider determines the database to use—typically, a Microsoft SQL Server database, but custom providers and custom data storage models can also be used.

Note

In ASP.NET, the default profile provider is based on SQL Express, a lightweight version of SQL Server. The default physical storage medium is a local file named aspnetdb.mdf, which is commonly located in the App_Data folder of the Web application. You can rename and move the file as you wish. If you change its schema, though, you have to employ an ad hoc provider that understands the new schema. Because it is an MDF file, you can also host the database in a full edition of SQL Server on the host machine.

Definition of the Data Model in a Web Site Project

Let’s begin our exploration of the profile API focusing on the tasks required in a Web site project. The profile API was originally introduced in ASP.NET 2.0 along with the Web site model at a time in which the popularity of the WAP model was in a downturn and everybody seemed to want to get rid of it. That sentiment was only a flash in the pan, however. The WAP model soon regained its prominent position in the minds of developers, and today Visual Studio 2010 offers two models to choose from. The choice is not painless when it comes to the profile API. I’ll present the profile API from the perspective of a Web site application first—because it’s likely you might have heard of it already. Next, I’ll point out differences related to WAP projects.

To use the ASP.NET profile API, you first decide on the structure of the data model you want to use. Then you attach the data model to the page through the configuration file. The layout of the user profile is defined in the web.config file and consists of a list of properties that can take any of the .NET CLR types. The data model is a block of XML data that describes properties and related .NET Framework types.

The simplest way to add properties to the profile storage medium is through name/value pairs. You define each pair by adding a new property tag to the <properties> section of the configuration file. The <properties> section is itself part of the larger <profile> section, which also includes provider information. The <profile> section is located under <system.web>. Here’s an example of a user profile section:

<profile>
  <properties>
    <add name="UseEuroMetricSystem" type="Boolean" />
    <add name="TemperatureSystem" type="String" />
  </properties>
  ...

</properties>

All the properties defined through an <add> tag become members of the dynamically created class and are then exposed as part of the HTTP context of each page. The type attribute indicates the type of the property. If no type information is set, the type defaults to System.String. Any valid CLR type is acceptable.

So in the preceding code snippet, we’re defining a profile class made of two properties. The profile pseudoclass we have in mind looks like the one shown here:

class PseudoProfile
{
   public Boolean UseEuroMetricSystem {get; set;}
   public String TemperatureSystem {get; set;}
}

Table 7-1 lists the valid attributes for the <add> element. Only name is mandatory.

Table 7-1. Attributes of the <add> Element

Attribute

Description

allowAnonymous

Allows storing values for anonymous users. It is false by default.

customProviderData

Contains specific data to feed a custom profile provider, if any.

defaultValue

Indicates the default value of the property.

name

Name of the property.

provider

Name of the provider to use to read and write the property.

readOnly

Specifies whether the property value is read-only. It is false by default.

serializeAs

Indicates how to serialize the value of the property. Possible values are Xml, Binary, String, and ProviderSpecific.

type

The .NET Framework type of the property. It is a string object by default.

The User Profile Class Representation

There’s no class like PseudoProfile anywhere in the application’s AppDomain; yet the declared data model is dynamically compiled to a class for strongly typed programmatic access. The following code snippet gives you a much clearer idea of the class being generated by ASP.NET out of the profile’s data model:

using System;
using System.Web;
using System.Web.Profile;

public class ProfileCommon : System.Web.Profile.ProfileBase
{
    public virtual bool UseEuroMetricSystem {
        get {
            return ((bool)(this.GetPropertyValue("UseEuroMetricSystem")));
        }
        set {
            this.SetPropertyValue("UseEuroMetricSystem", value);
        }
    }

    public virtual string TempSystem {
        get {
            return ((string)(this.GetPropertyValue("TempSystem")));
        }
        set {
            this.SetPropertyValue("TempSystem", value);
        }
    }

    public virtual ProfileCommon GetProfile(string username) {
        return ((ProfileCommon)(ProfileBase.Create(username)));
    }
}

This code is an excerpt from the real source code created by ASP.NET while compiling the content of the web.config file’s <profile> section.

An instance of this class is associated with the Profile property of the HTTP context class and is accessed programmatically as follows:

// Use the UseEuroMetricSystem property to determine how to render the page
if (HttpContext.Profile.UseEuroMetricSystem)
{
    ...
}

There’s a tight relationship between user accounts and profile information. We’ll investigate this in a moment—for now, you need to take note of this because anonymous users are supported as well.

Note

You can retrieve the hidden source code of the profile class (and other internal files) in the Temporary ASP.NET Files folder. The profile class in particular is located in a file named according to the pattern App_Code.xxx.N.cs, where xxx is a system-generated hash code and N is a 0-based index. Note that the path of the Temporary ASP.NET Files folder is different if you’re using IIS or the embedded Visual Studio Web server. If you’re using IIS, the path is

%Windows%Microsoft.NETFrameworkv4.0.30319Temporary ASP.NET Files

Otherwise, the path is

C:Users...AppDataLocalTempTemporary ASP.NET Files

You can programmatically find out the real path being used by reading the value of the following expression:

HttpRuntime.CodegenDir

You can do that by placing a breakpoint somewhere in the page startup code and evaluating the expression in a Visual Studio QuickWatch window.

Using Collection Types

In the previous example, we worked with single, scalar values. However, the personalization engine fully supports more advanced scenarios, such as using collections or custom types. Let’s tackle collections first. The following code demonstrates a Locations property that is a collection of strings:

<properties>
    <add name="Locations"
        type="System.Collections.Specialized.StringCollection" />
</properties>

Nonscalar values such as collections and arrays must be serialized to fit in a data storage medium. The serializeAs attribute simply specifies how. As mentioned, acceptable values are String, Xml, Binary, and ProviderSpecific. If the serializeAs attribute is not present in the <properties> definition, the String type is assumed. A collection is normally serialized as XML or in a binary format.

Using Custom Types

You can use a custom type with the ASP.NET personalization layer as long as you mark it as a serializable type. You simply author a class and compile it down to an assembly. The name of the assembly is added to the type information for the profile property:

<properties>
    <add name="ShoppingCart"
        type="My.Namespace.DataContainer, MyAssembly"
        serializeAs="Binary" />
</properties>

The assembly that contains the custom type must be available to the ASP.NET application. You obtain this custom type by placing the assembly in the application’s Bin directory or by registering it within the global assembly cache (GAC).

Grouping Properties

The <properties> section can also accept the <group> element. The <group> element allows you to group a few related properties as if they are properties of an intermediate object. The following code snippet shows an example of grouping:

<properties>
    ...
    <group name="Metrics">
        <add name="Speed" type="string" defaultValue="mph" />
        <add name="Temperature" type="string" defaultValue="F" />
    </group>
</properties>

Two properties have been declared children of the Metrics group. This means that from now on any access to Speed or Temperature passes through the Metrics name, as shown here:

var windSpeedDisplayText = String.Format("{0} {1}",
         windSpeed, Profile.Metrics.Speed);

The System.Web.UI.Page class doesn’t feature any Profile property. However, in a Web site project, the build machinery of ASP.NET generates an extra partial class where the Profile property is defined to just return HttpContext.Current.Profile.

Note

Default values are not saved to the persistence layer. Properties declared with a default value make their debut in the storage medium only when the application assigns them a value different from the default one.

Definition of the Data Model in a WAP Project

In a WAP project, you can choose between a weakly typed and strongly typed approach. The simplest approach (but most effective as well?) is the weak typing approach. In this case, you do exactly the same as you would do in a Web site project. The only difference is that you have no Profile property on the Page class and no dynamically built profile class.

As you saw earlier, however, a profile class is not a plain old CLR class—it is expected, instead, to inherit from System.Web.Profile.ProfileBase. The parent class features two generic methods to read and write properties: GetPropertyValue and SetPropertyValue. This is the real code that ultimately retrieves and stores the values from and to storage. The following code works like a champ in a WAP project:

HttpContext.Current.Profile.GetPropertyValue("UseEuroMetricSystem");

The drawback is that GetPropertyValue is designed to return an Object type. To get a Boolean or a String, you need to cast. The autogenerated profile class you would get in a Web site project just saves you from manually writing a few cast instructions. Here are the steps to take to define a strongly typed profile data model in a WAP project.

The idea is that you define your own strongly typed class and then attach its reference to the <profile> section of the web.config file. In this way, the profile built-in machinery will still be able to do its load-and-save work into the underlying base profile class—the ProfileBase class—and your wrapper will deliver you the pleasure of strongly typed programming.

<profile inherits="YourApp.UserProfile">
   ...
</profile>

Here’s a possible implementation for the handmade YourApp.UserProfile wrapper class:

namespace YourApp {

public class UserProfile : ProfileBase
{
    public static UserProfile GetUserProfile()
    {
        var user = Membership.GetUser();

        // Anonymous user?
        if (user == null)
            return GetUserProfile("");   // throw if anonymous access is not permitted
        return GetUserProfile(user.UserName);
    }

    public static UserProfile GetUserProfile(String username)
    {
        var profileFromStorage = Create(username);
        return profileFromStorage as UserProfile;
    }

    [SettingsAllowAnonymous(true)]
    public Boolean UseEuroMetricSystem
    {
        get { return (Boolean)
                HttpContext.Current.Profile.GetPropertyValue("UseEuroMetricSystem"); }
        set { HttpContext.Current.Profile.SetPropertyValue("UseEuroMetricSystem", value); }
    }

    [SettingsAllowAnonymous(true)]
    public String TempSystem
    {
        get
        {
            var current = (String)
                HttpContext.Current.Profile.GetPropertyValue("TempSystem");
            if (String.IsNullOrEmpty(current))
                return "F";
            return (String) current;
        }
        set { HttpContext.Current.Profile.SetPropertyValue("TempSystem", value); }
    }
}
}

The UserProfile class you see is configured to support both authenticated and anonymous access. If you want to enable it only for authenticated users, throw an exception if no user is found and remove the turn to make the argument of the SettingsAllowAnonymous attribute false. (Or remove the attribute altogether.)

To access properties from within the code, you proceed as follows:

var profile = Your.UserProfile.GetUserProfile();
if (profile.UseEuroMetricSystem)
    speedFormat = "{0} kmh";

You invoke the static GetUserProfile method on your wrapper class and get an instance of your own profile class fed by the underlying ASP.NET profile API. The Create method that GetUserProfile uses internally is part of the profile API, and specifically it is the part that communicates with the storage layer.

Interacting with the Page

To enable or disable profile support, you set the enabled attribute of the <profile> element in the web.config file. If the property is true (the default), personalization features are enabled for all pages. If personalization is disabled, the Profile property on the HTTP context object isn’t available to pages.

Creating the Profile Database

As mentioned earlier, profile support works strictly on a per-user basis and is permanently stored in a configured repository. Enabling the feature simply turns any functionality on, but it doesn’t create the needed infrastructure for user membership and data storage. If you intend to use made-to-measure storage (for example, a non–SQL Server database or a SQL Server database with a custom schema of tables), creating any infrastructure is entirely up to you. If you’re OK with the default table and structure, you resort to a free tool integrated in Visual Studio.

ASP.NET 4 (as well as earlier versions) comes with an administrative tool—the ASP.NET Web Site Administration Tool (WSAT)—that is fully integrated in Visual Studio. You invoke the tool by choosing the ASP.NET Configuration item from the Build menu. (See Figure 7-6.)

The ASP.NET Web Site Administration Tool.

Figure 7-6. The ASP.NET Web Site Administration Tool.

You can use this tool to create a default database to store profile data. The default database is a SQL Server file named aspnetdb.mdf, which is located in the App_Data special folder of the ASP.NET application. A proper connection string is added to the configuration file to be consumed by various ASP.NET provider-based frameworks. By default, the application will use it as a plain file through SQL Server Express. However, if you decide to host it in a full installation of SQL Server, all you need to do is update the connection string in the web.config file of your application.

The tables and schema of the database are fixed. Note that the same database—the aspnetdb.mdf file—contains tables to hold user profiles and also membership and role information. The use of a membership database with users and roles is important because personalization is designed to be user-specific and because a user ID—either a local Windows account or an application-specific logon—is necessary to index data.

Profile data has no predefined duration and is permanently stored. It is up to the Web site administrator to delete the information when convenient.

As mentioned, WSAT is not necessarily the way to go; it’s just one option for setting up the profile infrastructure. For example, if you’re using a custom provider, the setup of your application is responsible for preparing any required storage infrastructure—be it a SQL Server table, an Oracle database, or whatever else. We’ll cover the setup of profile providers in the next section.

Note

At this point, many developers start thinking that they probably don’t want to be bound to aspnetdb.mdf because it’s a general purpose tool or because it’s too generic of a repository for their data. So, many developers decide to plan to build a tailor-made custom provider and run their own solution.

Building custom providers is doable and fully supported by the framework. However, make sure that building such a provider doesn’t turn out to be simply an extra (and avoidable) pain in the proverbial neck. The aspnetdb.mdf solution is effective and free, and it provides zero cost of ownership. After you have hosted it in a SQL Server installation, you have the full power of management tools at your disposal. And, by the way, although you can reasonably consider renaming the database on a per-application basis, the database (and the related ASP.NET API) is designed to support multiple applications. In other words, you can even have a single instance of aspnetdb also in a hosting scenario.

Personally, I don’t mind using aspnetdb when I need profile support. Membership and role management, though, might be a different story.

Working with Anonymous Users

Although user profiles are designed primarily for authenticated users, anonymous users can also store profile data. In this case, though, a few extra requirements must be fulfilled. In particular, you have to turn on the anonymousIdentification feature, which is disabled by default:

<anonymousIdentification enabled="true" />

The purpose of anonymous user identification is to assign a unique identity to users who are not authenticated and recognize and treat all of them as an additional registered user.

Note

Anonymous identification in no way affects the identity of the account that is processing the request. Nor does it affect any other aspects of security and user authentication. Anonymous identification is simply a way to give a “regular” ID to unauthenticated users so that they can be tracked as authenticated, “regular” users.

In addition, to support anonymous identification you must mark properties in the data model with the special Boolean attribute named allowAnonymous. Properties not marked with the attribute are not made available to anonymous users.

<anonymousIdentification enabled="true" />
<profile enabled="true">
    <properties>
       <add name="UseEuroMetricSystem" type="Boolean"
            defaultValue="false" allowAnonymous="true" />
       <add name="TempSystem" type="String"
            defaultValue="F" />
       <add name="Locations"
        type="System.Collections.Specialized.StringCollection" />
    </properties>
</profile>

In the preceding code snippet, anonymous users can pick up the European metrics but cannot modify the way in which temperatures are displayed nor add their favorite locations.

Accessing Profile Properties

In a Web site project, the Page object features an extra Profile property added by the system via a partial class during the dynamic compilation step. Before the request begins its processing cycle, the Profile property of the page is set with an instance of the profile class created out of the content in the web.config file.

When the page first loads, profile properties are set to their default values (if any) or they are empty objects. They are never null. When custom or collection types are used to define properties, assigning default values might be hard. The code just shown defines a string collection object—the property Locations—but giving that a default value expressed as a string is not supported. At run time, though, the Locations property won’t be null—it will equal an empty collection. So how can you manage default values for these properties?

Properties that don’t have a default value can be initialized in the Page_Load event when the page is not posting back. Here’s how you can do that:

if (!IsPostBack)
{
    // Add some cities to the Locations property
    if (Profile.Locations.Count == 0) {
        Profile.Locations.Add("London");
        Profile.Locations.Add("Amsterdam");
    }
}

In a Web site project, the personalization data of a page is all set when the Page_Init event fires. However, when the Page_PreInit event arrives, no operation has been accomplished yet on the page, not even the loading of personalization data.

In a WAP project, if you opt for a strongly typed approach, you have no way to assign a default value to properties. The only workaround, obviously, is dealing with defaults right in the getter method of each property. Here’s an example:

[SettingsAllowAnonymous(true)]
public String TempSystem
{
    get
    {
        var current = (String) HttpContext.Current.Profile.GetPropertyValue("TempSystem");
        if (String.IsNullOrEmpty(current))
            return "F";
        return (String) current;
    }
    set { HttpContext.Current.Profile.SetPropertyValue("TempSystem", value); }
}

In a Web site project, the personalization data of a page is available only on demand—precisely, the first time you access the profile object.

Let’s consider some sample code that illustrates the power of the user profile API.

Note

The personalization data of a page is all set when the Page_Init event fires. However, when the Page_PreInit event arrives, no operation has been accomplished yet on the page, not even the loading of personalization data.

User Profiles in Action

Suppose you have a page that displays information according to user preferences. You should use the user profile API only to store preferences, not to store sensitive data. Losing the profile information should never cause the user any loss of money or serious inconvenience. Here’s the code you might have at the startup of the page request. The page first grabs in some way some weather-related information and then displays it as configured by the user:

protected void Page_Load(Object sender, EventArgs e)
{
   if (!IsPostBack)
   {
      var info = GrabWeatherInfo();
      DisplayData(info);
   }
}

private static WeatherInfo GrabWeatherInfo()
{
   ...
}

private void DisplayData(WeatherInfo info)
{
    // Type-safe solution for Web Application projects
    // (reusing the YourApp.UserProfile wrapper class discussed earlier)

    // Get profile information from the underlying repository
    var profile = YourApp.UserProfile.GetUserProfile();

    // Metric system
    var speedFormat = "{0} mph";
    if (profile.UseEuroMetricSystem)
        speedFormat = "{0} kmh";
    var speedText = String.Format(speedFormat, info.WindSpeed);

    // Temperature
    var tempText = String.Format("{0} {1}", info.Temperature, profile.TempSystem);
    lblWindSpeed.Text = speedText;
    lblTemperature.Text = tempText;

    // The sample page also displays a panel for users to change settings.
    // Display current settings through the edit panel as well.
    chkEuroMetric.Checked = profile.UseEuroMetricSystem;
    rdlTempSystem.SelectedIndex = (profile.TempSystem == "F" ? 0 : 1);
}

The output of the page can change depending on the settings entered by individual users. Figure 7-7 shows what the same page might look like for distinct users.

Different settings for different users.

Figure 7-7. Different settings for different users.

If anonymous access is permitted, any unauthenticated user is treated as the same one—meaning that all anonymous users share the same settings and any can change in the settings of one user affects all the others. (Most of the time, though, sites where profiles are fundamental just don’t allow anonymous access.)

How do you change settings? Here’s the code you can associate with the Save button you see in Figure 7-7:

protected void Button1_Click(Object sender, EventArgs e)
{
    // Retrieve and update the profile for the current user
    var profile = YourApp.UserProfile.GetUserProfile();
    profile.UseEuroMetricSystem = chkEuroMetric.Checked;
    profile.TempSystem = rdlTempSystem.SelectedItem.Value;

    // Persist settings for the current user
    profile.Save();

    // Refresh the page to ensure changes are immediately visible
    Response.Redirect("/profile.aspx");
}

The Redirect call is not strictly required; however, if it’s omitted, it won’t give the user an immediate experience based on the changes entered. If you omit the redirect, the changes (which are stored in the repository, anyway) will be visible only upon the next request.

Personalization Events

As mentioned, the personalization data is added to the HTTP context of a request before the request begins its processing route. But which system component is in charge of loading personalization data? ASP.NET employs an HTTP module for this purpose named ProfileModule.

The module attaches itself to a couple of HTTP events and gets involved after a request has been authorized and when the request is about to end. If the personalization feature is off, the module returns immediately. Otherwise, it fires the Personalize event to the application and then loads personalization data from the current user profile. When the Personalize event fires, the personalization data hasn’t been loaded yet. Handlers for events fired by an HTTP module must be written to the global.asax file.

void Profile_Personalize(object sender, ProfileEventArgs e)
{
    ProfileCommon profile = null;

    // Exit if it is the anonymous user
    if (User == null) return;

    // Determine the profile based on the role. The profile database
    // contains a specific entry for a given role.
    if (User.IsInRole("Administrators"))
        profile = (ProfileCommon) ProfileBase.Create("Administrator");
    else if (User.IsInRole("Users"))
        profile = (ProfileCommon) ProfileBase.Create("User");
    else if (User.IsInRole("Guests"))
        profile = (ProfileCommon) ProfileBase.Create("Guest");

    // Make the HTTP profile module use THIS profile object
    if (profile != null)
        e.Profile = profile;
    }
}

The personalization layer is not necessarily there for the end user’s amusement. You should look at it as a general-purpose tool to carry user-specific information. User-specific information, though, indicates information that applies to the user, not necessarily information entered by the user.

The personalization layer employs the identity of the current user as an index to retrieve the proper set of data, but what about roles? What if you have hundreds of users with different names but who share the same set of profile data (such as menu items, links, and UI settings)? Maintaining hundreds of nearly identical database entries is out of the question. But the standard profile engine doesn’t know how to handle roles. That’s why you sometimes need to handle the Personalize event or perhaps roll your own profile provider.

The code shown previously overrides the process that creates the user profile object and ensures that the returned object is filled with user-specific information accessed through the user role. The static method Create on the ProfileBase class takes the user name and creates an instance of the profile object specific to that user. ProfileCommon is the common name of the dynamically created class that contains the user profile.

The handler of the Personalize event receives data through the ProfileEventArgs class. The class has a read-write member named Profile. When the event handler returns, the profile HTTP module checks this member. If it is null, the module proceeds as usual and creates a profile object based on the user’s identity. If not, it simply binds the current value of the Profile member as the profile object of the page.

Migrating Anonymous Data

As mentioned, anonymous users can store and retrieve settings that are persisted using an anonymous unique ID. However, if at a certain point a hitherto anonymous user decides to create an account with the Web site, you might need to migrate to her account all the settings that she made as an anonymous user. This migration doesn’t occur automatically.

When a user who has been using your application anonymously logs in, the personalization module fires an event—MigrateAnonymous. Properly handled, this global event allows you to import anonymous settings into the profile of an authenticated user. The following pseudocode demonstrates how to handle the migration of an anonymous profile:

void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e)
{
    // Load the profile of the anonymous user
    ProfileCommon anonProfile;
    anonProfile = Profile.GetProfile(e.AnonymousId);

    // Migrate the properties to the new profile
    Profile.UseEuroMetricSystem = anonProfile.UseEuroMetricSystem;
    ...
}

You get the profile for the anonymous user and extract the value of any property you want to import. Next you copy the value to the profile of the currently logged-on user.

Profile Providers

In ASP.NET, the profile API is composed of two distinct elements: the access layer and the storage layer.

The access layer provides a strongly typed model to get and set property values and also manages user identities. It guarantees that the data is retrieved and stored on behalf of the currently logged-on user.

The second element of the profile system is data storage. The system uses ad hoc providers to perform any tasks involved with the storage and retrieval of values. ASP.NET comes with a profile provider that uses SQL Server Express as the data engine. If necessary, you can also write custom providers. The profile provider writes profile data into the storage medium of choice and is responsible for the final schema of the data.

Important

In ASP.NET, a provider is defined as a pluggable component that extends or replaces some system functionality. The profile provider is just one implementation of the ASP.NET provider model. Other examples of providers are the membership provider and role manager provider, both of which will be discussed later in the book. At its core, the provider infrastructure allows customers to change the underlying implementation of some out-of-the-box system functionalities while keeping the top-level interface intact. Providers are relatively simple components with as few methods and properties as possible. Only one instance of the provider exists per application domain.

Configuring Profile Providers

All features, such as user profiling, that have providers should have a default provider. Normally, the default provider is indicated via a defaultProvider attribute in the section of the configuration file that describes the specific feature. By default, if a preferred provider is not specified, the first item in the collection is considered the default.

The default profile provider is named AspNetSqlProfileProvider and uses SQL Server Express for data storage. Providers are registered in the <providers> section of the configuration file under the main node <profile>, as shown here:

<profile>
    <providers>
        <add name="AspNetSqlProfileProvider"
            connectionStringName="LocalSqlServer" applicationName="/"
            type="System.Web.Profile.SqlProfileProvider" />
    </providers>
</profile>

The <add> nodes within the <providers> section list all the currently registered providers. The previous code is an excerpt from the machine.config file. Attributes such as name and type are common to all types of providers. Other properties are part of the provider’s specific configuration mechanism. Tightly connected with this custom interface is the set of extra properties—in this case, connectionStringName and description. The description attribute is simply text that describes what the provider does.

The connectionStringName attribute defines the information needed to set up a connection with the underlying database engine of choice. However, instead of being a plain connection string, the attribute contains the name of a previously registered connection string. For example, LocalSqlServer is certainly not the connection string to use for a local or remote connection to an instance of SQL Server. Instead, it is the name of an entry in the new <connectionStrings> section of the configuration file. That entry contains any concrete information needed to connect to the database.

The LocalSqlServer connection string placeholder is defined in machine.config as follows:

<connectionStrings>
    <add name="LocalSqlServer"
        connectionString="data source=.SQLEXPRESS;
                         Integrated Security=SSPI;
                         AttachDBFilename=|DataDirectory|aspnetdb.mdf;
                         User Instance=true"
        providerName="System.Data.SqlClient" />
</connectionStrings>

As you can see, the connection string refers to an instance of SQL Server named SQLEXPRESS and attaches to the aspnetdb.mdf database located in the application’s data directory—the App_Data folder.

Structure of AspNetDb.mdf

As a developer, you don’t need to know much about the layout of the table and the logic that governs it; instead, you’re responsible for ensuring that any needed infrastructure is created. To do so, you use the Build|ASP.NET Configuration menu item in Visual Studio to start the ASP.NET site administration tool. A view of the tables in the database is shown in Figure 7-8.

A view of the interior of the AspNetDb database and the profile table.

Figure 7-8. A view of the interior of the AspNetDb database and the profile table.

Note that the AspNetDb database isn’t specific to the personalization infrastructure. As you can see in the figure, it groups all provider-related tables, including those for membership, roles, and users. The internal structure of each database is specific to the mission of the underlying provider.

Custom Profile Providers

The SQL Server profile provider is good at building new applications and is useful for profile data that is inherently tabular. In many cases, though, you won’t start an ASP.NET application from scratch, but you will instead migrate an existing application. You often already have data to integrate with the ASP.NET profile layer. If this data doesn’t get along with the relational model, or if it is already stored in a storage medium other than SQL Server, you can write a custom profile provider. An old but still helpful link is the following: http://msdn.microsoft.com/msdnmag/issues/07/03/ASPNET2/default.aspx.

Profile providers push the idea that existing data stores can be integrated with the personalization engine using a thin layer of code. This layer of code abstracts the physical characteristics of the data store and exposes its content through a common set of methods and properties. A custom personalization provider is a class that inherits ProfileProvider.

Finally, note that a custom provider doesn’t necessarily have to be bound to all profile properties. You can also use the default provider for some properties and a custom provider for others. Here’s how you specify the provider for a property using the declarative approach:

<properties>
    <add name="BackColor" type="string" provider="MyProvider" />
    ...
</properties>

In the preceding code, the BackColor property is read and written through the MyProvider provider. If you are in WAP, instead, and wrote your profile wrapper class, you resort to the ProfileProvider attribute:

[ProfileProvider("MyProvider")]
public String BackColor
{
   get { ... }
   set { ... }
}

Obviously, the provider name must correspond to one of the entries in the <providers> section.

Page Localization

The whole theme of localization is nothing new in the .NET Framework, and ASP.NET is no exception. You have had tools to write culture-specific pages since the very first version of ASP.NET. In addition, these tools didn’t change significantly with the stream of versions, and today they form a rather stable API.

Localization is not a hard feature to build and doesn’t touch any staggering peaks of technical difficulty. A successfully localizable application just requires planning, development care, and constant small-scale refactoring. Frankly, localization is not for just any (Web) application either. In this regard, I consider localization as an all-or-nothing feature of a Web project: either localization is a requirement or it is not. If it is a requirement, every little piece of UI (text, layout, CSS, script, and images) must be architected and implemented to be easily replaceable and configurable. Otherwise, I just don’t care about localization and stuff literals in the page layouts.

Considering localization from the perspective of an entire application with a not-so-short expectation of life, there are three aspects of it that need to be addressed: how to make resources localizable, how to add support for a new culture, and how to use (or whether to use) databases as a storage place for localized information. Let’s review the techniques that allow you to keep resources easily localizable.

Making Resources Localizable

A localizable ASP.NET Web Form uses resources instead of hard-coded text to flesh out the user interface. In this context, a resource is meant to be an item of stored text associated with a public name and typically compiled into its own assembly. A resource assembly is a standard class library that contains one or more RESX files. A RESX file is an XML document that contains resource names and content. Visual Studio provides a typical dialog box to add such a new item to the project. (See Figure 7-9.)

Adding a new resource item to the ASP.NET project.

Figure 7-9. Adding a new resource item to the ASP.NET project.

You always use the resource name to refer to its content from within application pages. After a resource assembly is linked to the application, the ASP.NET runtime selects the correct value at run time according to the user’s language and culture.

Note

Instead of creating and maintaining a resource assembly, you can simply create an App_GlobalResources folder under the site root and place there any resource RESX files you might need. Such files are compiled into resource assemblies on demand care of the ASP.NET runtime. A possible drawback is that the RESX files are deployed as source code to the site.

Global and Local Resources

The ASP.NET documentation distinguishes between global and local resources. Global resources are available to any pages in the application; local resources, instead, are specific to a single page or the pages located in a given directory hierarchy. In terms of syntax, global and local resources are the same thing—a RESX file. Local resources must be deployed to an App_LocalResources folder. You can have only one global resource folder in a site; instead, you can have multiple local resource folders, one for each section of the site you want to restrict resources to. In Figure 7-10, you can see a local resource folder under the Private folder that affects only the pages defined inside the Private folder and its child folders.

Global and local resource folders.

Figure 7-10. Global and local resource folders.

In a local resource folder, you can have resource files with folder-level visibility (such as personal.resx in Figure 7-10) as well as page-specific resource files. In this case, a simple naming convention binds the file to the page. If the page is named sample.aspx, its corresponding resource file will be sample.aspx.resx.

Global and local resource files can happily coexist in the same application. Finding the right balance between what’s global and what’s local is ultimately up to you. Overall, the best approach seems to be having multiple resource files—either local or global. You might start with a local resource file for each page, and then merge strings and other resources into a global resource file as you find them referenced from multiple pages.

Important

From what I have learned on the battlefield, having a single global file to hold all localizable resources turns into a not-so-pleasant experience, even for a moderately complex Web application. One issue is the size of the file, which grows significantly; another issue, which is even more painful, is the possible concurrent editing that multiple developers might be doing on the same file with the subsequent need for a continuous merge. However, I encourage you not to overlook the naming issue.

When you have hundreds of strings that cover the entire application scope, how do you name them? Many strings look the same or differ only in subtle points. Many strings are not entire strings with some sensible meaning; they often are bits and pieces of some text to be completed with dynamically generated content. And the concatenation might be different for various languages.

Trust me: naming a few of them in the restricted context of only some pages is doable; handling hundreds of them for the entire application is really painful.

Using Resources: Declarative vs. Programmatic

In ASP.NET Web Forms, a key decision to be made early is whether you want to insert localizable text declaratively, programmatically, or both. Inserting localized text programmatically means writing a method on each Page class that assigns ad hoc text before display. This approach offers the maximum flexibility and allows you to retrieve localized text using the API that best suits you. Here’s some code to read the value of the resource item named Welcome from a resource file named literals.resx:

MyResources.Literals.Welcome

MyResources is the default namespace of the assembly that contains the resource file. Literals is the name of the file and the class name that ultimately exposes text items as public static properties. Finally, Welcome is the name of the resource item. For this code to work, you must ensure you create an assembly with a Literals.resx file whose access modifier in Visual Studio is set to Public. Note that the default value is Internal, which will not make resource items publicly available. (See Figure 7-11.)

Editing a RESX document.

Figure 7-11. Editing a RESX document.

The preceding syntax is general enough to work with any RESX file, regardless of its local or global status. This is also the natural way of localizing applications in ASP.NET MVC. However, it doesn’t get along very well with the ASP.NET server controls that populate Web Forms pages. The point is that you can’t use the preceding expression in a <%= … %> code block in all possible locations within a Web Forms page.

The following markup compiles just fine:

<h1><%= MyResources.Literals.BookTitle %> </h1>

Unfortunately, you can’t embed the code block as the attribute of a server control. (This is where the key difference between Web Forms and ASP.NET MVC arises.) The following code won’t even compile:

<asp:MenuItem NavigateUrl="~/Default.aspx" Text="<%= MyResources.Literals.Home %>"/>

The reason has to be found in the way in which a server control produces its own output.

In the end, for a Web Forms page the most convenient approach results from any of the following:

  • Design your own localization layer that each page passes through to have its text localized. This layer is a sort of transformer that reads from localization storage and replaces placeholder text. Your localization storage can be RESX file or, why not, your own database table.

  • Go with any shortcuts that Visual Studio and ASP.NET machinery might have released. This includes a tailor-made syntax for local resources and a specific expression builder for declaratively binding control attributes to localized text. However, the declarative syntax for global resources requires the App_GlobalResources folder and direct deployment of any RESX files.

Let’s find out more about what’s required to deal with globally defined resources.

Dealing with Global Resources

Using global resources programmatically entails writing for each page some code as shown next. The code will be invoked just before display:

protected void LocalizeTextInPage()
{
    // For each control you expect in the page, retrieve the localized text
    Label1.Text = MyResources.Literals.Welcome;
    ...
    Label2.Text = HttpContext.GetGlobalResourceObject("globals.resx", "Description");;
    ...
}

If your global resources are stored through plain RESX files, you can retrieve it using either of the two expressions just shown. In addition to using the object expression that navigates into the class hierarchy of the resource assembly, you can also employ the GetGlobalResourceObject method of the HttpContext object. If the localized text resides elsewhere, the API for retrieving it is up to you.

Alternatively, if you prefer to take the declarative route, use the object expression within plain page markup and resort to the ASP.NET-specific $Resources expression builder for control attributes. Here’s an example:

<asp:Literal runat="server" Text="<% $Resources:Globals, WelcomeMessage %>" />

$Resources refers to an ASP.NET built-in expression builder. It accepts a parameter that is a comma-separated string trailing the colon (:) symbol. The first token indicates the name of the RESX file that is the source of the localized text. The second token indicates the name of the resource item to read. There are no facilities to bind declaratively localized text stored outside of RESX files.

Dealing with Local Resources

Local resources are strictly page-specific in the sense that if it’s properly named after the ASPX source file, the content of a resource file can be referenced using direct syntax from the markup, as shown here:

<asp:Label runat="server" ID="Label1"
           meta:resourcekey="Label1_ResourceID" />

The resourcekey meta attribute indicates that property values for the Label1 control are to be taken from a page-specific resource file. If the resource file for the page contains an entry such as Label1_ResourceID.Text, the Text property of Label1 will be set to the stored value. The same can be done for any other properties of the control.

Resources and Cultures

A RESX file is a plain XML document. How can you distinguish a RESX file that represents French localized text from the RESX of German localized text? A RESX file name that doesn’t include culture information is assumed to contain language-neutral text with no culture defined.

To create a resource assembly for a specific culture—say, French—you need to name the resource file as follows: sample.aspx.fr.resx. The fr string should be replaced with any other equivalent string that identifies a culture, such as de for German or en for English.

When resources from multiple cultures are available in the AppDomain, the ASP.NET runtime machinery detects the underlying culture and picks up the matching resource file. I’ll return in a moment at how to set and change the culture programmatically.

Setting the Current Culture in .NET Applications

In the .NET Framework, the culture is set on the current thread through the CurrentCulture and CurrentUICulture properties. In general, both properties are necessary when you want to support multiple languages in a page or view. In fact, the two properties refer to distinct capabilities and have an impact on different areas of the user interface.

The CurrentCulture property affects the results of functions, such as the date, the number, and currency formatting. The CurrentUICulture property, on the other hand, determines the localized resource file from which page resources are loaded. The following code snippet shows a possible way to arrange a unit test aimed at testing whether culture-specific items are correctly retrieved. If you intend to test only whether resource files are being used as expected, you can comment out the setting of CurrentCulture.

const String culture = "it-IT";
var cultureInfo = CultureInfo.CreateSpecificCulture(culture);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;

Note that the two culture properties might or might not have the same value. For example, you can switch the language of text and messages according to the browser’s configuration while leaving globalization settings (such as dates and currency) constant.

Note

Culture names are a combination of two pieces of information: the language and the country/region that you intend to refer to. The two strings are combined with a dash symbol (-). Often, but not necessarily, the two strings coincide. For example, it-IT means the Italian culture for the country of Italy, whereas en-US indicates the English culture for the United States, which is expected to be different from en-GB or en-SA.

Setting the Current Culture in ASP.NET Pages

If you’re writing an ASP.NET Web Forms application, you don’t need to deal with the Thread class. In ASP.NET, you have culture properties ready-made on the Page class. They are string properties named Culture and UICulture.

The default value being assigned to both properties is auto, meaning that ASP.NET automatically detects the browser’s language for the thread in charge of the request. The getter method of both properties is defined as shown here:

public String UICulture
{
    get { return Thread.CurrentThread.CurrentUICulture.DisplayName; }
    set { ... }
}

When the auto mode is on for the page culture, the end user is ultimately responsible for determining the language of the pages. All the developers need to do is ensure that proper resource files are available. If no suitable resource file is found for the detected culture, ASP.NET will fall back to the neutral (default) culture.

Obviously, a specific culture can be enforced programmatically or declaratively. You can employ a global setting for the culture by using the <globalization> section of the web.config file:

<globalization uiculture="it-IT" culture="it-IT" / >

A global and fixed setting for culture, however, is hardly what you want most of the time. Most of the time, instead, you want the ability to set the culture programmatically and the ability to change it on the fly as the user clicks an icon or requests a culture-specific URL.

Changing Culture on the Fly

To change the culture programmatically, you need to satisfy two key requirements. First, define how you’ll be retrieving the culture to set. The culture can be a value you read from some database table or perhaps from the ASP.NET cache. It can also be a value you retrieve from the URL. Finally, it can even be a parameter you get via geo-location—that is, by looking at the IP address the user is using for connecting.

After you have the culture ID to set, you have to set it by acting on the current thread, as mentioned earlier. Note that the culture must be set for each request because each request runs on its own thread.

If you intend to read and set the culture as part of the page initialization work, note that the following code, which might appear obvious at first, just won’t work:

void Page_Load(Object sender, EventArgs e)
{
    Culture = "IT";
    UICulture = "it-IT";
}

The Page_Load handler is fired too late to be effective. The recommended approach consists of overriding the InitializeCulture method on the Page class:

protected override void InitializeCulture()
{
   base.InitializeCulture();
   Culture = "IT";
   UICulture = "it-IT";
}

The setter method of both culture properties will then take care of setting culture information on the current thread. Setting the thread directly does work, but it’s unnecessary to do so.

Changing the language on the fly as the user clicks on a link is a bit trickier. The idea is that you override the InitializeCulture method so that the page reads the language to use from global storage—for example, the ASP.NET Cache or Session.

protected override void InitializeCulture()
{
   base.InitializeCulture();
   UICulture = DetermineLocaleToEnforce();
}
private String DetermineLocaleToEnforce()
{
    var language = Cache["Language"] as String;
    if (String.IsNullOrEmpty(language))
        language = "en-US";
    return language;
}

When the user interacts with the user interface in the postback, you simply read the newly selected language, update the storage, and then redirect to the same page for a refresh:

protected void Button1_Click(Object sender, EventArgs e)
{
    var languageInfo = GetCurrentLocale();
    Cache["Language"] = languageInfo;
    Response.Redirect("/private/moneyintl.aspx");
}
private String GetCurrentLocale()
{
    return Languages.SelectedValue;
}

This is good enough if your user interface is limited to listing a few image buttons with flags. If you want a drop-down list of languages to choose from, you also must take care of re-indexing the list of items. This translates into some extra code in Page_Load.

protected void Page_Load(Object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        var languageCode = DetermineLocaleToEnforce();
        var item = Languages.Items.FindByValue(languageCode);
        Languages.SelectedIndex = Languages.Items.IndexOf(item);
    }
}

Nicely enough, implementing the same feature is much simpler in ASP.NET MVC, even though ASP.NET MVC shares exactly the same run-time environment as ASP.NET Web Forms. The postback model, which is great at many things, makes some other things a bit harder than expected. In ASP.NET MVC, you can simply create your own action invoker and then, for each and every controller method, you retrieve the language and set it to the current thread. The action invoker is a way to intercept the execution of action methods in a rather unobtrusive way. In Web Forms, you can achieve the same result by using an HTTP module that kicks in for every request, reads the currently set language, and sets the culture on the current thread.

Note

More and more Web sites check the location from where a user is connected and suggest a language and a culture. This feature requires an API that looks up the IP address and maps that to a country/region and then a culture. Some browsers (for example, Firefox 3.5, Safari, iPhone, and Opera) have built-in geo-location capabilities that work according to the W3C API. (See http://www.mozilla.com/firefox/geolocation.)

To support other browsers (including Internet Explorer), you can resort to third-party services such as Google Gears. Google Gears is a plug-in that extends your browser in various ways, including adding a geo-location API that returns the country/region of the user from the current geographical location. Note that Google returns the ISO 3166 code of the country/region (for example, GB for the United Kingdom) and its full name. From here, you have to determine the language to use. The country/region code doesn’t always match the language. For the United Kingdom, the language is en. To install Google Gears, pay a visit to http://gears.google.com.

Adding Resources to Pages

An ASP.NET page is usually made of a bunch of auxiliary resources including script files, cascading style sheets (CSS), and images. When the browser downloads a page, it usually places a number of independent requests to the Web server and tracks when the document is ready. The display of the document, however, might begin before the entire document (and related links) has been downloaded. Developers of heavy pages made of several resources (a few dozens is not unusual) resort to a number of techniques to optimize the download experience of their pages. Let’s review a few interesting techniques that simplify the management of scripts, images, and other resources.

Using Script Files

The only HTML-supported way of linking script files to a page is via the <script> tag and its src attribute. When a page has several scripts, the degree of parallelism at which the browser can operate is dramatically lowered, as is the load time of the page. Typically, in fact, browsers are idle while downloading a script code, regardless of the host name.

It turns out that managing scripts effectively, and sometimes refactoring the page to maximize its download and rendering time, is a critical topic. Let’s see the most common techniques to deal with script files.

Scripts at the Bottom of the Page

Because of the way in which browsers operate, moving all <script> tags at the bottom of the page just before the </body> tag improves the download of the page. Unfortunately, this is not always possible.

Why do browsers stop any activity while downloading script code?

In general, the script being downloaded might contain some instructions, such as document.write, that could modify the status of the current Document Object Model (DOM). To avoid nasty situations that might derive from here, browsers download a script synchronously and run it right after downloading. A script that contains document.write calls can hardly be moved elsewhere without causing some damage.

Back with Internet Explorer 4, Microsoft introduced a little-known attribute for the <script> tag—the defer attribute. Later incorporated in the HTML 4 specification, the defer attribute was just meant to tell the browser whether or not loading the script can be deferred to the end of the page processing. A script that specifies the defer attribute implicitly states it is not doing any direct document writing. Using the defer attribute is not a standard technique because of the non-uniform way in which browsers support it. For this reason, moving script tags manually at the end is the most common trick to speed up pages. For more information on the defer attribute, have a read of http://hacks.mozilla.org/2009/06/defer.

Note

Two libraries are extremely popular as far as improving the script downloading is concerned. One is LABjs (available at http://www.labjs.com), and the other is RequireJS (available at http://www.requirejs.org). Both allow loading scripts (and other resources) in parallel, which maintains possible (declared) dependencies between files.

Using a Content Delivery Network

Among other things, the download time also depends on the physical distance between the client browser and the server expected to serve a given resource. For high-volume, international sites, this can be a significant problem.

A content delivery network (CDN) is a third-party, geographically distributed server that serves commonly accessed files from the nearest possible location. By using a CDN, you guarantee the best service without the costs for your organization of setting up such a wide network of servers.

For your code, the change is minimal—you just replace your local server URL with the CDN URL and get the script from there. Here’s how to link the ASP.NET AJAX library from the Microsoft CDN:

<script type="text/javascript"
        src="http://ajax.microsoft.com/ajax/4.0/MicrosoftAjax.js" />

Popular libraries such as jQuery and Microsoft ASP.NET AJAX are being offered through Google and Microsoft CDN.

Using a CDN is also beneficial because it increases the likelihood that the browser cache already contains a resource that might have been referenced using the same URL by other sites using the same CDN. The perfect example of a file that would greatly benefit users when put on a CDN is the one mentioned a moment ago—the jQuery library. You won’t benefit much, on the other hand, from placing on a CDN files that only one application uses.

Reasons for Minifying a Script File

A golden rule of Web site performance optimization says that once you have minimized the impact of static files (scripts, style sheets, and images), you’re pretty much done. In addition to the time and distance of the download, the size also matters—the smaller, the better.

You can use Gzip compression on the Web server to serve any resources quickly. Regular pages, including ASP.NET pages, are often returned gzipped, but the same doesn’t always happen for other static resources such as scripts and style sheets. Images on the other side are often already compressed (PNG, JPG, GIF), and any attempt to further compress them results in waste of time rather than an improvement.

Beyond this, consider that script files are rich with white spaces and blanks. Simply removing these characters can cut a significant percentage of software fat out of the file. This is just what minifiers are for.

A minifier is a tool that parses a given script file and rewrites it in a way that is functionally equivalent to the original but devoid of any unnecessary characters. The jQuery library is commonly served in its minified form. A minified script file is nearly impossible to read or understand for a human, so I don’t recommend using minified files during development.

Microsoft released a minifier tool; you can get it at http://aspnet.codeplex.com/releases/view/40584. This tool can work on script and CSS files. Also, in addition to removing white spaces and blanks, it safely attempts to reduce curly brackets and to make variable names shorter.

Note

You might want to look at build-time minifier tools such as Chirpy because having to manually minify several files can be a bit of a pain. See http://chirpy.codeplex.com.

Localized Scripts

Like other Web resources, scripts can be subject to localization. At the very end of the day, a script is a relatively long string of text, so there’s really nothing that prevents you from embedding a script into the application resources along with a plain RESX file.

The method GetWebResourceUrl on the ClientScript property of the Page class can be used to return the URL to any resource stored in a satellite (localized) assembly. In this way, you link your scripts from the assembly, deploy the localized assembly, and you’re done.

The only other alternative you have is maintaining different copies of the script and resolve the name programmatically. In ASP.NET 4, the ScriptManager control can streamline this task quite a bit. Here’s how to use the script manager component:

<asp:ScriptManager ID="ScriptManager1" runat="server" EnableScriptLocalization="true">
   <Scripts>
      <asp:ScriptReference Path="Person.js" ResourceUICultures="it-IT, de-DE" />
   </Scripts>
</asp:ScriptManager>

When the property EnableScriptLocalization is true, the <Scripts> section lists all script files to be downloaded that might be subject to localization. Localization consists of mangling the provided name of the script in a way that incorporates culture information. For example, the preceding code will emit the following markup if the UI culture is set to Italian:

<script ... src="person.it-IT.js" />

The value of the page property UICulture determines the culture code being used to mangle the file name. When configuring the ScriptManager control, you indicate the supported cultures through the ResourceUICultures property on individual script references. If a related file is missing, you’ll get a 404 error for the request. Otherwise, the markup will be emitted to target the language-neutral script file.

Using Cascading Style Sheets and Images

Cascading style sheets and images are the remaining two-thirds of the auxiliary static resources around most Web pages. Some consolidated techniques also exist to minimize the impact of these resources on your pages.

The first consideration to make is that the more requests you make, the more your users are likely to wait to see the page. Aggregating multiple scripts in a single (but larger) file is relatively easy and effective. It is doable for CSS files too; but with images? How can you combine multiple images to be used in distinct areas of the page and then reference just the section you need and where you need it?

Grouping Images into Sprites

To reduce the number of HTTP requests that a page requires in order to fetch all the images it needs, you use sprites.

A sprite is a single image that results from the composition of multiple images that are stored side by side, forming a grid of any size you like. You then link the image URL to any <img> tag where you need a section of it and use CSS styles to specify exactly which portion you want in a given place. Here’s an example:

<img src="sprite.png" class="image1" />
<img src="sprite.png" class="image2" />
<img src="sprite.png" class="image3" />

You can even embed the reference to the image into the CSS as shown here:

<div class="UserInformation" />

The CSS class is defined as follows:

.UserInformation {
    width:123px;
    height:115px;
    background-image:url(sprite.png);
    background-position:-0px 0;
}

In other words, you pinpoint the fragment of the sprite you like using CSS attributes such as background-position, background-image and, of course, width and height.

Microsoft is currently working on an extension to ASP.NET 4 that supports sprites. For more information, check out http://aspnet.codeplex.com/releases/view/50140.

Note

Image inlining is another potentially useful technique for dealing with images and static resources more comfortably. Image inlining consists of streamlining a Base64-encoded version of the image file into a CSS file or an HTML page. As of today, very few browsers support this technique and, in addition, the Base64 encoding increases the size of individual images, making for a large download.

External References vs. Inline Content

This is one of those evergreen questions that are revamped periodically in geek talks. Is it better to embed script and style sheets (and to some extent images) into a page, or is it preferable to keep several distinct references that the browser can deal with?

External references increase the number of HTTP requests being made, but they keep the page size smaller (often significantly smaller) and, more importantly, can be cached by the browser. Frankly, inline content is a great thing at development time where, instead, the effects of browser caching can be quite annoying. For deployed sites, browser caching saves you HTTP requests and is a feature that you can fine-tune when preparing the response for a given page or resource.

As mentioned, just reducing the number of HTTP requests might not ensure optimal performance. You should work in two directions and try to produce a magical mix of fewer HTTP requests for not-so-large resources.

Note

To measure the performance and quality of Web pages, you can use YSlow—a Firefox add-on integrated with the Firebug Web development tool. (See http://developer.yahoo.com/yslow.) Based on a set of commonly accepted best practices, the tool analyzes a Web page and provides recommendations for improving the overall performance. As far as Internet Explorer is concerned, Internet Explorer 9 comes with the IE9 Developer toolbar, which provides similar capabilities.

Summary

In this chapter, we examined a few issues you might face when building pages and interacting with them—errors, personalization, and resource handling.

Often, good programs do bad things and raise errors. In the Web world, handling errors is a task architecturally left to the run-time environment that is running the application. The ASP.NET runtime is capable of providing two types of error pages, both of which are not very practical for serious and professional applications, although for different reasons. When a user who is locally connected to the application does something that originates an error, by default ASP.NET returns a “geek” page with the stack trace and the full transcript of the exception that occurred. The remote user, on the other hand, receives a less compromising page, but certainly not a user-friendly one. Fortunately, though, the ASP.NET framework is flexible enough to let you change the error pages, even to the point of distinguishing between HTTP errors.

Personalization allows you to write pages that persist user preferences and parametric data from a permanent medium in a totally automated way. As a programmer, you’re in charge of setting up the personalization infrastructure, but you need not know anything about the internal details of storage. All you do is call a provider component using the methods of a well-known interface.

Finally, modern Web pages are much more than just HTML markup. Script files, images, CSSs, and literals need to be localized and effectively loaded. We examined a number of consolidated and effective techniques to localize pages, optimize page rendering, and download and minimize the impact of large and numerous scripts, style sheets and images.

In the next chapter, we’ll take page authoring to the next level by exploring master pages and wizards.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.185.42