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.
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
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();
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
{
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">
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);
}
}
internal void CacheExpired(string key, object val, CacheItemRemovedReason reason) {
if (reason != CacheItemRemovedReason.Removed){
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. 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]{
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() {
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.
History
first release 6/29/2006.
update 9/25/2006.