Introduction
ASP.NET is a fantastic platform for building web applications. One of the main services it provides is the ability to store/cache collected or useful data in memory.
ASP.NET supports scoping your cached data in many ways: at the Application level using Cache
and Application
objects, at the User level using the Session
, for the duration of a specific user interaction with a Page using View State and finally for the duration of the request inside the Context.
Conceptually this looks a little like this:
Background
All of these are useful. However there does appear to be one conspicuous oversight. How do you store information between the chain of requests needed for a user to complete a multistage process?
There are lots of examples of multistage processes, for example:
- A call center application in which a helpdesk employee (Dave B.) enters user issues when they phone the help desk. The whole issue capture process is done in three steps: on the first page Dave captures the user�s details, on the second he captures the details of the bug, and on the final page he captures any miscellaneous comments made by the user.
- A Health and Safety application in which a health officer (Sally), captures information about workplace hazards. On the first screen Sally captures details for the hazard, on the second she captures her ideas and thoughts about how to mitigate the hazard and finally on the third screen she captures her action plan.
You might be thinking that the Session is a good candidate for storing the information captured in each step. However the Session is scoped to a User not to a specific User Process. What is wrong with that? Well, if you have a power user like Dave, who fancies himself as a bit of a PC expert, you�ll find you have users who like to multitask by entering issues simultaneously into multiple windows or tabs. If you used the Session for capturing this information, you could easily lose data as the windows corrupt each other�s data.
So what about ViewState? Well, ViewState has some significant limitations. Firstly, by default, your data must be serialized so it can be sent by the server with each response and returned by the browser with each request. Not all objects are serializable, so this puts limitations on what you can put in the ViewState. Also as the ViewState is sent with each request and response, if it gets too big, perceived performance will suffer. Dave and Sally would get annoyed really quickly if their application takes 5 seconds to load each page! Sure these limitations can be overcome if you hook into and modify the default behavior of ASP.NET ViewState; however this is not for the lighthearted.
A more unavoidable limitation of ViewState is that it is bound to a single page or control. Multistage processes often involve multiple pages, and unfortunately information can�t easily be transferred from the ViewState of one page to the ViewState of another.
So it seems we are lacking an appropriate place to store our information during a multi stage process. I think the solution is to create something called a �ProcessContext�. A ProcessContext is a framework you can use to easily create applications that keep users like Dave and Sally happy.
The key requirements of a ProcessContext are:
- Data put into the ProcessContext is stored on the server.
- ProcessContext can be transferred between pages.
- ProcessContexts are isolated from each other. For example, if a single user initiates two processes, data in one process will not corrupt the other.
Conceptually, this is how things would look if we had a ProcessContext:
Implementing a ProcessContext
The first step to create a ProcessContext
is to create a way of uniquely identifying it. Let�s call this the ProcessID
. Once we have a ProcessID
we can store information for a particular ProcessContext
keyed against that ProcessID
. Where the ProcessContexts
themselves are stored is not really important, all that is important is that we can get back to the right one in the future.
So let's define an interface called IProcessParticipant
which can then be implemented by pages, controls or anything else that you can think of in the future.
The IProcessParticipant
interface looks like this:
public interface IProcessParticipant
{
string AttachProcessToUrl(string url);
string ProcessID {get;set;}
Hashtable ProcessContext{get;}
}
To actually implement a ProcessContext
, I subclass System.Web.UI.Page
and implement the interface in a class called ProcessParticipantPage.
To do this we first override OnInit
checking to see if a ProcessID
has been handed to us via the QueryString
.
protected override void OnInit(EventArgs e)
{
ProcessID = Request.QueryString["ProcessID"];
base.OnInit (e);
}
This allows a ProcessContext
to be easily transferred between pages using the QueryString
. In fact AttachProcessToUrl
is used to take a URL and decorate its QueryString
with the current ProcessID
.
public string AttachProcessToUrl(string url)
{
if (url.IndexOf("?") == -1)
url = url + "?";
else
url = url + "&";
return url + string.Format("ProcessID={0}",ProcessID);
}
The ProcessID
getter and setter use the ProcessID
key in the ViewState of the page. If no ProcessID
is found in the ViewState, then a new one is generated. This ensures a ProcessID
is always available when required.
public string ProcessID
{
get
{
if (ViewState["ProcessID"] == null)
ViewState["ProcessID"] = Guid.NewGuid().ToString();
return ViewState["ProcessID"] as string;
}
set
{
if (value == null) return;
else ViewState["ProcessID"] = value;
}
}
Notice that this implementation uses the ViewState to store the ProcessID
as it is the simplest option available. The ProcessID
is simply a GUID, which is of course not a lot to serialize between client and server, so it adds almost no overhead. An alternative would be to include a hidden field in the web form, i.e. in a similar way to the way ViewState works. A hidden field would be guaranteed to work in all situations, not just when ViewState is enabled. However for simplicity we are using ViewState, so we need to make sure ViewState is always available to our page.
public override bool EnableViewState
{
get
{
return true;
}
set
{
if (value == false) throw new Exception("ViewState is Required");
}
}
Now for clarity we store the information associated with a ProcessContext
under the ProcessID
key of the Session
object, but you could easily change this to use the Cache
object or even a database if you wanted. To ensure flexibility, we use a Hashtable
to actually store information associated with a particular ProcessContext
.
public Hashtable ProcessContext
{
get
{
Hashtable results = Session[ProcessID] as Hashtable;
if (results == null)
{
results = new Hashtable();
Session[ProcessID] = results;
}
return results;
}
}
We now have a complete working ASP.NET page class that can be used as a base class for all pages that participate in a process and need to share information with other pages in the process.
Using the ProcessContext
All you need do to enable the ProcessContext in your web page code is to simply change this line in your code:
public class YourPage: System.Web.UI.Page
to this:
public class YourPage: ProcessParticipantPage:
Once the ProcessContext
is available, you simply use it like any hashtable, for example:
ProcessContext["YourKey"] = data;
string stored = ProcessContext["YourKey"] as string;
ProcessContext.Clear();
As you can see this is very simple, hopefully you will find it useful too.
To show you just how simple it is, I have created a simple ASP.NET application for Sally to capture her Health and Safety hazards, sorry Dave get your own programmer!
The application for Sally has three pages:
- HazardDetails.aspx
- Analysis.aspx
- ActionPlan.aspx
On HazardDetails.aspx in the btnNext
event handler, we grab what is in the form and put it in the ProcessContext
under the appropriate key. Then we attach the ProcessContext
to the next page in the process (Analysis.aspx) and redirect to that URL.
private void btnNext_Click(object sender, System.EventArgs e)
{
string title = this.txtTitle.Text;
string severity = this.txtSeverity.Text;
string description = this.txtDescription.Text;
ProcessContext["title"] = title;
ProcessContext["severity"] = severity;
ProcessContext["description"] = description;
Response.Redirect(this.AttachProcessToUrl("Analysis.aspx"));
}
We do much the same in the btnNext
event handler of the Analysis page, capturing Sally�s ideas and thoughts. Finally on the last page, ActionPlan.aspx, when they hit btnSubmit
, we capture the new plan and schedule from that page and grab what has already been put into the ProcessContext
to submit the completed Hazard and Analysis data to the database in one step.
private void btnSubmit_Click(object sender, System.EventArgs e)
{
string title = ProcessContext["title"] as string;
string severity = ProcessContext["severity"] as string;
string description = ProcessContext["description"] as string;
string ideas = ProcessContext["ideas"] as string;
string analysis = ProcessContext["analysis"] as string;
string plan = this.txtPlan.Text;
DateTime duedate = DateTime.Parse(this.txtDue.Text);
InsertInDB(title,severity,description,ideas,analysis,plan,due);
}
Conclusion
This application is trivial but it shows the power of the ProcessContext
. If you play around with opening HazardDetails.aspx in two windows, you will see how what you enter into one window is isolated from the other. So should Sally ever become a power user, this application will be ready!
In this article, I have demonstrated a simple way to add a ProcessContext
to your ASP.NET applications; this can be useful in many situations, especially in Wizard like processes that can be run in multiple windows simultaneously by the same user. I hope you find this example helpful.
Remember it�s all about Context!
Update
ASP.NET 2.0 will provide a feature called Cross Page Posting which allows you to post your web form to the next page in a sequence, this next page then has access to the PreviousPage from which you can access information from the previous form. This can help with Wizards but is still not as powerful as a full fledged ProcessContext
.
Update #2
With Thomas Eyde and Hzi's suggestions I have created a standalone version of the ProcessContext
that exposes a static
property ProcessContext.Current
from which you can assess the ProcessContext
data.
It works by looking for a ProcessID
first on the QueryString
, then on the Form
for a hidden field called __PROCESSID
and finally if nothing is found, it creates a new ProcessID
and registers the hidden field to store the ProcessID
between Postbacks if you haven't yet got the QueryString
to contain the ProcessID
.
You can still ProcessContext.Current.AttachProcessToUrl
as before, there is also a convenient ProcessContext.Current.Redirect
method that does an AttachProcessToUrl
and then a Response.Redirect
.
Additionally, if you are concerned about the size of what you are storing in the Session
you can use ProcessContext.Current.Discard
to free your memory.
Now with this version, all you need to do is this:
ProcessContext.Current["MyData"] = value;
string myData = ProcessContext.Current["MyData"] as string;
ProcessContext.Current.Redirect("Page2.aspx");
Perhaps a new article is in order?