Recently I deployed an ASP.Net web application that was designed to gather complex form data and store it in a database. The object model was large and complex enough that it couldn’t be stored in View State, given the network within our organization. So instead we stored the form data in Session state. Simple enough, server memory is comparatively cheap. During 10 months of requirements meetings however, it never came up that users might want to have multiple forms open at the same time, each looking at different instances of the business data. And this week, precisely that happened.
What does that spell? That’s right: M-A-Y-H-E-M.
One of our users was especially enterprising, and thought that she’d save time on data entry by opening up different instances of the form in different IE browser windows and cut and paste common information from one to the other. It’s a perfectly reasonable thing to assume. However, the forms weren’t designed with that usage pattern in mind. Specifically we only kept one instance of the form data in Session state at any time.
ASP.Net Session state can be managed by either storing a session ID in a cookie or in a URL query string. We were using cookie mode since we were already stuffing the query string for other purposes. Unfortunately, this means that multiple IE browser windows will fetch the same cookie from disk, send it, and thus get the same session. In our application, that means that new instances of the form will overwrite old single-instance form data in server memory, and instances of the form running on older browser instances could still be out there dangerously out of sync with the server.
The solution is surprisingly simple. The basic idea is to store multiple versions of form data in Session state indexed by a sequence string. This sequence string is regenerated on every postback and written out to View State at PreRender. On subsequent postback Load, the sequence string is read back and used to retrieve the correct form data. The key is to always mange form data instances and the sequence string together. In my project, I put this in a base class for the code behind. This base class will allow the user to work with many instance of form data. It even protects against accidental data corruption when the user opens a window with Ctrl-N, works in the new window for a while, and then tries to some back to the old window. There is also some built in protection if the user tries to load multiple instances of the same form data through other means (eg- searching for it twice in a search application and opening it each time). Finally, some functionality providing for keeping form data open across Server.Transfer() is provided.
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
///
/// A base class for code behind that adds ability to broker
/// multiple instances of business data among concurrently open
/// browser windows.
///
public abstract class CodeBehindBase : System.Web.UI.Page
{
///
/// An overridable function to return new instances of form data to load when there
/// is no postback.
///
protected abstract object NewFormData();
///
/// Returns an object of form data for the current sequenceID.
/// This is for use in the derived class.
///
protected object GetFormData()
{
object retval = GetFormData(sequenceID);
if (retval == null)
{
throw new Exception("Form data was not found in the session. This could have been caused by " +
"opening a duplicate window using \"Ctrl-N\". Close this window and the duplicate window " +
"should be OK.");
}
return retval;
}
///
/// Returns an object of form data if the key is present.
/// If not, returns null.
///
private object GetFormData(string sid)
{
Dictionary formDataObjects = (Dictionary)Session["FormDataObjects"];
if (formDataObjects.ContainsKey(sid))
{
return formDataObjects[sid];
}
return null;
}
private string sequenceID;
///
/// A sequence ID to identify this web page
///
public string SequenceID
{
get { return sequenceID; }
}
///
/// Generates a new sequence ID.
///
private string NewSequenceID()
{
// Just using the clock with one second resolution is sufficient for most cases.
// If you need to worry about more that one browser window open per second,
// use milliseconds.
return DateTime.Now.ToString();
}
///
/// Links a particular sequence ID to a particular instance of form data
///
private void LinkFormData(string sid, object formData)
{
Dictionary formDataObjects = (Dictionary)Session["FormDataObjects"];
foreach (object o in formDataObjects.Values)
{
if (formData.Equals(o))
{
throw new Exception("Tried to load multiple instances of the same form data.");
}
}
formDataObjects[sid] = formData;
}
///
/// Unlinks a particular sequence ID from its instance of form data
///
private void UnlinkFormData(string sid)
{
Dictionary formDataObjects = (Dictionary)Session["FormDataObjects"];
if (formDataObjects.ContainsKey(sid))
{
formDataObjects.Remove(sid);
}
}
///
/// ASP.Net OnInit
///
protected sealed override void OnInit(EventArgs e)
{
// Adds a hidden field to the control tree to hold the sequencce number
// in ViewState so we can identify the browser window. Must be added here
// or else it will miss having ViewState loaded.
HiddenField hdSequenceID = new HiddenField();
hdSequenceID.ID = "hdSequenceID";
hdSequenceID.Value = "";
Form.Controls.Add(hdSequenceID);
// Initializes the data structure for linking sequence IDs to form data
if (Session["FormDataObjects"] == null)
{
Session["FormDataObjects"] = new Dictionary();
}
// Page_Init is called here if it is defined in the derived class.
base.OnInit(e);
}
///
/// ASP.Net OnLoad
///
protected sealed override void OnLoad(EventArgs e)
{
// Set the SequenceID coming back from the page.
sequenceID = ((HiddenField)Form.FindControl("hdSequenceID")).Value;
if (!Page.IsPostBack)
{
// Get an instance of form data to use
object formData = null;
if (Page.Request.Params["SequenceID"] != null)
{
// If this is a Server.Transfer, check if the SequenceID is in the query string.
// If so, load the form data from the old page. If you're worried about spoofing,
// you'd probably want to turn this off.
string sid = Page.Request.Params["SequenceID"];
formData = GetFormData(sid);
UnlinkFormData(sid);
}
else
{
// Otherwise get new form data.
formData = NewFormData();
}
// If not a postback, then the seuqenceID would have the value set in OnInit.
// So create a new sequence ID here and link it to the form data.
sequenceID = NewSequenceID();
LinkFormData(sequenceID, formData);
}
base.OnLoad(e);
}
///
/// ASP.Net PreRender
///
protected sealed override void OnPreRender(EventArgs e)
{
// Regenerate the sequenceID on every postback. This prevents problems arising when the
// user opens up multiple browser windows with "Ctrl-N". The new window will still work,
// but older windows will safely error out.
// Get the form data
object formData = GetFormData(sequenceID);
// Unlink the old sequence ID
UnlinkFormData(sequenceID);
// Generate a new sequence ID
sequenceID = NewSequenceID();
// Relink the form data
LinkFormData(sequenceID, formData);
// Write the sequence ID out to the page.
((HiddenField)this.FindControl("hdSequenceID")).Value = sequenceID;
// Calls Page_PreRender.
base.OnPreRender(e);
}
}
To get the features, the user must implement the NewFormData() method to provide blanks instances of form data when the page is not a postback, use GetFormData() in the derived class code behind for handling the form data, implement Equals() on the form data object so that the base class can tell when duplicates are added (if needed), and when transferring to another cooperating page using Server.Transfer the user can specify the SequenceID in the query string to maintain state.Bulletproof! I’ll write a quick article on this for the code project soon with examples and post the link here.
On a side note: ever hear of CommVault? It’s a great enterprise level backup system that has the capability to backup and restore SQL Server databases. I put it to good use this week. But honestly, we’ve been rolled out for more than a month already, processed more than 200 forms, and this is the first time we’d seen this, and it affected only one form for one user.
Update 5/29/2007: The code project article is up!
Ideally, one would like a different session variable for every open browser window. So it would be nice if we can reuse the same session variable for different pages, while we are in the same browser window.
Generating a new sequence ID whenever a page posts back for the first time would work. But that means, for each form, you will have a new entry in session, even if that form is open in the same browser window.
How do you delete old entries from session? Wouldn’t you eventually run out of memory using this method ?
[...] then it will probably be better to generate a GUID and store that in viewstate, using the GUID to access a part of sessionstate which can be kept unique for that instance of the page, regardless of the [...]
right, that would work,
but in my case I’m saving viewstate in session… so i can’t access that GUI ID from viewstate because i need it to index session and find my viewstate…
Anywho, the solution i’m investigating now is to make sessionstate cookieless, and to make sure all popups are generated from a different function which initializes a new session id…
I could not get your solution work when the user hits the Refresh button on their browser. Does it work for you?
Have you found a way around to page refresh issue?
Hi, this is a pretty cool idea, one thing is it only seems to work for forms (not that it claims to do otherwise), is there a way to store a viewstate field on every call regardless of if it is a postback? I want to use a similar technique to store a tab bar state (which tabs & which selected) in the session but have it not break with new browser windows.
Hi, we have the same problem. Our web app. stores a complex objects in a session, therefore if that get duplicated we risk loosing data or multiplying numbers. I’ve seen very similar solution on other site, however as someone pointed out before it crashes when user wants to refresh the page. Is there any way of avoiding that and detecting page refresh???