65.9K
CodeProject is changing. Read more.
Home

Automatically Saving Web Form Data

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.32/5 (14 votes)

Jun 29, 2006

3 min read

viewsIcon

138626

How to automatically save user input in web applications.

Introduction

This article explains how to automatically save user input in the background whilst the user is filling forms. This is particularly useful for large forms and you don't miss the data if the user closes the browser without saving the form, or when they navigate away from the web page or the browser crashes. The technique used here can also be used to implement roaming user session for an authenticated web user, i.e., the user rebinds to an existing session if he logs in again prior to session expiration - even if he logs in from another system. This functionality is somewhat similar to ASP.NET 2.0 profiling.

The Idea

The idea is to send user data to a web server on a periodic basis where it is stored in memory and later flushed to the database for persistent storage.

JavaScript is used to monitor the user input in the browser and fill in a hash table with the user input. On specified periods, the hash table is serialized as a query string and sent to an ASPX page (AutoSave.aspx) using an XmlHttp object. AutoSave.aspx populates an in memory object with the values from the query string. After a specified period, usually at session timeout, the values are flushed to the database.

The Implementation

  1. Bind the OnBlur event of all input controls to populate a hash table when the user enters data in a form.
  2. Submit user data to the server by calling the AutoSave() function. The XmlHttp wrapper implementation is from http://www.codeproject.com/Ajax/AJAXWasHere-Part1.aspx.
  3. Call AutoSave just before the browser is closed by the user, i.e., window.onbeforeunload event.
  4. //WebForm1.aspx
    
    <script defer="defer" language="javascript">
    
        bindEvents(); //binds onblur events,  onchange for DropDown Lists 
    
    
        var xmlHttp; 
        xmlHttp = GetXmlHttpObject(CallbackMethod); 
                        
        function AutoSave()
        { 
            if (!Data.isEmpty()) {
                qstring = Data.toQueryString();
                SendXmlHttpRequest(xmlHttp, "AutoSave.aspx?" + 
                                   qstring.substring(0,qstring.length-1)); 
                Data.clear();
            }
        } 
            
        function CallbackMethod() 
        { 
            try
            {
                //readyState of 4 or 'complete' represents 
    
                if (xmlHttp.readyState == 4 || xmlHttp.readyState == 'complete')
                {
                    var response = xmlHttp.responseText; 
                    if (response.length > 0)
                    {
                        alert("Unable to Auto Save Data. Please " + 
                              "check your internet connectivity");
                    } 
                }
            }
            catch(e){}
        }
                
        window.setInterval(AutoSave, 15000);
        window.onbeforeunload = AutoSave;
    </script > 
  5. Using the hash table implementation in JavaScript by Michael Synovic:
  6. //WebForm1.aspx
    
    <script defer="defer" language="javascript">
    
    /*Bind event with Controls */
    
        function bindEvents(){
            var textBoxes = document.getElementsByTagName("input");
            for (i=0; i< textBoxes.length; i++){
                if (textBoxes[i].type == 'text' || textBoxes[i].type == 'radio'){
                    textBoxes[i].onblur = updateHashTable;
                }
            }
            for (i=0; i< textBoxes.length; i++){
                if (textBoxes[i].type == 'checkbox'){
                    textBoxes[i].onblur = updateHashTableforCheckBox;
                }
            }
        
            var comboBoxes = document.getElementsByTagName("select");
            for (j=0; j< comboBoxes.length; j++){
                comboBoxes[j].onchange = updateHashTableforCombo;
            }
        }
    
        var Data= new Hashtable();
    
        function updateHashTable(){
        Data.put(this.id, this.value);
        }
        function updateHashTableforCheckBox(){
        Data.put(this.id, this.checked);
        }
        function updateHashTableforCombo(){
            Data.put(this.id, this.options(this.selectedIndex).value);
        }
    </script>
  7. First, we check if the DTO is already there, i.e., we've a user session that is not yet expired. We bind it with the existing in-memory DTO. Otherwise, we create a new object in the Cache to hold the user data. The Cache is used instead of the Session because it can survive across user sessions/logins and we can use a callback method to emulate the Session_End event. The CacheExpired method is invoked after cache timeout and flushes the data to the database.
  8. //WebForm1.aspx.cs
    
    private void Page_Load(object sender, System.EventArgs e){
    
        if (!Page.IsPostBack){
                    
        UserData userData;
        if (Cache[Context.User.Identity.Name] == null){
            userData = new UserData();
            Cache.Insert(Context.User.Identity.Name, userData ,null, 
               Cache.NoAbsoluteExpiration, 
               TimeSpan.FromMinutes(Session.Timeout), 
               CacheItemPriority.Default, new  CacheItemRemovedCallback(CacheExpired));
        }
        else{
            userData = Cache[Context.User.Identity.Name] as UserData;
        }
    
        FillPage(userData); //to populate web form controls with values from DTO
    
    }
    
    internal void CacheExpired(string key, object val, CacheItemRemovedReason reason) {
        if (reason != CacheItemRemovedReason.Removed){
        //Save.aspx invokes userData.updateDB() to update data in database
            HttpContext.Current.Server.Execute("Save.aspx", new StringWriter());
        }
    }
  9. On AutoSave.aspx, we iterate through the query string values and set the properties of the DTO and update the DTO object in memory.
  10. //AutoSave.aspx.cs
    
    private void Page_Load(object sender, System.EventArgs e)
    {
        UserData userData = Cache[Context.User.Identity.Name] as UserData;
        for(int i=0; i < Request.QueryString.Count; i++){
            try{
                userData[Request.QueryString.GetKey(i)] = Request.QueryString.Get(i);
            }
            catch (Exception ex){
                continue;
            }
        }
            Cache[Context.User.Identity.Name] = userData;
        
    }
  11. A DTO (Data Transfer Object) is defined to hold the user data in memory. A string indexer is implemented to directly assign the values to the class properties from the query string.
  12. The updateDB() method is to flush the data from the memory to the database. updateDB() is invoked only when the user submits the form or when the cached DTO expires.
  13. //DTO.cs
    
    public class UserData {
        public string this[string paramName]{// string indexer
    
            get {
                return this.GetType().GetProperty(paramName).GetValue(
                                      this, null).ToString();
            }
            set{
                this.GetType().GetProperty(paramName).SetValue(this,value,null);
            }
        }
    
        private string _LastName;
        public string LastName {
            get { return _LastName; }
            set { _LastName = value; }
        }
    
        private string _FirstName;
        public string FirstName {
            get { return _FirstName; }
            set { _FirstName = value; }
        }
            
        private string _Email;
        public string Email {
            get { return _Email; }
            set { _Email = value; }
        }
            
        public void updateDB() {
            
            /***here: invoke Stored Procedure to update data in database***/
            HttpContext.Current.Cache.Remove(HttpContext.Current.User.Identity.Name);
        }

Limitations

  1. The solution won't work if a web page is automatically filled in by a client browser by some web-form-filler software.
  2. If the user input fields are defined inside user controls (ASCX) that are reused in many places, we might need to modify the solution a bit as currently, we have name-mapping between data values to DTO properties.

History

  • First release - 6/29/2006.
  • Update - 9/25/2006.