Using ASP.NET to Prompt a User to Save When Leaving a Page
By Scott Mitchell
Introduction
Last week I wrote an article titled Prompting a User to Save When Leaving a Page,
which looked at how to use the client-side onbeforeunload event to display a confirmation messagebox when a
user attempted to leave a data-entry page after having modified the data's contents without explicitly saving the data.
To summarize last week's article, adding such a feature required the following steps:
Writing code that saves the initial values of the page's input form fields in a client-side array.
Creating an event handler for the onbeforeunload event that checks to see if the input form fields'
values differ from those in the array. If they do, then a string is returned, prompting the user if they really
want to leave. Otherwise, no string is returned, and the user can leave the page as they normally would (i.e., without
being prompted).
Finally, for buttons or other HTML elements that could cause the user to leave the page but should not require
that the user be prompted, a client-side onclick event handler was used to set a flag that indicated that
the onbeforeunload event handler didn't need to check for changes. This is typically used with a "Save" button
that, when clicked, causes a postback, but whose posting back should not prompt the user that they are about to leave the page.
As last week's article examined, these steps could be accomplished by adding two client-side <script> blocks
and client-side onclick events where needed. While adding this script code is not terribly difficult, I find
it simpler to move this logic to server-side methods that will inject the appropriate client-side script for us. In this
article we'll examine how to extend the base Page class, adding a couple of methods that will allow for a user
to be prompted when they leave the page without saving without the page developer having to write a single line of client-side
script code. (If you've yet to read Prompting a User to Save When Leaving a Page,
please be sure to do so before continuing on.)
An Update to this Article...
Since the publication of this article I have received many questions from readers on a couple of issues - namely, being able
to suppress the prompt when using a Web control whose AutoPostback property is set to True, and how to suppress
client-side script error messages in Internet Explorer when leaving the page via client-side script using the eval()
function. These two questions are discussed and answered in An Update on Prompting a User to Save When Leaving an ASP.NET Page.
A High-Level Look at What We Want to Accomplish
The code-behind class for any ASP.NET Web page is derived, either directly or indirectly, from the Page class
in the System.Web.UI namespace. The Page class contains the base functionality that all ASP.NET
Web pages must provide. For example, the Page class includes properties like IsValid and IsPostBack,
and events like Load (which triggers the Page_Load event handler to execute). If there is some functionality
that you know you will need in a number of pages, you can provide this functionality by creating a custom class that derives
from the Page class, and then have your ASP.NET Web pages' code-behind classes derive from this custom class
(rather than from the Page class directly).
In this article we'll create such a custom class that extends the Page class. Our extended Page class
will contain methods that we can call that will inject the appropriate client-side script so that if a user attempts to leave
the page after making changes without saving, a confirmation will be displayed. Specifically, there will be two methods:
MonitorChanges(webControl) - this method accepts a Web control (such as a TextBox, CheckBox,
DropDownList, etc.) and adds the necessary client-side script to monitor this control's value. That is, if this control's
value is changed and then the user attempts to leave the page without saving, they'll be prompted with a warning.
BypassModifiedMethod(webControl) - this method is used to indicate that a particular Web control
should not cause the confirmation to be displayed. This will typically be used on "Save" Buttons, LinkButtons -
namely on Web controls that might cause a postback but, even if changes have been made, should not display the confirmation.
In order to squirt out the correct client-side script, these methods will utilize three methods from the Page class:
RegisterClientScriptBlock(), RegisterStartupScript(), and RegisterArrayDeclaration().
These three server-side methods add client-side code to the ASP.NET page's rendered markup. Let's take a brief moment to
examine these three methods.
Working with Client-Side Script Code in Server-Side Code
Working on Web applications requires a keen understanding of the logical, physical, and temporal differences. These differences
were more apparent in classic ASP, but ASP.NET and its Web Forms paradigm effectively blurs the distinction between the client
and server. Regardless, the distinction still very much exists and it is important to be familiar with the separation.
There are often times when we might want to inject client-side script from a server-side method. To facilitate this the
Page class provides a number of methods. To add a block of client-side script code, use either
RegisterClientScriptBlock(key, script) or RegisterStartupScript(key, script). These methods both accept two string
values as input: a key and a script value. The key uniquely identifies the script block being injected,
while the script value contains the actual client-side script to inject. (Note that the script input parameter
must include the precise markup to inject, including the <script> tag itself.)
The main difference between these two methods is the location in the markup where the controls emits its script. Both methods
inject their script content inside the <form>, but RegisterClientScriptBlock(key, script)
adds it before the Web controls within the form, whereas RegisterStartupScript(key, script) adds
the script after the Web control markup.
The other Page class that we'll need to utilize is the RegisterArrayDeclaration(arrayName, arrayValue).
This method creates a client-side array with the values specified. To create an array named foo with values 1 through n
you'd call the RegisterArrayDeclaration(arrayName, arrayValue) method n times, like so:
A thorough discussion of the client-side injection methods in the Page class is a bit beyond the scope of this
article. For more information, utilize the following two articles of mine: Working
with Client-Side Script and Injecting
Client-Side Script from an ASP.NET Server Control. (The first article also discusses in more detail the process of
extending the functionality of the Page by creating a custom, derived class, and having ASP.NET pages' code-behind classes
deriving from this custom class.)
Creating the MonitorChanges(webControl) Method
A page developer using our extended Page class - which I named ClientSidePage - can indicate that a
particular Web control on the page should be monitored for changes by calling the MonitorChanges() method, passing
in the Web control to watch. This method needs to do the following two tasks:
Add the Web control's client-side ID to a client-side array. (This array is what is used to
grab the initial values of the input controls, and, upon page exit, is used to check to see if the input fields' values
have changed.)
Inject the client-side script that saves the initial form field values and that handles the onbeforeunload
client-side event.
This first task is accomplished directly in the MonitorChanges() method; the second task is delegated to an additional
method, as shown below (the complete ClientSidePage class is available for download at the end of this article in both VB.NET
and C#):
Public Class ClientSidePage
Inherits System.Web.UI.Page
Public Sub MonitorChanges(ByVal wc As WebControl)
If wc Is Nothing Then Exit Sub
If TypeOf wc Is CheckBoxList OrElse TypeOf wc Is RadioButtonList Then
'Add an array element for each item in the checkbox/radiobutton list
For i As Integer = 0 To CType(wc, ListControl).Items.Count - 1
Page.RegisterArrayDeclaration("monitorChangesIDs", """" & _
String.Concat(wc.ClientID, "_", i) & """")
Page.RegisterArrayDeclaration("monitorChangesValues", "null")
Next
Else
Page.RegisterArrayDeclaration("monitorChangesIDs", _
"""" & wc.ClientID & """")
Page.RegisterArrayDeclaration("monitorChangesValues", "null")
End If
AssignMonitorChangeValuesOnPageLoad()
End Sub
Private Sub AssignMonitorChangeValuesOnPageLoad()
If Not Page.IsStartupScriptRegistered("monitorChangesAssignment") Then
Page.RegisterStartupScript("monitorChangesAssignment", _
"<script language=""JavaScript"">" & vbCrLf & _
" assignInitialValuesForMonitorChanges();" & vbCrLf & _
"</script>")
Page.RegisterClientScriptBlock("monitorChangesAssignmentFunction", _
"<script language=""JavaScript"">" & vbCrLf & _
" function assignInitialValuesForMonitorChanges() {" & vbCrLf & _
" for (var i = 0; i < monitorChangesIDs.length; i++) {" & vbCrLf & _
" var elem = document.getElementById(monitorChangesIDs[i]);" & vbCrLf & _
" if (elem) if (elem.type == 'checkbox' || elem.type == 'radio') " & _
" monitorChangesValues[i] = elem.checked; " & _
" else monitorChangesValues[i] = elem.value;" & vbCrLf & _
" }" & vbCrLf & _
" }" & vbCrLf & vbCrLf & vbCrLf & _
" var needToConfirm = true;" & vbCrLf & _
" window.onbeforeunload = confirmClose;" & vbCrLf & vbCrLf & _
" function confirmClose() {" & vbCrLf & _
" if (!needToConfirm) return;" & vbCrLf & _
" for (var i = 0; i < monitorChangesValues.length; i++) {" & vbCrLf & _
" var elem = document.getElementById(monitorChangesIDs[i]);" & vbCrLf & _
" if (elem) if (((elem.type == 'checkbox' || elem.type == 'radio') && elem.checked != monitorChangesValues[i]) || (elem.type != 'checkbox' && elem.type != 'radio' && elem.value != monitorChangesValues[i])) { needToConfirm = false; setTimeout('resetFlag()', 750); return ""You have modified the data entry fields since last savings. If you leave this page, any changes will be lost. To save these changes, click Cancel to return to the page, and then Save the data.""; }" & vbCrLf & _
" }" & vbCrLf & _
" }" & vbCrLf & vbCrLf & _
" function resetFlag() { needToConfirm = true; } " & vbCrLf & _
"</script>")
End If
End Sub
...
End Class
Before delving into the methods, first note that the ClientSidePage class derives from System.Web.UI.Page.
It is vital that ClientSidePage extend the Page class in order to use this class as the base class for our ASP.NET
pages' code-behind classes.
For More Information on Base Classes...
The ClientSidePage class is an example of a base class that is designed to be inherited by an ASP.NET page's
code-behind class. This is a common practice in more involved ASP.NET Web applications and is discussed in more detail
in the article Using a Custom Base Class for your ASP.NET Page's Code-Behind Classes.
The MonitorChanges() method's first task is to create an array that contains the client-side IDs
of the input form fields to watch. This is done by creating two arrays: monitorChangesIDs, which holds the
IDs of the elements to watch; and monitorChangesValues, which holds the initial values of
these form fields. The MonitorChanges() method takes the passed-in Web control's ClientID and adds
it to the first array, and then adds a null to the second. (The second array is populated through client-side
code, which we'll examine in a bit.)
One thing to note is that the MonitorChanges() checks to see if the Web control passed in is a RadioButtonList or
CheckBoxList. If it is one of these two types of Web controls, special care must be taken since these controls create a set of children radio buttons or checkboxes whose
client-side IDs are of the form CheckBoxOrRadioButtonListID_indexOfCheckBoxOrRadioButton.
That is, a RadioButtonList with an ID of favSport would have its rendered radio buttons with
IDs of favSport_0, favSport_1, favSport_2, and favSport_3.
Therefore, for RadioButtonLists and CheckBoxLists, the MonitorChanges() method registers each of the control's
corresponding radio buttons or checkboxes in the client-side array. (One thing to note is that for data-bound RadioButtonLists
and CheckBoxLists, you will need to call MonitorChanges()after you do the databinding.)
After adding the client-side IDs to the array, MonitorChanges() calls the AssignMonitorChangeValuesOnPageLoad()
method, which does two things: injects startup script that calls the client-side assignInitialValuesForMonitorChanges()
function (this function then populates the monitorChangesValues array with the specified form fields' initial values);
and injects the actual code for the assignInitialValuesForMonitorChanges() function, along with the closeConfirm()
function (which serves as the event handler for the onbeforeunload event).
The code in the client-side closeConfirm() function is virtually identical to the code examined in the last week's
article, Prompting a User to Save When Leaving a Page.
The only difference is that here when a discrepancy is found and the user is prompted if they really want to exit the page,
we set the needToConfirm flag to false, but then setup a timer so that it's reset back to true
in 0.75 seconds. You may, understandably, be wondering why this is done.
The reason is, admittedly, a bit of a hack. To understand why we need this hack, realize that whenever a hyperlink is clicked,
the browser says, "Ok, you are leaving the page," and fires its onbeforeunload event. This sounds fine, but things
can get a bit weird if the hyperlink contains client-side JavaScript in its href that causes the page to unload
again. Specifically, if you have a hyperlink with an href with code that submits the form or redirects the user, when that hyperlink is
clicked, the user will be prompted once, asking if they want to leave the page. If they click OK, the hyperlink's JavaScript
will run, submitting the form or redirecting the user, which causes the browser to again prompt the user, asking them if they are sure
they want to leave the page. To suppress this second message, we simply set needToConfirm to false for
0.75 seconds. What this does, is when the hyperlink is first clicked, it prompts the user. If the user clicks OK, and the JavaScript
kicks in, the JavaScript doesn't cause a second, superfluous confirmation messagebox (assuming the JavaScript can run and complete
in 0.75 seconds). This hack is especially pertinent to ASP.NET since LinkButtons are hyperlinks with JavaScript code in their
href that submits (posts back) the form.
Excluding Buttons From Prompting the User
As we examined in Prompting a User to Save When Leaving a Page, the onbeforeunload event causes the user
to be prompted if they want to save when they exit the page after having made changes, but without saving. Specifically, this
onbreforeunload event fires when the user closes their browser, enters another URL, clicks on a hyperlink, or
posts back the form. What we want to avoid is having the "Save" button display a confirmation messagebox. Rather, we want
the "Save" button to be able to cause a postback without warning the user that values have been changed.
To accomplish this we need to have the "Save" button's client-side onclick event set the needToConfirm
flag to false. This can be accomplished via the BypassModifiedMethod(webControl) method, which
simply adds this needed client-side attribute to the passed-in Web control. The code for this method is rather simple, and shown below:
Public Class ClientSidePage
Inherits System.Web.UI.Page
...
Public Sub BypassModifiedMethod(ByVal wc As WebControl)
wc.Attributes("onclick") = "javascript:" & GetBypassModifiedMethodScript()
End Sub
Public Function GetBypassModifiedMethodScript() As String
Return "needToConfirm = false;"
End Function
End Class
Using the ClientSidePage Class in an ASP.NET Web Page
Let's wrap things up with looking at a simple data-entry Web page that utilizes the features we've examined thus far.
Start by creating a Web page with a number of form fields, such as TextBoxes, CheckBoxes, DropDownLists, RadioButtonLists,
and so on. Next, add a "Save" button. Now, to have the page exhibit the desired behavior - that is, to have it prompt the
user if they make a change to one of the data entry form fields and attempt to leave the page without saving - do the following:
In the page's code-behind class, have it derive from ClientSidePage
In the Page_Load event handler have a call to the MonitorChanges() method for each data-entry
Web control on the page. Do this on every page load (including postbacks). Also call BypassModifiedMethod(),
passing in any Buttons that should not cause the user to be prompted.
In the download at the end of this article, there's a sample ASP.NET page, WebForm1.aspx, with a number of
data-entry form fields. Here's the server-side code for monitoring changes to these fields:
Public Class WebForm1
Inherits ClientSidePage
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'Monitor the changes for the Web controls whose values you
'want to watch
MonitorChanges(name)
MonitorChanges(useASPNET)
MonitorChanges(favColor)
MonitorChanges(musicLikes)
MonitorChanges(favSport)
'For those controls (like "Save" buttons) that cause a postback
' that should NOT prompt the user, call BypassModifiedMethod
BypassModifiedMethod(btnSave)
End Sub
...
End Class
Notice that the code-behind class is derived from ClientSidePage (and not System.Web.UI.Page), that
there's a MonitorChanges() call for each data-entry form field, and that there's a call to
BypassModifiedMethod() for the "Save" button.
An Update to this Article...
Since the publication of this article I have received many questions from readers on a couple of issues - namely, being able
to suppress the prompt when using a Web control whose AutoPostback property is set to True, and how to suppress
client-side script error messages in Internet Explorer when leaving the page via client-side script using the eval()
function. These two questions are discussed and answered in An Update on Prompting a User to Save When Leaving an ASP.NET Page.