Click here to Skip to main content
Click here to Skip to main content

Dirty Panel Extender (ASP.NET AJAX)

, 5 Sep 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
A dirty panel extender implementation with ASP.NET AJAX control toolkit.
ASP.NET Ajax Dirty Panel Extender in Action

Introduction

My website is a rich social network that offers users many web forms to fill. For example, users can post articles and edit lengthy profiles. Often they click on a link that takes them away from the page or press the wrong key (e.g. backspace that navigates to the previous page). In both cases their changes get lost. And it is always frustrating to have to re-enter the same text twice. Wouldn't it be nice to warn the user that he has unsaved data and give him an opportunity to cancel, then save his data?

This is a Panel Extender for ASP.NET AJAX 1.0 that automatically detects if any input control inside it was changed and shows an alert if the user tries to leave the page before saving the data. The extender supports most HTML input controls and can detect whether either data, selection or both have changed.

Background

This article uses the same techniques as described in this prior AJAX DirtyPanel article, but is implemented as a panel extender for Microsoft ASP.NET AJAX 1.0. The extender model offers a very clean and straightforward solution described in the implementation section below.

Using the Code

Standard Pages

Assuming you have an ASP.NET AJAX enabled site that uses the Ajax Control Toolkit, simply add the DirtyPanelExtender project to your solution, register the extender on the .aspx page and add an extender to a panel.

 <%@ register assembly="DirtyPanelExtender" 
            namespace="DirtyPanelExtender" tagprefix="dp" %>
 ...
 <dp:DirtyPaneleEtender id="demoPanelExtender" runat="server" 
                        targetcontrolid="demoPanel"
  OnLeaveMessage="There's still unsaved data on the page!" />
 <asp:UpdatePanel id="demoPanel" runat="server">
 ...

Master Pages

The master page scenario enables all website pages to enable the dirty panel feature automatically. You must wrap the ContentPlaceHolder in a panel and extend the panel with the DirtyPanelExtender.

<form id="form1" runat="server">
 <asp:ScriptManager ID="ScriptManager1" runat="server" />
 <dp:DirtyPanelExtender ID="demoPanelExtender" runat="server" 
                    TargetControlID="masterPanel"
  OnLeaveMessage="There's still unsaved data on the page!" />
 <asp:UpdatePanel ID="masterPanel" runat="server">
 <ContentTemplate>
  <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
  </asp:ContentPlaceHolder>
 </ContentTemplate>
 </asp:UpdatePanel>
</form> 

Implementation

Creating a Basic Extender

Creating an extender skeleton is described in this walkthrough. The basics include:

  • DirtyPanelExtenderBehavior.js: all client-side script logic
  • DirtyPanelExtender.cs: server-side control implementation
  • DirtyPanelExtenderDesigner.cs: design-time functionality

Hooking window.onbeforeunload

The window.onbeforeunload callback is the essential hook that will trap closing of the window. It is possible to prompt the user before the window is unloaded.

window.onbeforeunload = function (eventargs)
{
  if(! eventargs) eventargs = window.event;
  eventargs.returnValue = "You have unsaved data. 
                Are you sure you want to close this window?"
}

See MSDN for detailed information about the window.onbeforeunload handler.

Multiple DirtyPanels

The implementation supports multiple dirty panels by creating an array of panels.

 var DirtyPanelExtender_dirtypanels = new Array()

The panel initialization code that will add itself to this array.

initialize : function() 
{
 DirtyPanelExtender.DirtyPanelExtenderBehavior.callBaseMethod
                            (this, 'initialize');
 DirtyPanelExtender_dirtypanels[DirtyPanelExtender_dirtypanels.length] = this;
}

It is now possible to iterate through the array in JavaScript.

for (i in DirtyPanelExtender_dirtypanels)
{
 var panel = DirtyPanelExtender_dirtypanels[i];
 ...
}

Hooking window.onbeforeunload for Dirty Panels

Every panel will expose a panel.isDirty that will return true if any of the existing form fields has changed (making the panel "dirty"), plus an OnLeaveMessage property to store the message to show. The hooking will only need to happen for a dirty panel.

