Thursday, September 19, 2024

Using ASP.NET to Prompt a User to Save When Leaving a Page

Previously 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:

1. Writing code that saves the initial values of the page’s input form fields in a client-side array.

2. 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).

3. 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 the 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.)

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:

    Page.RegisterArrayDeclaration("foo", "1");
    Page.RegisterArrayDeclaration("foo", "2");
    ...
    Page.RegisterArrayDeclaration("foo", "n");

    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:

    1. 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.)

    2. 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
    &nbsp&nbsp Inherits System.Web.UI.Page

    &nbsp&nbsp Public Sub MonitorChanges(ByVal wc As WebControl)
    &nbsp&nbsp&nbsp If wc Is Nothing Then Exit Sub

    &nbsp&nbsp&nbsp If TypeOf wc Is CheckBoxList OrElse TypeOf wc Is RadioButtonList Then
    &nbsp&nbsp&nbsp&nbsp 'Add an array element for each item in the checkbox/radiobutton list
    &nbsp&nbsp&nbsp&nbsp For i As Integer = 0 To CType(wc, ListControl).Items.Count - 1
    &nbsp&nbsp&nbsp&nbsp&nbsp Page.RegisterArrayDeclaration("monitorChangesIDs", """" & _
    &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp String.Concat(wc.ClientID, "_", i) & """")
    &nbsp&nbsp&nbsp&nbsp Page.RegisterArrayDeclaration("monitorChangesValues", "null")
    &nbsp&nbsp&nbsp&nbsp Next
    &nbsp&nbsp&nbsp Else
    &nbsp&nbsp&nbsp Page.RegisterArrayDeclaration("monitorChangesIDs", _
    &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp """" & wc.ClientID & """")
    &nbsp&nbsp&nbsp Page.RegisterArrayDeclaration("monitorChangesValues", "null")
    &nbsp&nbsp&nbsp End If

    &nbsp&nbsp&nbsp AssignMonitorChangeValuesOnPageLoad()
    &nbsp&nbsp End Sub

    &nbsp Private Sub AssignMonitorChangeValuesOnPageLoad()
    &nbsp&nbsp If Not Page.IsStartupScriptRegistered("monitorChangesAssignment") Then
    &nbsp&nbsp&nbsp Page.RegisterStartupScript("monitorChangesAssignment", _
    &nbsp&nbsp&nbsp&nbsp "<script language=""JavaScript"">" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " assignInitialValuesForMonitorChanges();" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp "</script>")

    &nbsp&nbsp&nbsp Page.RegisterClientScriptBlock("monitorChangesAssignmentFunction", _
    &nbsp&nbsp&nbsp&nbsp "<script language=""JavaScript"">" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " function assignInitialValuesForMonitorChanges() {" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " for (var i = 0; i < monitorChangesIDs.length; i++) {" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " var elem = document.getElementById(monitorChangesIDs[i]);" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " if (elem) if (elem.type == 'checkbox' || elem.type == 'radio') " & _
    &nbsp&nbsp&nbsp&nbsp " monitorChangesValues[i] = elem.checked; " & _
    &nbsp&nbsp&nbsp&nbsp " else monitorChangesValues[i] = elem.value;" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " }" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " }" & vbCrLf & vbCrLf & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " var needToConfirm = true;" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " window.onbeforeunload = confirmClose;" & vbCrLf & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " function confirmClose() {" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " if (!needToConfirm) return;" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " for (var i = 0; i < monitorChangesValues.length; i++) {" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " var elem = document.getElementById(monitorChangesIDs[i]);" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " 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 & _
    &nbsp&nbsp&nbsp&nbsp " }" & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " }" & vbCrLf & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp " function resetFlag() { needToConfirm = true; } " & vbCrLf & _
    &nbsp&nbsp&nbsp&nbsp "</script>")
    &nbsp&nbsp End If
    &nbsp End Sub

    &nbsp ...
    End Class F

    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.

    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 previous 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
    &nbsp Inherits System.Web.UI.Page

    &nbsp ...

    &nbsp Public Sub BypassModifiedMethod(ByVal wc As WebControl)
    &nbsp&nbsp wc.Attributes("onclick") = "javascript:" & GetBypassModifiedMethodScript()
    &nbsp End Sub

    &nbsp Public Function GetBypassModifiedMethodScript() As String
    &nbsp&nbsp Return "needToConfirm = false;"
    &nbsp 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:

    1. In the page’s code-behind class, have it derive from ClientSidePage

    2. 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
    &nbsp Inherits ClientSidePage

    &nbsp Private Sub Page_Load(ByVal sender As System.Object, _
    &nbsp &nbsp &nbsp &nbsp ByVal e As System.EventArgs) Handles MyBase.Load
    &nbsp &nbsp 'Monitor the changes for the Web controls whose values you
    &nbsp &nbsp 'want to watch
    &nbsp &nbsp MonitorChanges(name)
    &nbsp &nbsp MonitorChanges(useASPNET)
    &nbsp &nbsp MonitorChanges(favColor)
    &nbsp &nbsp MonitorChanges(musicLikes)
    &nbsp &nbsp MonitorChanges(favSport)

    &nbsp &nbsp 'For those controls (like "Save" buttons) that cause a postback
    &nbsp &nbsp ' that should NOT prompt the user, call BypassModifiedMethod
    &nbsp &nbsp BypassModifiedMethod(btnSave)
    &nbsp End Sub

    &nbsp ...
    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.

    *Originally published at 4GuysFromRolla.com

    Scott Mitchell, author of five ASP/ASP.NET books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies for the past five years. An active member in the ASP and ASP.NET community, Scott is passionate about ASP and ASP.NET and enjoys helping others learn more about these exciting technologies. For more on the DataGrid, DataList, and Repeater controls, check out Scott’s book ASP.NET Data Web Controls Kick Start (ISBN: 0672325012). Read his blog at : http://scottonwriting.net

    Related Articles

    1 COMMENT

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here

    Latest Articles