Automatically Saving Web Form Data






4.32/5 (14 votes)
Jun 29, 2006
3 min read

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
- Bind the
OnBlur
event of all input controls to populate a hash table when the user enters data in a form. - Submit user data to the server by calling the
AutoSave()
function. TheXmlHttp
wrapper implementation is from http://www.codeproject.com/Ajax/AJAXWasHere-Part1.aspx. - Call
AutoSave
just before the browser is closed by the user, i.e.,window.onbeforeunload
event. - Using the hash table implementation in JavaScript by Michael Synovic:
- 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. TheCacheExpired
method is invoked after cache timeout and flushes the data to the database. - On AutoSave.aspx, we iterate through the query string values and set the properties of the DTO and update the DTO object in memory.
- 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.
- 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.
//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 >
//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>
//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());
}
}
//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;
}
//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
- The solution won't work if a web page is automatically filled in by a client browser by some web-form-filler software.
- 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.