window.onbeforeunload = function (eventargs)
{
 for (i in DirtyPanelExtender_dirtypanels)
 {
  var panel = DirtyPanelExtender_dirtypanels[i];
  if (panel.isDirty())
  {
   if(! eventargs) eventargs = window.event;
   eventargs.returnValue = panel.get_OnLeaveMessage();
   break;
  }
 }
} 

Suppressing Dirty Check for Postbacks

The dirty panel only needs to trap navigating away from the page and not regular AJAX interaction built into the page. This notably enables upload controls without an UpdatePanel.

function __newDoPostBack(eventTarget, eventArgument)
{
// suppress prompting on postback
window.onbeforeunload = null;
return __savedDoPostBack (eventTarget, eventArgument);
}

var __savedDoPostBack = __doPostBack;
__doPostBack = __newDoPostBack; 

Determining Whether a Panel is Dirty

Determining whether the panel is dirty is the hardest part. First, there's no native support for whether an input box or other editable control has changed. Old values must be tracked and compared. In addition, hidden values should not be updated on a regular postback. Original values are saved in a hidden field in OnPreRender.

protected override void OnPreRender(EventArgs e)
{
 string values_id = string.Format("{0}_Values", TargetControl.ClientID);
 string values = (Page.IsPostBack ? 
    Page.Request.Form[values_id] : String.Join(",", GetValuesArray()));
 ScriptManager.RegisterHiddenField(this, values_id, values);
 base.OnPreRender(e);
} 

The implementation of GetValuesArray simply iterates through child controls and saves those that are editable. Special care is taken for various types of controls.

  • ListControl types, including DropDownList and ListBox: save both data and initial selections
  • RadioButtonList: save an entry for each radio button with its selected state; radio button contents don't work
  • IEditableTextControl: save any .Text value of an editable control
  • ICheckBoxControl: save checkbox state

Note that it now looks trivial to implement a way to reset the dirty flag, for example when the user presses the Save button. It is only necessary to reset the saved values. Unfortunately things are not that simple, especially if the extender is used with an UpdatePanel. You must emit JavaScript within that panel that will reset the value of the hidden field.

public void ResetDirtyFlag()
{
   ScriptManager.RegisterClientScriptBlock
                (TargetControl, TargetControl.GetType(),
   string.Format("{0}_Values_Update", TargetControl.ClientID), 
        string.Format("document.getElementById('{0}').value = '{1}';",
   string.Format("{0}_Values", TargetControl.ClientID), 
                String.Join(",", GetValuesArray())), true);
} 

The isDirty function deconstructs the hidden field value and compares the current form values one-by-one, for each type of input control.

isDirty : function() {
 var values_control = document.getElementById(this.get_element().id + 
                                "_Values");
 var values = values_control["value"].split(",");
 for (i in values) {
  var namevalue = values[i];
  var namevaluepair = namevalue.split(":");
  var name = namevaluepair[0];
  var value = (namevaluepair.length > 1 ? namevaluepair[1] : "");
  var control = document.getElementById(name);
  if (control == null) continue;
  if (control.type == 'checkbox' || control.type == 'radio') {
   var boolvalue = (value == "true" ? true : false);
   if(control.checked != boolvalue) {
    return true;
   }
  } else if (control.type == 'select-one') {
   if ( control.size > 0 ){
    // control is listbox
    ...
    if( encodeURIComponent(optionValues) != value ){
     return true;
    }
   } else if(control.selectedIndex != value) {
     return true;
   }
  } else {
   if(encodeURIComponent(control.value) != value) {
       return true;
   }
  }
 }
 return false;
} 

Dealing with Lists

The actual implementation of isDirty is a little more complex, especially for lists. These typically inherit from ListControl. It is necessary to support both selection and data changes in the list, and GetValuesArray creates two hidden variables, id:selection:value and id:data:value to represent the current state.

