This article explains how to automatically save the user input in background whilst the user is filling forms. This is particularly useful for large forms and you don't miss the data if user closes the browser without saving the form, 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 its existing session if he logs in again prior to session expiration - even if
Introduction
This article explains how to automatically save the user input in background whilst the user is filling forms. This is particularly useful for large forms and you don't miss the data if user closes the Browser without saving the form, 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 its existing session if he logs in again prior to session expiration - even if logs in from another system.
This functionality is somewhat similar to ASP.NET 2.0 profiling.
This functionality is somewhat similar to ASP.NET 2.0 profiling.
The Idea
The idea is to send user data to web server on periodic basis where it is stored in memory and later flushed to database for persistent storage.
JavaScript is used to monitor the user input in browser and fill in a hash table with user input. On specified periods, the hash table is serialized as querystring and sent to an aspx page (AutoSave.aspx) using 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 database.
The Implementation
1. Bind onblur event of all input controls to populate hash table when user enters data in form.
2. Submit user data to server by calling AutoSave() function. xmlHttp wrapper implementation is from http://www.codeproject.com/Ajax/AJAXWasHere-Part1.asp
2. Submit user data to server by calling AutoSave() function. xmlHttp wrapper implementation is from http://www.codeproject.com/Ajax/AJAXWasHere-Part1.asp
3. Call AutoSave just before browser is going to close by the user, i.e. window.onbeforeunload event.
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 >4. Using Hash Table Implementation in JavaScript by Michael Synovic
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>
5. First we check if the DTO is already there, i.e. we've user session that is not yet expired, bind with the existing in-memory DTO, otherwise, create a new object in Cache to hold user data. Cache is used instead of Session object because it can survive across user sessions/logins and we can use Callback method to emulate Session_End event. CacheExpired method is invoked after Cache timeout and flushes the data to database.
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()); } }
6. On AutoSave.aspx, we iterate through the Query String values and set the properties of DTO and update the DTO object in memory.
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; }
7. 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 class properties from Querystring.
8.
8.
updateDB()
method is to flush the data from memory to database. updateDB()is invoked only when user Submits the form or Cached DTO is expired.
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 web page is automatically filled in 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 've name-mapping between data values to DTO properties.
No comments:
Post a Comment