Build Your ASP.NET Pages on a Richer Bedrock

Download the source code for this article.

Contents

Build a Richer Bedrock
Trap the Browser Refresh
Consume the Page Refresh Event
Entertain Users During Lengthy Operations
Focus to Controls
Conclusion

All Microsoft ASP.NET pages originate from a common root page represented by the System.Web.UI.Page class. To service a request for an .aspx resource, the ASP.NET runtime builds a dynamic class and makes it inherit from the base Page class, or from a class that, in turn, inherits from the base. If the page is created within a Microsoft Visual Studio .NET 2003 project where the code-behind model is supported, the dynamically created Page class inherits from the code-behind class which, in turn, inherits from the base page.

The base Page class implements the classic ASP.NET page lifecycle (the load-postback-render cycle) and provides derived pages with a bunch of predefined members and capabilities, such as postback detection, script injection, rendering, and viewstate management.

In the end, the System.Web.UI.Page class is just a base class defining a common and minimal set of capabilities and behaviors. In a specific application, pages can reasonably aim at more, and expose a richer programming interface. There are two possible types of extensions: general-purpose enhancements to the page's infrastructure, and application-specific features. An example of the former type of extension is a property that represents the menu of the page and its items. An application-specific page is normally designed starting from a logical "master" page made of static and common areas and customizable regions. The contents of these regions may vary on a page basis; regions are typically populated through templates, placeholders, and user controls.

Note that in Microsoft ASP.NET 2.0, the introduction of master pages greatly simplifies the building of application specific pages with custom and programmable properties and members.

What if, instead, you want a richer and more composite infrastructure for the pages? How can you instruct all of your pages to provide additional system-level features, like the ability to detect the F5 (Refresh) key? The code required to implement a given trick can always be merged with the operational code of each specific page—the code you find packed in the code-behind class. However, even with only two or three features implemented, the quality of the resulting code begins dangerously to resemble that of the notorious spaghetti-code. You definitely need to explore alternative ways.

Build a Richer Bedrock

A better approach is to build a new base Page class, and use that in the place of the standard System.Web.UI.Page class. In this article, I'll take a few common features and their common implementations and wrap them up in a new and richer Page class. The features I'm going to consider are:

  • Trapping the F5 (Refresh) key.
  • Starting and controlling a lengthy operation that needs an intermediate feedback page to be sent to the user.
  • Setting the input focus upon page loading.

If you're a frequent visitor of newsgroups and community sites dedicated to ASP.NET, and if you're a good reader of articles, books, and newsletters, then you probably know already how to implement each of the above features individually in the context of ASP.NET 1.x applications. The challenge here is making all of them available through a unique component and a single entry-point.

By defining a custom Page class, you can make all your additional features and services available to any new .aspx page with minimum effort and maximum return. Code-behind pages created with Visual Studio .NET 2003 are declared as follows.

public class WebForm1 : System.Web.UI.Page
{
   :
}

To make the Web form class inherit from a non-default Page class, you just change the base type, as follows.

public class WebForm1 : Msdn.Page
{
   :
}

If the page is created outside Visual Studio .NET 2003, or makes any use of inline code, you set its base type through the Inherits attribute in the @Page directive.

<% @Page Inherits="Msdn.Page" ... %>

You can change the base type of ASP.NET pages on a per-page basis as discussed here, or using the <pages> node in the configuration files.

<pages pageBaseType="Msdn.Page" />

The pageBaseType attribute of the <pages> node indicates the name and assembly of the class to use as the base type of all dynamically created Page classes. By default, the attribute is set to System.Web.UI.Page in the machine.config file. You can override the setting in the application's web.config file.

Let's see how each of the previously listed features is implemented in practice and encapsulated in an all-encompassing class.

Trap the Browser Refresh

In an article originally published on aspnetPRO Magazine several months ago, I outlined the steps needed to detect when the user presses the F5 browser button to refresh the current page. The page refresh is the browser's response to a specific user action—hitting the F5 key or clicking the toolbar button. The page refresh action is a sort of internal browser operation, for which the browser doesn't provide any external notification in terms of events or callbacks. Technically speaking, the page refresh consists of the "simple" reiteration of the latest request. In other words, the browser caches the latest request it had served, and reissues it when the user hits the page refresh key.

