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. The XmlHttp
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.
<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 >
- Using the hash table implementation in JavaScript by Michael Synovic:
<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>
- 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.
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());
}
}
- On AutoSave.aspx, we iterate through the query string values and set the properties of the DTO and update the DTO object in memory.
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;
}
- 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.
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
- 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.