Table of contents
This is the second in a series of articles on a class library for ASP.NET applications that I have developed. It contains a set of common, reusable page classes that can be utilized in web applications as-is to provide a consistent look, feel, and set of features. New classes can also be derived from them to extend their capabilities. The features are all fairly modular, and may be extracted and placed into your own classes too. For a complete list of articles in the series along with a demonstration application and the code for the classes, see Part 1 [^].
This article describes the data change checking features of the BasePage
class. This is a fairly large section, so it was split out from the first article to keep it from getting too long. It covers the properties and methods related to the change checking features of the BasePage
class as well as the client-side code used to implement it.
In a rich-client application, it is fairly easy to detect changes in data entry controls and warn the user that they are about to lose their changes when they attempt to leave the form. You also have full control over how they can leave the form or the application. In a web-based application, it is more difficult. You have some chance of catching the attempt to leave if they click a link or button on your page, but they may also leave by entering a new URL in the browser's address text box, they may navigate away by using the back, forward, or history links, or they may just close the browser. It can be quite irritating for the end-user to navigate away from the page only to realize that they did not save their changes first. Telling them "Then don't do that" just makes them cranky.
The BasePage
class and those derived from it provide a set of properties that will allow you to trap most attempts to leave the Web Form before the user has saved their changes, and ask them whether or not they really want to leave. When enabled, it will also track the dirty state of the form automatically. While it is not 100% effective, it does a pretty good job in most situations. With that said, here are the limitations:
- This only works in Internet Explorer 4+ as it relies on the
OnBeforeUnload
event on the browser's window object. For non-IE browsers, the only support provided is dirty state tracking via the BasePage.Dirty
property. It has only been tested in IE, FireFox, and Netscape so other browsers may still have issues not addressed here.
- If you decide to leave anyway, you may get a double prompt about trying to leave without saving, in some cases. This usually occurs on links in
DataGrid
web controls (i.e., an "Add" command link button). This is more of an annoyance than anything.
- It will exit without prompting if the active element is one you do not want to get prompted on (a Save button, for example), but it has the focus and you leave by doing something that does not interact with the page such as closing the browser, clicking the back button, etc. This is a rare case, but it can happen.
- It may exit without prompting if a control has
AutoPostBack
enabled, and you leave the page without modifying any controls on the page after an auto-postback has occurred. This problem can be circumvented by the use of the BasePage.Dirty
property. When BasePage.CheckForDataChanges
is set to true
, the client-side code will detect changes and the page will automatically track the dirty state for you. You can also explicitly set it to true
in cases where no changes may have occurred in the client-side so that the user will get prompted if they attempt to leave the page later on (i.e., a button is clicked to fill in default values on a new record).
- It only looks at the standard HTML controls on the page (text box, text area, checkbox, radio button, and select controls such as dropdown lists and list boxes). Since almost all of the ASP.NET web controls render as one of the standard HTML controls, this does not present any problems. However, if you use any ActiveX controls on the web page for data entry, they will not be checked.
The BasePage
class contains five properties that work together with a client-side JavaScript function to provide data change checking:
Property |
Description |
CheckForDataChanges |
Set this Boolean property to true to enable data change checking. Set it to false (the default) to disable it. When enabled, the page will render a hidden field, some extra JavaScript variables, and a function to check for changes. The function is bound to the window object's OnBeforeUnload event when the page loads. It is also registered as an OnSubmit statement to allow automatic tracking of the dirty state. Changes to the BP_bIsDirty hidden field cannot occur in the OnBeforeUnload event so they must be made in the OnSubmit event instead. |
Dirty |
This is a Boolean property that you can set to true to force the page to always prompt the user about saving the changes prior to leaving. This is most useful in cases where a postback has occurred to modify the state of the form (i.e., to disable controls or load different values based on a selection in a dropdown list). It will automatically get set to true by the client-side code if changes have been made to the form controls. The CheckForDataChanges property must be set to true in order to use this property. The prompting behavior is only available in Internet Explorer. For all other browsers, it only tracks the dirty state. Do not forget to set this property to false after saving or canceling edits to the page. |
ConfirmLeaveMessage |
This string property contains the message that will be displayed when the user attempts to leave without first saving their changes (Internet Explorer only). If not set, a default message is displayed. |
BypassPromptIds |
This property is set to a string array of control IDs that should not cause prompting even if changes occur (i.e., a Save or Cancel button). When postback occurs due to a control with one of these IDs, no prompting will occur even if changes have been made (Internet Explorer only). The dirty flag will still get updated. If not set, all controls that cause a postback will result in prompting if data has been changed. |
SkipDataCheckIds |
This property is set to a string array of control IDs that should be ignored when checking for changed data. For example, you may have a read-only text box used to display text or messages that gets updated during the course of using the form. Since it is not part of the saved information, it can be ignored and will not prevent leaving the page if it is the only thing that changed. If not set, all data entry controls are checked for changes. |
Change checking is usually enabled in the Page_Load
event the first time the page is loaded. The CheckForDataChanges
property is set to true
and the BypassPromptIds
property is set to a list of controls that should not cause prompting to occur (i.e., a Save button, dropdown lists with AutoPostBack
enabled, etc.). For example:
private void Page_Load(object sender, System.EventArgs e)
{
if(Page.IsPostBack == false)
{
this.CheckForDataChanges = true;
this.BypassPromptIds =
new string[] { "btnSave", "btnCancel",
"chkLimitToTeam" }
}
}
If specifying the IDs of such controls as the sort links in data grid headers, you will need to run the page, view the source, and get the names of the link controls from the rendered HTML. Be sure to change the '$' characters in the names to ':' when specifying them in the bypass list, as the __doPostback()
function changes them before the data changing checking code gets the ID.
Changes in the data controls can only be detected from the point at which the user starts interacting with the page up to the point at which a postback occurs. If you have controls on the page that cause a postback, such as a button that alters the state of some controls but it does not actually save changes made up to that point, you may need to make use of the Dirty
property. This is needed because once the page is rendered after the postback, it has no idea what the original values were prior to that. Setting the Dirty
property to true
in the event handler for such postbacks will ensure that the user is prompted to save their changes before leaving the page. All it does is set a flag that the JavaScript function checks and, if true, always prompts the user to save their changes regardless of whether or not anything has actually been changed since the page was rendered.
The rendering of the client-side variables and script occurs in the overridden OnPreRender()
method. The code is fairly straightforward, and simply uses a StringBuilder
object to format the variables and script code and then registers it with the page. The script is stored as a resource in the assembly so that you do not have to distribute it separately along with the assembly. Also, to insure that the correct form is affected by the code, a variable is rendered that contains the form's unique ID. While ASP.NET does limit you to one form with a runat='server'
attribute, it will let you have other regular HTML forms on the page without that attribute. By enforcing use of the web form's client-side name, it will not break any pages that may utilize other regular HTML forms. The code below is for the .NET 1.1 version of the class. The .NET 2.0 version varies only slightly with its use of the Page
object's new methods for registering the script blocks, submit statement, and the hidden field:
protected override void OnPreRender(EventArgs e)
{
StringBuilder sb;
string[] idList;
base.OnPreRender(e);
if(this.CheckForDataChanges == true)
{
this.RegisterHiddenField("BP_bIsDirty",
this.Dirty.ToString(
CultureInfo.InvariantCulture).ToLower(
CultureInfo.InvariantCulture));
this.RegisterOnSubmitStatement("BP_DirtyCheck",
"BP_funCheckForChanges(true);");
sb = new StringBuilder(
"<script type='text/javascript'>\n<!--\n" +
"var BP_arrBypassList = new Array(", 4096);
idList = this.BypassPromptIds;
if(idList != null)
{
sb.Append('\"');
sb.Append(String.Join("\",\"", idList));
sb.Append('\"');
}
sb.Append(");\nvar BP_arrSkipList = new Array(");
idList = this.SkipDataCheckIds;
if(idList != null)
{
sb.Append('\"');
sb.Append(String.Join("\",\"", idList));
sb.Append('\"');
}
sb.Append(");\nvar BP_strDataLossMsg = \"");
sb.Append(this.ConfirmLeaveMessage);
sb.Append("\";\nvar BP_strFormName = \"");
sb.Append(this.PageForm.UniqueID);
sb.Append("\";\n//-->\n</script>\n");
sb.Append("<script type='text/javascript' src='");
sb.Append(ResSrvHandler.ResSrvHandlerPageName);
sb.Append("?Res=DataChange.js'></script>");
this.RegisterClientScriptBlock("BP_DCCJS",
sb.ToString());
}
...
}
The OnInit
method is overridden to retrieve the value of the hidden field when the page is created on the server. If change checking is enabled, it will set the Dirty
property to the value of the hidden field as determined on the client during postback. If change checking is disabled, it will always be set to false
, as the field will not exist:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
this.Dirty = Convert.ToBoolean(Request.Form["BP_bIsDirty"],
CultureInfo.InvariantCulture);
...
}
The JavaScript code is what makes it all work on the client side. When rendered, it consists of a hidden field that is set to the current value of the Dirty
property, a set of variables that are populated with the values from the ConfirmLeaveMessage
, BypassPromptIds
, and SkipDataCheckIds
properties, and the JavaScript functions BP_funCheckForChanges()
and BP_OnSubmit()
. Inline JavaScript code at the end of the script binds the change checking function to the window object's OnBeforeUnload
event.
The change checking function is also registered as part of the form's OnSubmit
attribute to allow it to detect changes and set the hidden field to true
. The client-side code module also substitutes the BP_OnSubmit()
function as the web form's real Submit
method. The original handler is saved as a new BP_RealOnSubmit
method on the form. The function will call the original handler after calling the change checking function to update the dirty state flag. The reason for this extra work is that controls with AutoPostBack
enabled, such as dropdown lists, call the form's Submit
method directly and it does not fire the OnSubmit
event. By substituting our own handler, we can still track changes to the form data in these cases:
document.forms.BP_RealOnSubmit =
document.forms.submit;
function BP_OnSubmit()
{
BP_funCheckForChanges(true);
try {
document.forms.BP_RealOnSubmit();
} catch(e) { }
}
document.forms.submit = BP_OnSubmit;
window.onbeforeunload = BP_funCheckForChanges;
When the user tries to leave the page, the change checking function is called and the following steps occur:
function BP_funCheckForChanges(bInOnSubmit)
{
var strID, nIdx, nNumOpts, nSkipCnt, nPos, nOptIdx;
var oElem, oOptions, oOpt, nDefSelIdx, nSelIdx;
var oForm = document.forms;
var nElemCnt = oForm.elements.length;
var bPrompt = (typeof(bInOnSubmit) == "undefined");
var ctlDirty = document.getElementsByName("BP_bIsDirty")[0];
if(bPrompt == true && BP_bOnBeforeUnloadFired == true)
return;
var bChanged = (ctlDirty.value == "true");
The first part initializes several variables for the function. The bPrompt
variable is initially set to true
or false
based on whether or not a parameter was passed to the function. When passed a parameter, it has been called as part of the OnSubmit
event and should never prompt, as we are only interested in updating the state of the dirty flag. When called as part of the OnBeforeUnload
event, it should prompt unless told not to later on because of something in the bypass ID list. The reason for the separate calls is that the OnBeforeUnload
event occurs after everything has been packaged up ready for sending to the server, and thus changes to the hidden field do not get sent back if made in that event. A side effect of this is that for non-IE browsers, you at least get dirty state tracking. The bChanged
variable is set to the current value of the BP_bIsDirty
hidden field. This allows it to maintain the dirty state and prompt to save changes even after a postback in which nothing else has changed. Note that in order to get a reference to the hidden field, we have to use the document.getElementsByName
method. This is because the page renders the registered hidden fields with a name
attribute but without an id
attribute. Using the "ByName" method insures that the code will work on non-IE browsers:
strID = "";
oElem = document.getElementById("__EVENTTARGET");
if(oElem == null || typeof(oElem) == "undefined" ||
oElem.value == "")
{
if(typeof(document.activeElement) != "undefined")
{
oElem = document.activeElement
strID = oElem.id;
}
}
else
strID = oElem.value;
if(strID == "" && oElem != null &&
typeof(oElem) != "undefined")
{
if(oElem.tagName == "A" &&
oElem.href.indexOf("__doPostBack") != -1)
return;
if(typeof(oElem.parentElement) != "undefined")
strID = oElem.parentElement.id;
}
if(strID != "")
{
nSkipCnt = BP_arrBypassList.length;
for(nIdx = 0; nIdx < nSkipCnt; nIdx++)
if(strID == BP_arrBypassList[nIdx])
bPrompt = false;
else
{
nPos = strID.length - BP_arrBypassList[nIdx].length;
if(nPos >= 0)
if(strID.substr(nPos) == BP_arrBypassList[nIdx])
bPrompt = false;
}
}
For Internet Explorer only, the event target element (the one that caused the postback) is checked to see if its control ID is in the bypass list. If so, the bPrompt
flag is set to false
so that no prompting occurs. For example, you do not want it to prompt the user to save their changes when the Save button is clicked. When checking the control ID, it looks for an exact match or one that ends in the ID from the list. The "ends with" match is there to handle controls embedded in DataGrid
web controls. Their client-side IDs are altered based on the row they appear in to keep them unique. Since we do not know the unique ID, the partial match based on the ID assigned at design time will work.
nSkipCnt = BP_arrSkipList.length;
for(nIdx = 0; !bChanged && nIdx < nElemCnt; nIdx++)
{
oElem = oForm.elements[nIdx];
for(nOptIdx = 0; nOptIdx < nSkipCnt; nOptIdx++)
{
if(oElem.id == BP_arrSkipList[nOptIdx])
break;
nPos = oElem.id.length - BP_arrSkipList[nOptIdx].length;
if(nPos >= 0)
if(oElem.id.substr(nPos) == BP_arrSkipList[nOptIdx])
break;
}
if(nOptIdx < nSkipCnt)
continue;
if(oElem.type == "text" || oElem.tagName == "TEXTAREA")
{
if(oElem.value != oElem.defaultValue)
bChanged = true;
}
else
if(oElem.type == "checkbox" || oElem.type == "radio")
{
if(oElem.checked != oElem.defaultChecked)
bChanged = true;
}
else
if(oElem.tagName == "SELECT")
{
oOptions = oElem.options;
nNumOpts = oOptions.length;
nDefSelIdx = nSelIdx = 0;
for(nOptIdx = 0; nOptIdx < nNumOpts; nOptIdx++)
{
oOpt = oOptions[nOptIdx];
if(oOpt.defaultSelected)
nDefSelIdx = nOptIdx;
if(oOpt.selected)
nSelIdx = nOptIdx;
}
if(nDefSelIdx != nSelIdx)
bChanged = true;
}
}
Next, each control is checked to see if the data it contains has been changed. If the control ID is in the list of elements to skip, it will be ignored. This allows you to have controls on the page that can be modified without causing it to prompt to save changes (i.e. message text areas etc.). As with the bypass list, the control ID can be an exact match or one ending in the specified ID.
Changes are detected based on the control type. For text boxes and text areas, the value
property is compared to the defaultValue
property. For checkboxes and radio buttons, the checked
property is compared to the defaultChecked
property. For select
controls (dropdown lists and list boxes), each item in the collection is scanned. The current selection is found and compared to the item that was marked as the default selection to see if there was a change. If no item in the list was marked as the default, the first element is considered to be the default.
if(bChanged)
{
ctlDirty.value = "true";
if(bPrompt)
{
event.returnValue = BP_strDataLossMsg;
BP_bOnBeforeUnloadFired = true;
window.setTimeout("BP_funClearIfCancelled()", 1000);
}
}
If no changes were found and the Page
class's dirty flag is still false
, the function exits and no prompting occurs. No prompting occurs if called as part of the OnSubmit
event either.
For Internet Explorer, if a change was detected, or the page class' dirty flag was set to true
and it was called as part of the OnBeforeUnload
event, the event.returnValue
property is set to the confirmation message. This causes the browser to pop up a message box asking whether or not it is okay to leave the page. The confirmation message is displayed in the message box preceded by the question "Are you sure you want to navigate away from this page?" and followed by instructions to click OK to continue or Cancel to stay on the current page. Those two messages are added by the browser and cannot be changed. Only the text that you supply that appears in the middle can be modified via the ConfirmLeaveMessage
property.
In addition to setting the message, we also set a flag variable (BP_bOnBeforeUnloadFired
) that prevents a double prompt that would normally occur under certain conditions. It also sets up a time out event that calls the BP_funClearIfCancelled
function. This serves two purposes if post-back is cancelled. It clears the flag that prevents the double prompt, and it also clears __EVENTTARGET
as it doesn't get cleared if you cancel an auto-postback item and then click a button, for example. If not cleared, it could cause unexpected behavior in the server side code in some cases where that variable is used.
I have used the BasePage
class and a few derived from it in all of my ASP.NET applications, to give them a consistent look, feel, and set of features. The data change checking features, though full use is limited to Internet Explorer, have been extremely helpful in eliminating a common user complaint about losing data due to forgetting to save changes before leaving a page. Hopefully, you will find this class and the others in the library, or parts of them, as useful as I have.
- 04/02/2006
- Breaking changes: Property and method names have been modified to conform to the .NET naming conventions with regard to casing (
BasePage.BypassPromptIds
and BasePage.SkipDataCheckIds
).
- 11/26/2004
Changes in this release:
- Made some changes based on suggestions from Danny Dot Net to prevent the double prompt when posting back via a hyperlink-type control. A few other changes were made that also eliminate the double prompt in almost all other situations as well.
- Fixed up a potential problem with the
__EVENTTARGET
variable not being cleared if post back is cancelled.
- 12/01/2003