Introduction
In this article, I explain how to save user input data automatically when filling forms. This functionality is similar to ASP.NET profiling. This is useful for large web forms and if the user closes the browser without saving the form. Here, I also use ASP.NET caching to retrieve data for authenticated users.
Overview
JavaScript is used to bind events on the client side, collect user input data, and also to fill a hash table with user input. On a given period, the hash table is serialized and sent as a query string to the ASPX page using an XmlHttp
object. AutoSave.aspx populates an in-memory object with a query string value and on session timeout, values are saved to the database.
Using the code
- Bind the
OnBlur
event of all input controls to fill a hash table when user inputs are given.
- Submit all user input data to the server side.
- Submit all data to the server side just before the user closes the browser.
<script src="jshashtable.js" type="text/javascript"></script>
<script type="text/javascript">
window.onload = bindEvents;
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);
}
function AutoSave()
{
if(!Data.isEmpty())
{
qstring = Data.toQueryString();
SendXmlHttpRequest("AutoSave.aspx?"+qstring)
Data.clear();
}
}
var xmlHttp;
function SendXmlHttpRequest(url)
{
xmlhttp=null;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
if (xmlhttp!=null)
{
xmlhttp.onreadystatechange=state_Change;
xmlhttp.open("GET",url,true);
xmlhttp.send(null);
}
else
{
alert("Your browser does not support XMLHTTP.");
}
}
function state_Change()
{
if (xmlhttp.readyState==4)
{
if (xmlhttp.status==200)
{
var textBoxes = document.getElementsByTagName("input");
for (i=0; i< textBoxes.length; i++)
{
textBoxes[i].id.innerHTML=xmlhttp.status;
}
}
else
{
alert("Problem retrieving XML data:" + xmlhttp.statusText);
}
}
}
window.setInterval(AutoSave, 10000);
window.onbeforeunload = AutoSave;
</script>
var Hashtable = (function() {
function isUndefined(obj) {
return (typeof obj === "undefined");
}
function isFunction(obj) {
return (typeof obj === "function");
}
function isString(obj) {
return (typeof obj === "string");
}
function hasMethod(obj, methodName) {
return isFunction(obj[methodName]);
}
function hasEquals(obj) {
return hasMethod(obj, "equals");
}
function hasHashCode(obj) {
return hasMethod(obj, "hashCode");
}
function keyForObject(obj) {
if (isString(obj)) {
return obj;
} else if (hasHashCode(obj)) {
var hashCode = obj.hashCode();
if (!isString(hashCode)) {
return keyForObject(hashCode);
}
return hashCode;
} else if (hasMethod(obj, "toString")) {
return obj.toString();
} else {
return String(obj);
}
}
function equals_fixedValueHasEquals(fixedValue, variableValue) {
return fixedValue.equals(variableValue);
}
function equals_fixedValueNoEquals(fixedValue, variableValue) {
if (hasEquals(variableValue)) {
return variableValue.equals(fixedValue);
} else {
return fixedValue === variableValue;
}
}
function equals_equivalence(o1, o2) {
return o1 === o2;
}
function arraySearch(arr, value, arrayValueFunction,
returnFoundItem, equalityFunction) {
var currentValue;
for (var i = 0, len = arr.length; i < len; i++) {
currentValue = arr[i];
if (equalityFunction(value, arrayValueFunction(currentValue))) {
return returnFoundItem ? [i, currentValue] : true;
}
}
return false;
}
function arrayRemoveAt(arr, idx) {
if (hasMethod(arr, "splice")) {
arr.splice(idx, 1);
} else {
if (idx === arr.length - 1) {
arr.length = idx;
} else {
var itemsAfterDeleted = arr.slice(idx + 1);
arr.length = idx;
for (var i = 0, len = itemsAfterDeleted.length; i < len; i++) {
arr[idx + i] = itemsAfterDeleted[i];
}
}
}
}
function checkKeyOrValue(kv, kvStr) {
if (kv === null) {
throw new Error("null is not a valid " + kvStr);
} else if (isUndefined(kv)) {
throw new Error(kvStr + " must not be undefined");
}
}
var keyStr = "key", valueStr = "value";
function checkKey(key) {
checkKeyOrValue(key, keyStr);
}
function checkValue(value) {
checkKeyOrValue(value, valueStr);
}
function Bucket(firstKey, firstValue, equalityFunction) {
this.entries = [];
this.addEntry(firstKey, firstValue);
if (equalityFunction !== null) {
this.getEqualityFunction = function() {
return equalityFunction;
};
}
}
function getBucketEntryKey(entry) {
return entry[0];
}
function getBucketEntryValue(entry) {
return entry[1];
}
Bucket.prototype = {
getEqualityFunction: function(searchValue) {
if (hasEquals(searchValue)) {
return equals_fixedValueHasEquals;
} else {
return equals_fixedValueNoEquals;
}
},
searchForEntry: function(key) {
return arraySearch(this.entries, key, getBucketEntryKey,
true, this.getEqualityFunction(key));
},
getEntryForKey: function(key) {
return this.searchForEntry(key)[1];
},
getEntryIndexForKey: function(key) {
return this.searchForEntry(key)[0];
},
removeEntryForKey: function(key) {
var result = this.searchForEntry(key);
if (result) {
arrayRemoveAt(this.entries, result[0]);
return true;
}
return false;
},
addEntry: function(key, value) {
this.entries[this.entries.length] = [key, value];
},
size: function() {
return this.entries.length;
},
keys: function(keys) {
var startIndex = keys.length;
for (var i = 0, len = this.entries.length; i < len; i++) {
keys[startIndex + i] = this.entries[i][0];
}
},
values: function(values) {
var startIndex = values.length;
for (var i = 0, len = this.entries.length; i < len; i++) {
values[startIndex + i] = this.entries[i][1];
}
},
containsKey: function(key) {
return arraySearch(this.entries, key, getBucketEntryKey,
false, this.getEqualityFunction(key));
},
containsValue: function(value) {
return arraySearch(this.entries, value,
getBucketEntryValue, false, equals_equivalence);
}
};
function BucketItem() {}
BucketItem.prototype = [];
function getBucketKeyFromBucketItem(bucketItem) {
return bucketItem[0];
}
function searchBucketItems(bucketItems, bucketKey, equalityFunction) {
return arraySearch(bucketItems, bucketKey,
getBucketKeyFromBucketItem, true, equalityFunction);
}
function getBucketForBucketKey(bucketItemsByBucketKey, bucketKey) {
var bucketItem = bucketItemsByBucketKey[bucketKey];
if (bucketItem && (bucketItem instanceof BucketItem)) {
return bucketItem[1];
}
return null;
}
function Hashtable(hashingFunction, equalityFunction) {
var bucketItems = [];
var bucketItemsByBucketKey = {};
hashingFunction = isFunction(hashingFunction) ? hashingFunction : keyForObject;
equalityFunction = isFunction(equalityFunction) ? equalityFunction : null;
this.put = function(key, value) {
checkKey(key);
checkValue(value);
var bucketKey = hashingFunction(key);
var bucket = getBucketForBucketKey(bucketItemsByBucketKey, bucketKey);
if (bucket) {
var bucketEntry = bucket.getEntryForKey(key);
if (bucketEntry) {
bucketEntry[1] = value;
} else {
bucket.addEntry(key, value);
}
} else {
var bucketItem = new BucketItem();
bucketItem[0] = bucketKey;
bucketItem[1] = new Bucket(key, value, equalityFunction);
bucketItems[bucketItems.length] = bucketItem;
bucketItemsByBucketKey[bucketKey] = bucketItem;
}
};
this.get = function(key) {
checkKey(key);
var bucketKey = hashingFunction(key);
var bucket = getBucketForBucketKey(bucketItemsByBucketKey, bucketKey);
if (bucket) {
var bucketEntry = bucket.getEntryForKey(key);
if (bucketEntry) {
return bucketEntry[1];
}
}
return null;
};
this.containsKey = function(key) {
checkKey(key);
var bucketKey = hashingFunction(key);
var bucket = getBucketForBucketKey(bucketItemsByBucketKey, bucketKey);
if (bucket) {
return bucket.containsKey(key);
}
return false;
};
this.containsValue = function(value) {
checkValue(value);
for (var i = 0, len = bucketItems.length; i < len; i++) {
if (bucketItems[i][1].containsValue(value)) {
return true;
}
}
return false;
};
this.clear = function() {
bucketItems.length = 0;
bucketItemsByBucketKey = {};
};
this.isEmpty = function() {
return bucketItems.length === 0;
};
this.keys = function() {
var keys = [];
for (var i = 0, len = bucketItems.length; i < len; i++) {
bucketItems[i][1].keys(keys);
}
return keys;
};
this.toQueryString = function ()
{
var result="";
var keys = [];
var values = [];
for (var i = 0, len = bucketItems.length; i < len; i++)
{
result += bucketItems[i][1].entries[0][0]+
"="+bucketItems[i][1].entries[0][1]+"&"
}
result = result.substring(0,result.length-1);
return result;
}
this.values = function() {
var values = [];
for (var i = 0, len = bucketItems.length; i < len; i++) {
bucketItems[i][1].values(values);
}
return values;
};
this.remove = function(key) {
checkKey(key);
var bucketKey = hashingFunction(key);
var bucket = getBucketForBucketKey(bucketItemsByBucketKey, bucketKey);
if (bucket) {
if (bucket.removeEntryForKey(key)) {
if (bucket.size() === 0) {
var result = searchBucketItems(bucketItems, bucketKey,
bucket.getEqualityFunction(key));
arrayRemoveAt(bucketItems, result[0]);
delete bucketItemsByBucketKey[bucketKey];
}
}
}
};
this.size = function() {
var total = 0;
for (var i = 0, len = bucketItems.length; i < len; i++) {
total += bucketItems[i][1].size();
}
return total;
};
}
return Hashtable;
})();
The server-side code:
protected void Page_Load(object sender, 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());
}
}
public void FillPage(UserData userData)
{
}
AutoSave.aspx.cs
protected void Page_Load(object sender, 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;
}
UserData.cs
public class UserData
{
public 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
- If the page form is automatically filled by a web-form-filler software, this won't work.
- In modern browsers, query string support is only up to 2000 chars; data loss happens if that length is exceeded.
References