Just because all browsers (that I'm aware of) don't provide any kind of notification for the page refresh event, there's no way the server-side code (ASP.NET, classic ASP, or ISAPI DLLs, for example) can distinguish a refresh request from an ordinary submit or postback request. To help ASP.NET detect and handle page refreshes, you need to build surrounding machinery that makes two otherwise identical requests look different.

The browser implements the refresh, resending the last sent HTTP payload; to make the copy look different from the original, an extra service must add additional parameters and the ASP.NET page must be capable of catching them. The figure below provides the big picture of the subsystem I'm going to build.

Figure 1. The machinery set up to make a refresh request look different from a postback/submit request

Each request served in the context of the session is given a unique and progressive ticket number. The ASP.NET page generates the ticket just before the response is generated and stores it in a custom hidden field sent to the browser. When the user submits a new request (causes the displayed page to post back), the hidden field (if any) is automatically attached to the request for the server.

On the Web server, a new HTTP module intercepts the AcquireSessionState event, retrieves the current ticket from the hidden field, and compares it to an internally cached last-served ticket ID. The last-served ticket is stored in the session state. If the current ticket is greater than the last-served ID, or if the two are both zero, then the request is an ordinary submit or postback. Aside from this, the refresh HTTP module doesn't do anything more than usual and lets the request pass unchanged.

If the last-served ticket is greater than, or equal to, the current ticket, the request is identified as a page refresh. In this case, the HTTP module limits to create a new entry in the Items collection of the HTTP context of the request. In ASP.NET, the HttpContext object represents the context of the request and accompanies the whole cycle of the request from begin to end. The Items property of the HttpContext object is a cargo collection that can be used by HTTP modules, factory handlers, and handlers to forward custom information to the actual page object. Anything stored in the Items collection is visible to all the components involved with the processing of the current request. The lifetime of the information is the same as of the request; therefore, any data is destroyed when the response is generated. By using the HttpContext.Current static property, you can access the HTTP context of the ongoing request from any class involved in the process.

The Refresh HTTP module creates a new entry, named IsPageRefreshed, in the Items collection. The boolean value of the entry indicates whether the page is requested through a regular submit/postback or as a refresh. The listing below shows the implementation of the Refresh HTTP module.

using System;
using System.Web;
using System.Web.SessionState;

namespace Msdn
{
  public class RefreshModule : IHttpModule {
     // IHttpModule::Init
   public void Init(HttpApplication app)
   {
      // Register for pipeline events
      app.AcquireRequestState += 
             new EventHandler(OnAcquireRequestState);
   }

   // IHttpModule::Dispose
   public void Dispose() {}

   // Determine if a F5 or back/fwd action is in course
   private void OnAcquireRequestState(object sender, EventArgs e) {
      // Get access to the HTTP context 
      HttpApplication app = (HttpApplication) sender;
      HttpContext ctx = app.Context;

      // Check F5 action
      RefreshAction.Check(ctx);
         return;
   }
   }
}

The RefreshAction class contains the logic that determines if the current request is a page refresh. If it is determined to be a page refresh, then the Items collection of the HttpContext contains a new entry: IsPageRefreshed, set to true.

public static void Check(HttpContext ctx)
{
   // Initialize the ticket slot
   EnsureRefreshTicket(ctx);

   // Read the last ticket served in the session (from Session)
   int lastTicket = GetLastRefreshTicket(ctx);

   // Read the ticket of the current request (from a hidden field)
   int thisTicket = GetCurrentRefreshTicket(ctx);

   // Compare tickets
   if (thisTicket > lastTicket || 
      (thisTicket==lastTicket && thisTicket==0))
   {
      UpdateLastRefreshTicket(ctx, thisTicket);
      ctx.Items[PageRefreshEntry] = false;
   }
   else
      ctx.Items[PageRefreshEntry] = true;
}

The names of the hidden field and the session slots are set as public constants in the RefreshAction class and can be used from outside the class.

How can an application page take advantage of this machinery? When detecting a page refresh is really useful and helpful? The HTTP module doesn't block any request; it simply adds more information for the final ASP.NET page to deal with the request. This extra information includes a boolean value to denote a page refresh.

Consume the Page Refresh Event

There are a few actions that users of Web pages execute commonly and, to some extent, in a lighthearted way. These actions include Back, Forward, Stop, and Refresh. These actions, however, constitute a sort of standard toolkit for Internet navigators. Intercepting and perhaps subclassing any of these actions may result in a sort of "limitation" of generally accepted Internet practices. The impact on users might not be that positive.

On the other hand, when the user refreshes the current page, or moves back to a previously visited page, he or she submits a previously processed request to the server that might potentially break the consistency of the application state. The impact on the application, in this case, might not be that positive, too.

Imagine the following scenario:

You display data through a DataGrid and provide each row with a button for users to delete the represented row of data. Although pretty common in practice (raise your hand, those of you who have it implemented in your current application), this approach is doubly dangerous. Users can easily click the wrong button by mistake, thus breaking the data consistency, and if they refresh the page after a deletion (no matter whether it was a mindful or accidental one) chances are that a second row is deleted.

When you refresh the page, the browser simply repeats the last post. From the perspective of the ASP.NET runtime, that is merely a new request to service. The ASP.NET runtime has no way to distinguish between a regular request and an accidentally repeated one. If you work in a disconnected manner, and delete records by position on an in-memory DataSet, the likelihood that you delete one record too many is high. Even more likely is adding extra records by refreshing the page, in the case of the latest operation ending with an INSERT.

These examples clearly exploit some arguable design issues, yet they represent fully realistic scenarios. What, then, is the best way to block a page refresh?

The machinery discussed earlier in the article preprocesses the request and determines if the page is being refreshed. This information is pipelined down to the page handler through the HttpContext object. In the page, developers can retrieve this data using the following code.

bool isRefresh = (bool) HttpContext.Current.Items["IsPageRefreshed"];

Better yet, if you use a custom and more specific Page class you can wrap it into an easier to use property—the IsPageRefresh property.

public bool IsPageRefresh {
  get {
    object o = 
      HttpContext.Current.Items[RefreshAction.PageRefreshEntry];
    if (o == null)
       return false;
    return (bool) o; 
  }
}

By making your Page class inherit from a new and richer base class (Msdn.Page in this example), you can exploit the new property to know about the real origin of the request. Here's an example of how to implement a critical operation that shouldn't be repeated on page refreshes.

void AddContactButton_Click(object sender, EventArgs e) {
   if (!IsPageRefresh)
      AddContact(FName.Text, LName.Text);
   BindData();
   TrackRefreshState();
}

The new contact is added only if the page is not refreshing; that is, it is added only if the user regularly clicked on the Add-Contact push button. What's the role of the rather weird TrackRefreshState method in the code snippet above?

The method updates the ticket counter and makes sure the new page response contains the hidden field with the up-to-date ticket. In this example, the next ticket is obtained by increasing a value stored in the session state by one. (The use of the session state here is totally arbitrary and can be better replaced with a more extensible provider model, like that in ASP.NET 2.0.)

However, there's one key aspect to remark about TrackRefreshState (whose name has deliberately been chosen to recall the more familiar TrackViewState method). By calling the method, among other things you add the hidden field with the current request ticket to the page response. Without the hidden field (see Figure 1), the refresh machinery has no way to detect if the next postback is a refresh or a submit. In other words, by calling TrackRefreshState in a postback event handler, you tell the system that you want to track that operation (and only that) for page refreshes. In this way, you track only the page refreshes that are potentially harmful, and not all of them that may happen in the session lifetime.

To take advantage of the page refresh capability, just add a new page to your Microsoft Visual Studio .NET project, open the code-behind file, and change the base class of the page to Msdn.Page. Next, place a call to TrackRefreshState—a new public method on the Msdn.Page class—wherever you execute an operation that shouldn't be refreshed. Use the new boolean property IsPageRefresh to check the refresh state.

Entertain Users During Lengthy Operations

Several articles and conference talks have already provided the community with various solutions concerning how to track a particularly time-consuming operation over the Web. By "time consuming operation" I mean all those operations that, in a Windows Forms scenario, typically require a progress bar. A progress bar in a Web page is highly problematic. The progress bar should reasonably be able to come and go to the server to read any information useful to update the ticks. In addition, this should not be done through a postback or a refresh meta-tag so as to avoid the full refresh of the page. In any case, strong Dynamic HTML support is required.

A simpler way to entertain users while a lengthy operation is in course is to display an intermediate feedback page with a wait message or, better yet, a little animation. This page is clearly context-insensitive, but definitely more helpful than an hourglass on a blank and way too slow to load new page.

A simple but effective approach to displaying some feedback while a lengthy operation completes can be summarized in the following steps:

  • Redirect the user to the feedback page once he or she has clicked to start the task. The feedback page must know the URL of the page that will actually perform the task. This URL can either be passed on the query string or placed in an accessible data store, including the session state.
  • The feedback page starts loading and then redirects to the work page. In this case, the redirection is accomplished by the script in the page's onload Javascript event. The browser loads and displays the feedback page and then points to the work page. The page performs its lengthy task while the feedback page is displayed to the user.
  • The feedback page can be as complex and UI-rich as needed. It can contain a "Please, wait..." message, display an animated GIF, or exploit some Dynamic HTML capabilities to display anything that looks like a real progress bar.

I purposedly created a LengthyAction class to help manage the start of a lengthy task.

private const string UrlFormatString = "{0}?target={1}";
public static void Start(string feedbackPageUrl, 
  string targetPageUrl)
{
   // Prepare the URL for the feedback page
   string url = String.Format(UrlFormatString,
      feedbackPageUrl, targetPageUrl);

   // Redirect the call to the feedback page
   HttpContext.Current.Response.Redirect(url);
}

The class features only one static method—Start. The Start method takes the URL of the feedback page and the target page, namely the page that performs the task. The method combines both arguments into a single URL and redirects.

The feedback page can have any user interface you like, but it poses a couple of key requirements. The page must be able to retrieve the name of the work page and provide a possibly automatic mechanism to redirect to the work page by means of a script. I defined a custom base Page class that these capabilities are already built into. In doing so, I had to make some assumptions. In particular, my implementation assumes that the name of the work page is communicated through the query string using a well-known attribute name—target. The name of the target page is stored in a public property named TargetURL. In addition, the feedback page supplies a function named GetAutoRedirectScript. The goal of this function is to return the script code that is required to implement redirection by means of a script.

public string GetAutoRedirectScript() {
   return String.Format("location.href='{0}';", TargetUrl);
}

To keep things as simple as possible, the FeedbackBasePage class also looks for a generic HTML control named Body. This is exactly what you get out of the following markup.

<body runat="server" id="Body">  

If there's an easy way to program the body tag of the page, the FeedbackBasePage class will find it out and silently add the onload attribute; otherwise, you must add the onload attribute manually. Such an attribute is necessary for the feedback page to work.

HtmlGenericControl body = FindControl(BodyId) as HtmlGenericControl;
if (body != null)
   body.Attributes["onload"] = GetAutoRedirectScript();

The final markup code served to the browser will look like this.

<body οnlοad="location.href='lengthyop.aspx'">

Let's review the steps required to implement a lengthy operation using the classes discussed in the article.

You first reference the needed assembly and then start writing the following event handler for the click button that triggers the operation.

void ButtonLengthyOp_Click(object sender, EventArgs e) {
   LengthyAction.Start("feedback.aspx", "work.aspx");
}

Next, you add the feedback page to the project. It is a regular Web form page where you modify the <body> tag as above and change the base class to FeedbackBasePage. The user interface of the feedback will be displayed after you click to start the process and before the results are ready. See the figure below.

Figure 2. The sequence of a lengthy operation

In this example, I worked on a sort of cross-page postback which happens to be a much more common scenario for particularly lengthy operations. However, this poses the problem of transporting the view state and, in general, any parameters that the work page needs to complete its task. You can use the work page's query string to concatenate serialized versions of objects or, alternatively, store everything in the ASP.NET Cache or Session object. You can't use the HTTP context in this case because the operation spans over multiple HTTP requests, each with a different set of items.

Note that the URL of the feedback page contains some details of the call and may look like the following.

feedback.aspx?target=work.aspx?param1=123&param2=hello

To hide these details, you can define a custom HTTP handler and bind it to any fake URL you like better. The HTTP handler might retrieve any required information (including the name of the feedback and work page) from the cache or session state.

Focus to Controls

A nice new feature of ASP.NET 2.0 allows you to specify which input control should be given the focus when the page is first displayed. This is a slick feature that saves users the burden to click on, say, a textbox to start entering data.

To assign the input focus to an HTML component, you need a little piece of Javascript code. Let me say it upfront: it's not rocket science, and it is something that you can easily add as inline code in the onload attribute of the <body> tag. However, having a SetFocus method on the Page class to determine the name of the control that you want to focus on the server is definitely a great step forward. In ASP.NET 2.0, in fact, you can use following code.

void Page_Load(object sender, System.EventArgs e) {
   SetFocus("TheFirstName");
}

When the page displays, the input control named TheFirstName is given focus. Easy and effective; but how to code it in ASP.NET 1.x?

Again, tips for implementing this feature are widely known in the community and can be found with not much effort making a search on Google. The challenge is integrating it in a base Page class to make it reusable over and over again.

Let's extend the Msdn.Page base class with the following declarations.

private string m_focusedControl;
public void SetFocus(string ctlId) {
   m_focusedControl = ctlId;
}

The SetFocus method collects the ID of the control and stores it in an internal member. In the PreRender event of the page, a call is made to another helper function that builds and injects the Javascript code.

private void AddSetFocusScript()
{
   if (m_focusedControl == "")
      return;

   // Add the script to declare the function
   StringBuilder sb = new StringBuilder("");
   sb.Append("<script language=javascript>");
   sb.Append("function ");
   sb.Append(SetFocusFunctionName);
   sb.Append("(ctl) {");
   sb.Append("  if (document.forms[0][ctl] != null)");
   sb.Append("  {document.forms[0][ctl].focus();}");
   sb.Append("}");

   // Add the script to call the function
   sb.Append(SetFocusFunctionName);
   sb.Append("('");
   sb.Append(m_focusedControl);
   sb.Append("');<");
   sb.Append("/");   // break like this to avoid misunderstandings...
   sb.Append("script>");

   // Register the script (names are CASE-SENSITIVE)
   if (!IsStartupScriptRegistered(SetFocusScriptName)) 
      RegisterStartupScript(SetFocusScriptName, sb.ToString());
}

The Javascript code is constructed like a dynamic string and is accumulated in the StringBuilder object. The next step is adding that string to the page output. In ASP.NET, to add some client script code to the page, you must register it with a particular page-level collection. For this reason, the Page class makes a few RegisterXxx methods available. Each RegisterXxx method adds the block of Javascript code to a different collection, for insertion in a different point within the final page markup. For example, RegisterStartupScript injects the code just before the closing tag of the form. RegisterClientScriptBlock, instead, emits the script code just after the opening tag of the form. It is important that you include in the script both tags of the <script> element. Each script block is identified with a key, so that multiple server controls can use the same script block without it being emitted to the output stream twice or more.

The following block of Javascript code is inserted in the page just before the closing tag of the form. In this way, it runs at startup immediately after initialization.

<form>
:
<script language=javascript>
function __setFocus(ctl) {  
   if (document.forms[0][ctl] != null) {
       document.forms[0][ctl].focus();
   }
}
__setFocus('TheFirstName');
</script>
</form>

Using the SetFocus public method on the Msdn.Page class, you can decide, at any point in the page's code, which control will receive the input focus when the page is displayed in the browser. More importantly, you can base this decision on runtime conditions and/or postback events.

Conclusion

One of the key advantages of an object-oriented technology like ASP.NET is that you can use inheritance extensively. You can create new, customized server controls with a minimum effort by inheriting and enhancing the public interface of existing controls. In derived classes, you can override virtual methods, thus changing the internal behavior of the component. Applying these object-oriented programming (OOP) principles to controls seems to be pretty natural and common; I wouldn't say the same for the classes that represent an ASP.NET page.

Nevertheless, page inheritance is widely used to build an executable representation of each requested .ASPX page. Code-behind pages are just pages that inherit from the base System.Web.UI.Page class. Why don't you define an intermediate Page class that provides a richer bedrock to your application-specific pages?

This is just the challenge of the article. I've taken three popular features that many developers tried more or less successfully to implement—trapping the Refresh key, controlling lengthy operations, and assigning the input focus to controls—and packed them all into the context of a single, all-encompassing Page class.

The new Page class—the Msdn.Page class—used instead of the base Page class in code-behind and code-inline applications supplies more basic features to developers, and in an easy and reusable way. A richer base Page class is a milestone in the building of a more solid platform for ASP.NET applications. All pages of a serious applications should be built from a custom and made to measure Page class.

Related Books

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值