Published: 05 May 2010
By: Jing Ding
Download Sample Code

Use a DirtyPageReminder to alert users for unsaved changes when they navigate away from the current pages or close the browsers.

Contents [hide]

Introduction

In one of my recent projects, I was asked to remind users for unsaved changes when they leave a page. I did a web search, and found some useful links. Among the most notables were ELC's DirtyForm jQuery plug-in (link) and Scott Mitchell's ASP.NET series (link1, link2, and link3). However, neither met my requirements completely. Therefore, I came up with my own solution, integrating useful pieces from the above two approaches.

Design Decisions

Before writing a single line of code, I had to make some design decisions: when, how and what.

When to alert

When is the best time to alert users if they are trying to leave a page with unsaved data? DirtyForm uses an active approach. It attaches event handlers to known navigation triggers within a page. The handler is executed early in a navigation event's life cycle, right after a trigger is clicked. An alternative passive approach, used in Scott's series, waits patiently late into the life cycle until a single point of exit is reached. That is when the browser starts to unload the page, and raises the window.onbeforeunload event. The event is also raised for navigations originated from outside a page, such as bookmarks, browser's back button, or even closing the browser. The passive alert seems a simpler solution. But it is actually more challenging to implement. Even though supported in most modern browsers, the window.onbeforeunload event is not a standard. Hence, browser compatibility is a big issue. It is the ability to handle outside-page navigations that made the passive alert an easy decision.

How to find data-entry fields

In order to determine whether there are any unsaved changes on a page, you must first find all the data-entry fields. In Scott's series, an ID-based approach is used. Data-entry controls' ClientIDs are registered in an array. The IDs are later used in getElementById() to identified the fields. This means a lot of extra work for page developers. They have to write registration code for every single data-entry control. They also have to keep the registration and the underlying controls synchronized. Whenever data controls are added, removed, or modified, the corresponding registration has to be modified too. An alternative approach, used in DirtyForm, scans the entire page and filters the elements by their tag names and types. Although there are different tags (e.g. input, textarea, select, etc.) and different input types (e.g. text, check, radio, submit, etc.) for data-entry elements, jQuery makes it one line of code:

The one-liner made the decision a no-brainer.

What type of changes to be monitored

Any change or true change? The any-change approach attaches handlers to all data-entry elements' change events, setting a global dirty flag whenever any change happens. This will mark a page as dirty and trigger a false alert even if a user reverts changes back to their original values. The true-change approach remembers all elements' initial values at page load, and compares their current values against the initial values at the time when a navigation is triggered. Even though more coding is required, the latter seems a better solution, because it can avoid the false alerts. Both DirtyForm and Scott's series used the true-change approach. In addition, jQuery's .val() method simplifies reading values from different types of elements, making it an even easier decision.

Implementation Issues

A step-by-step instruction of how to implement a server control is out of the scope of this article. I would like to cover the issues I deemed original. That is, I could not find them on the web, or I had to combine what I found from several websites.

Strange onbeforeunload behavior in IE

As mentioned in the previous section, onbeforeunload is not a standard window event. Browsers are free to interpret its behavior. In Firefox, it behaves as most would expect: the event is only raised once right before the browser starts to unload a page. In IE, however, its behavior is sometimes unexpected: it is raised twice on LinkButtons! This is because IE makes a false assumption: any anchor element with href value not starting with "#" is a redirecting link. When the link is clicked, IE raises the event before the actual URL is evaluated. This is not an issue for true links, but causes a problem on LinkButtons, whose href attributes are javascript:__doPostBack(). IE fires the event immediately when a LinkButton is clicked. After evaluating the href, however, IE realizes that it is not a true link, hence cancels the unload process, and starts the postback. If the postback initiates a redirect at server side, IE will re-start the unload precess and raise a true onbeforeunload event.

In order to prevent IE from firing the first onbeforeunload event on a LinkButton, its href value has to be changed to something starting with "#". The javascript code (postback call) can be moved to onclick handler. If the LinkButton already has its own onclick handler, for example, "return confirm(‘Are you sure?’)", execute the original handler first. Then run the postback call only if the old handler doesn't return false. Here is the deHref function implementing the above idea:

There is one thing not mentioned above. The postback call using eval() is wrapped in a try…catch block without any error handling code. This leads us to the next implementation issue.

“Unspecified error” in IE after navigation canceled

With modified LinkButtons, IE raises onbeforeunload event only once as expected. If users opt to cancel the navigation and stay on the page, IE throws an javascript "Unspecified error". Depending on browser settings, the error may popup a dialog, or display an error message in browser's status bar. Annoying as it is, the error actually seems not break anything on the page. Users can continue their workflow if they ignore the error. However, most users will be freaked out when the error pops up, let along continue their work.

The error message, "Unspecified error", provides little information in debuging the issue. It is similar to the NullReferenceException. The exception itself is useless for debuging. You have to pinpoint the line that throws the exception, and then formulate a fix or work-around. The above "Unspecified error" is thrown at eval()if not wrapped in a try…catch block. Scott Mitchell gave more details about the issue, and proposed the try…catch trick.