else if (control is ListControl)
{
   StringBuilder data = new StringBuilder();
   StringBuilder selection = new StringBuilder();
   foreach (ListItem item in ((ListControl) control).Items)
   {
       data.AppendLine(item.Text);
       selection.AppendLine(item.Selected.ToString().ToLower());
   }
   values.Add(string.Format("{0}:data:{1}", control.ClientID, 
                Uri.EscapeDataString(data.ToString())));
   values.Add(string.Format("{0}:selection:{1}", control.ClientID, 
                Uri.EscapeDataString(selection.ToString())));
} 

isDirty will process both types of values.

} else if (control.type == 'select-one' || control.type == 'select-multiple') 
 {
  if (namevaluepair.length > 2) {
  // composite control (has data and selection)
     if ( control.options.length > 0) {
         // the control has a list of values
         // there's data:value and selection:value
         var code = value;
         value = (namevaluepair.length > 2 ? namevaluepair[2] : "");
         var optionValues = "";
         // concat all listbox items
         for( var cnt = 0; cnt < control.options.length; cnt++) {
            if (code == 'data') {
                optionValues += control.options[cnt].text;
            } else if (code == 'selection') {
                optionValues += control.options[cnt].selected;
            }
            optionValues += "\r\n";
         }
         if( encodeURIComponent(optionValues) != value ) {
            // items in the listbox have changed
            return true;
         }
     }
 } else if(control.selectedIndex != value) {
     return true;
 }  

Conclusion

This is a simple and useful control. I also found the ASP.NET AJAX extender model very well structured and clean, adding useful functionality to existing controls in a straightforward manner, a significant improvement over the reference AJAX implementation for Anthem.

Known Issues

  • bug: doesn't work with Opera; tested with Opera 9.21
  • bug: doesn't work with Safari; tested with 3.0.2 WinXP
  • bug: partial support for RadioButtonList - selection changes only, no dynamic data changes

History

  • 08/10/2007: initial version
  • 08/11/2007: fixed bug - target control client ID wrong
  • 08/11/2007: fixed bug - fixed for upload controls and standard AJAX scenarios; suppressed prompting for all postbacks
  • 08/13/2007: added demo and documentation for using the extender with master pages
  • 08/28/2007: added RadioButtonList and ListBox support and demo for both data and selection (thanks to David Christensen)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

dB.
Team Leader Application Security Inc., www.appsecinc.com
United States United States
Daniel Doubrovkine has been in software engineering for twelve years and is currently development manager at Application Security Inc. in New York City. He has been involved in many software ventures, including Xo3 and Vestris Inc, was a development lead at Microsoft Corp. in Redmond, and director of Engineering at Visible Path Corp. in New York City. Daniel also builds and runs a foodie website, http://www.foodcandy.com.

Comments and Discussions

 
Question[My vote of 1] This doesn't work with web-kit browsers like Chrome Pinmembermsdevtech26-Jul-12 8:27 
QuestionUpgraded to .NET Framework 4.0 and AJAX 4.1.50927.0 [modified] Pinmemberdatamaster-ca30-Sep-11 6:50 
Generallost focus during tab [modified] PinmemberMember 17585964-Mar-11 10:58 
GeneralRe: lost focus during tab Pinmemberdatamaster-ca7-Mar-11 5:12 
GeneralMy vote of 5 PinmemberBharat Gambhir25-Nov-10 3:42 
Ultimate solution provided by db, yet not compatible with vs2008. Please do the neccessary changes. Thanks Bharat Gambhir
GeneralInherited 2008 web application that uses the DirtyPanelExtender. PinmemberMember 10113906-Sep-10 9:36 
GeneralRe: Inherited 2008 web application that uses the DirtyPanelExtender. PinmemberdB.6-Sep-10 17:09 
GeneralRe: Inherited 2008 web application that uses the DirtyPanelExtender. PinmemberBenLynch6-Sep-10 22:00 
GeneralRe: Inherited 2008 web application that uses the DirtyPanelExtender. PinmemberdB.7-Sep-10 2:16 
GeneralRe: Inherited 2008 web application that uses the DirtyPanelExtender. PinmemberBenLynch7-Sep-10 5:34 
GeneralRe: Inherited 2008 web application that uses the DirtyPanelExtender. PinmemberBenLynch7-Sep-10 11:48 
GeneralNot working. PinmemberMubashar_Iqbal23-Aug-10 6:33 
GeneralRe: Not working. PinmemberdB.6-Sep-10 17:09 
GeneralI couldn't get this to work Pinmembertoddmo3-Aug-10 16:14 
GeneralRe: I couldn't get this to work PinmemberdB.4-Aug-10 2:50 
GeneralRe: I couldn't get this to work [modified] Pinmembertoddmo4-Aug-10 3:26 
GeneralRe: I couldn't get this to work PinmemberdB.4-Aug-10 3:34 
GeneralRe: I couldn't get this to work Pinmembertoddmo4-Aug-10 3:48 
GeneralGiving alert for nonsecure items on https Pinmemberdsunaria28-Jul-10 2:00 
GeneralRe: Giving alert for nonsecure items on https PinmemberdB.30-Jul-10 7:38 
GeneralEasy way to reset the DirtyPanelExtender from JavaScript Pinmemberit-bergmann15-Dec-09 22:01 
GeneralRe: Easy way to reset the DirtyPanelExtender from JavaScript PinmemberdB.16-Dec-09 3:32 
GeneralRe: Easy way to reset the DirtyPanelExtender from JavaScript [modified] Pinmemberit-bergmann18-Dec-09 5:09 
Generalthis works for me with JS update function and using NET 3.5 Pinmemberit-bergmann18-Dec-09 10:44 
GeneralAlways dirty again [modified] Pinmemberit-bergmann24-Nov-09 1:47 
GeneralRe: Always dirty again PinmemberdB.24-Nov-09 2:15 
GeneralRe: Always dirty again Pinmemberit-bergmann24-Nov-09 2:27 
GeneralRe: Always dirty again Pinmemberit-bergmann27-Nov-09 23:14 
QuestionRe: Always dirty again PinmemberMember 89430958-May-12 0:50 
QuestionTips for .NET 3.5? Pinmembergrenadier23-Oct-09 11:56 
AnswerRe: Tips for .NET 3.5? PinmemberdB.23-Oct-09 23:14 
AnswerRe: Tips for .NET 3.5? Pinmemberit-bergmann13-Dec-09 9:29 
GeneralRe: Tips for .NET 3.5? PinmemberdB.14-Dec-09 4:05 
GeneralRe: Tips for .NET 3.5? Pinmemberit-bergmann14-Dec-09 7:50 
GeneralAnyone managed to get ASP.NET 3.5 AJAX HTMLEditor to work with this? [modified] PinmemberdB.1-Oct-09 19:20 
QuestionHow to do dirty check on button click? PinmemberAssimalyst24-Sep-09 1:28 
AnswerRe: How to do dirty check on button click? PinmemberdB.24-Sep-09 3:10 
GeneralRe: How to do dirty check on button click? PinmemberErik Bordi26-Oct-09 3:08 
GeneralHave you gotten this to work in a user control [modified] Pinmembertomp-fts9-Aug-09 10:55 
GeneralRe: Have you gotten this to work in a user control PinmemberdB.11-Aug-09 3:31 
GeneralRe: Have you gotten this to work in a user control Pinmembertomp-fts11-Aug-09 4:02 
Generalerror on page when "Cancel" button is clicked Pinmembertomp-fts28-May-09 18:04 
GeneralRe: error on page when "Cancel" button is clicked PinmemberdB.29-May-09 2:39 
GeneralRe: error on page when "Cancel" button is clicked Pinmembertomp-fts29-May-09 17:47 
GeneralRe: error on page when "Cancel" button is clicked PinmemberAssimalyst20-Aug-09 3:48 
GeneralRe: error on page when "Cancel" button is clicked Pinmembertomp-fts20-Aug-09 4:29 
Generalerror on page Pinmemberbquick2-Apr-09 11:37 
GeneralRe: error on page Pinmemberbquick3-Apr-09 2:26 
GeneralRe: error on page PinmemberdB.12-Apr-09 7:19 
GeneralError on Cancel in MOSS webpart. PinmemberRuss Motz26-Mar-09 10:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 5 Sep 2007
Article Copyright 2007 by dB.
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid