Download The Source Code for this article.
Contents
BUILD A RICHER BEDROCKTRAP The Browser Refreshconsume The Page Refresh EventEncertain Uses During length Operationsfocus to ControlsconClusion
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 Great Simplifies The Building Of Application Specific Pages with customs and media.
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 inherit from a non-default page class, you just change the base type, as flollows.
Public Class Webform1: msdn.page
{
:
}
If the page is create in @Page Directive.
<% @Page inherits = "msdn.page" ...%>
You Chan Change The Base Type of ASNET PAGES ON A Per-page Basis as discussed here, or using the
LET's See How Each of the Previously Listed Features IS IMPLEMENTED IN PractICE AND ENCAPSULATED IN ALL-ENCOMPAMPASING 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 does not 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 Reissue 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 does not 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: httpmodule {
// httpmodule :: init
Public void init (HTTPApplication APP)
{
// register for pipeline events
app.acquirerequestState =
New EventHandler (onacquireRequestState);
}
// httpmodule :: 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 contexttpApplication app = (httpapplication) Sender;
HTTPCONText CTX = app.context;
// Check F5 Action
Refreshction.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 THANTICKET = GetCurrentRefreshticket (CTX);
// compare tickets
IF (Thisticket> Lastticket ||
(Thisticket == lastticket && thinkicket == 0))))
{
UpdateLastRefreshticket (CTX, THisticket);
CTX.Items [PageRefreshenTry] = false;
}
Else
CTX.Items [PageRefreshenTry] = TRUE;
}
................
How can an application page take advantage of this machinery When detecting a page refresh is really useful and helpful The HTTP module does not 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 Explloit Some Arguable Design Issues, Yet They Reperest Fully Realistic Scenarios. What, 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 ["ispagerefreeshed"];
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 [refreshction.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 used 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 what 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 ............................ ...
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 should not 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 Length 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 Length 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.
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 Browser Will Look.
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) {
Lengththyaction.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
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.Figure 2. The sequence of a length optY 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 not 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
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");
}
.
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 folload deflarations.
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 ("");