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.
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.
select, etc.) and different input types (e.g.
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.
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
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
In order to prevent IE from firing the first
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
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
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.
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
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
.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
- Copy DirtyPageReminder.dll to your project's bin folder.
- Drag and drop the control onto your page. Set optional
- Move navigation-trigging controls out of UpdatePanels, or register them as full postback triggers.
- Register "Save & Navigate" controls and non-navigating full postback triggers to skip dirtiness checking with the
- If there is a "Save & Stay" button or something similar on the page, call
MarkPageClean() method after data is committed to database.
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.
Please login to rate or to leave a comment.