There is another "Unspecified error" thrown when a navigation is triggered and canceled from within an UpdatePanel. The line throwing the error is highlighted below. It is within ASP.NET Ajax extension client-side framework. Obviously, the framework is waiting for a server response to complete an asynchronous request; and the cancellation is not one of what it is expecting. It is interesting to notice that the line is inside a try block without catch statement. Otherwise, the try…catch trick would have also worked here. Is the omission of catch statement by design, or a bug? Since I cannot modify source code of the framework, I have to find another workaround. Moving the navigation triggers out of the UpdatePanel or registering them as full postback triggers should work. This is, however, not the only issue that Ajax makes the implementation tricky.

Ajax issues

The ASP.NET Ajax extension framework makes page developers' life a lot easier, at the expense of control developers' life. Developing an Ajax-friendly server control is much more trickier, especially if the control has a lot to do at client-side. The trickiness arises from the difference inside and outside of UpdatePanels, as well as the difference between partial and full postbacks. More specifically, elements inside UpdatePanels lose their properties and behaviors acquired at client-side on partial postbacks; and full postbacks wipe them out on the entire page. Therefore, control developers must pay extra caution about the availability of their controls' client-side properties and behaviors under various environments (inside or outside UpdatePanels, on partial or full postbacks, etc.). In this case, it is to make sure that,

  • LinkButtons' hrefs are kept modified, and
  • Data-entry controls' initial values are saved in a safe place to survive postbacks.

1. Keeping LinkButtons' href modified

Some LinkButtons may restore their href attributes back to javascript:__doPostBack() on postbacks. So the previously mentioned deHref() function should be called on every postback. If there is a ScriptManager on the page, call the function on Sys.Application.load event, which is raised on both partial and full postbacks. Otherwise, call the function on jQuery's document.ready event.

2. Safe-keeping initial values

jQuery's .data() method makes it very easy to attach data to elements. This is exactly what DirtyForm does, attaching a data-entry element's initial value to itself. However, the elements inside UpdatePanels will lose their attached data on partial postbacks. A safe place to survive partial postbacks is the form element, because it is outside all UpdatePanels. But the form element is still not safe enough for full postbacks. In fact, there is no safe place at client-side. In order to be kept across full postbacks, the initial values have to be submitted to the server, and later sent back to the client. That's quite some overhead in network traffic, as well as in serialization and deserialization of the initial values at client-side. To avoid the overhead, full postback support is turned off by default. The initial values are stored in an array on initial page load, and kept at client-side. The array is serialized to a hidden field on initial page load, and deserialized from the hidden field on postbacks only if the support is explicitly turned on. Here is the complete DirtyPage.js.

Listing 1: DirtyPage.js

How to Use

I have wrapped the above mentioned tricks and treats into a server control, DirtyPageReminder. So you don't have to write a single line of javascript. Below are the simple steps to use the control.

  1. Copy DirtyPageReminder.dll to your project's bin folder.
  2. Drag and drop the control onto your page. Set optional AlertMessage and SupportFullPostback properties.
  3. Move navigation-trigging controls out of UpdatePanels, or register them as full postback triggers.
  4. Register "Save & Navigate" controls and non-navigating full postback triggers to skip dirtiness checking with the RegisterSkipCheckingTrigger() method.
  5. If there is a "Save & Stay" button or something similar on the page, call MarkPageClean() method after data is committed to database.

Summary

In this article, I presented a server control, DirtyPageReminder, for reminding users of unsaved data on pages. I also detailed several issues in its design and implementation, such as onbeforeunload in IE and Ajax-friendliness, which might be useful in developing other server controls.

<<  Previous Article Continue reading and see our next or previous articles Next Article >>

About Jing Ding

Jing Ding is a Sr. Systems Consultant at the Ohio State University Medical Center. He is an Iron Speed MVP. His native language is C#.

This author has published 6 articles on DotNetSlackers. View other articles or the complete profile here.

Other articles in this category


Code First Approach using Entity Framework 4.1, Inversion of Control, Unity Framework, Repository and Unit of Work Patterns, and MVC3 Razor View
A detailed introduction about the code first approach using Entity Framework 4.1, Inversion of Contr...
jQuery Mobile ListView
In this article, we're going to look at what JQuery Mobile uses to represent lists, and how capable ...
Exception Handling and .Net (A practical approach)
Error Handling has always been crucial for an application in a number of ways. It may affect the exe...
JQuery Mobile Widgets Overview
An overview of widgets in jQuery Mobile.
Book Review: SignalR: Real-time Application Development
A book review of SignalR by Simone.
Top
 
 
 

Discussion


Subject Author Date
placeholder Dirty Page Reminder Lonnie Thomas 5/5/2010 5:25 AM
RE: Dirty Page Reminder Jing Ding 5/5/2010 8:08 AM
placeholder Setup For Master / Content Pages Neal Landeau 6/7/2010 7:06 PM
RE: Setup For Master / Content Pages Jing Ding 6/7/2010 9:43 PM
placeholder Gridview with pagination pearl jem 7/12/2012 12:28 AM
RE: Gridview with pagination Jing Ding 7/12/2012 7:27 AM
placeholder AJAX Calendar Control Tim Chimento 10/23/2013 1:20 PM
Radio Buttons and Check Boxes Tim Chimento 10/24/2013 12:08 PM

Please login to rate or to leave a comment.