Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Dirty Panel Extender (ASP.NET AJAX)

4.61/5 (30 votes)
5 Sep 2007CPOL4 min read 3   1.8K  
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.

ASP.NET
<%@ 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.

ASP.NET
<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.

JavaScript
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.

JavaScript
var DirtyPanelExtender_dirtypanels = new Array()

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

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

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

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.

JavaScript
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.

JavaScript
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.

C#
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.

C#
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.

JavaScript
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.

C#
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.

JavaScript
} 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)