Dot Net Tricks

Articles about .NET, ASP.NET, C#, Object Oriented Programming and Agile Methodologies
Welcome to Dot Net Tricks Sign in | Join | Help
in Search

Software Theosophy

ASP.NET Wizards and Session Variables

I love Session.  I know it’s dirty and wrong but Session is so easy.  In the old days of ASP classic, you had to be careful with putting things like COM objects into Session (apparently most COM objects were not thread safe, but correct me if I’m wrong on that.)  But now you can put .NET objects into session without too many problems.  Session does what I expect it to do.  It makes things more like a windows forms application.  I know I can use ViewState, but Session is so much cleaner.  For instance:

 

  • Session, unlike ViewState, can be used to pass objects between pages.  You can also put things into ViewState and then use Server.Transfer() but the URL doesn’t change which can be confusing for the user and you get weird behavior if the user clicks the Refresh button right after they’ve performed some transaction.
  • Session doesn’t have to be Serializable.  It can be painful putting the [Serializable] attribute on every single business class in your business layer.  Even if you change your ViewState persistence mechanism to store its data in Cache or Session, by overriding SavePageStateToPersistenceMedium() and LoadPageStateFromPersistenceMedium() anything you put in ViewState still has to be serializable.
  • Since ViewState serializes anything put into it, you break object references.  For example, if I run the following code:

    this.ViewState["customer"] = this.CurrentCustomer ;
    this.CurrentCustomer.FirstName = "bob";

    Does the FirstName property in the ViewState[“customer”] variable that is in viewstate change to “bob”?  No, it doesn’t.  Anything in ViewState is a copy ob that object, not a reference, even if you are putting reference types into ViewState.  This can make your pages pretty complex as you have to make sure to update any references after you pull your objects back out of ViewState.  With Session, you don’t have to do this unless you’re storing session in a separate server or sql server.

 

At the same time, I also hate session.  Worse still, my boss hates Session.  Why?  Because Session, since it is basically a global variable, is shared by EVERY page the current user has open, even two instances of the same page.  This can lead to serious problems.

 

To illustrate what I’m talking about, let’s pretend we have a wizard which updates customer records in our ordering processing system.  The customers are listed on a page using a data grid, and when the user clicks on one they are sent to the “Edit Customer” wizard.  We want to have 3 screens in this wizard, one to edit the customer’s basic information, such as name and email, another to edit their various shipping addresses, and one screen to review everything before they save this data back to the database.

 

I can store the customer’s information in a session variable to be used by this wizard.  However, I have to be careful.  For example, say I store the customer object that I’m editing in the first page of the wizard in session.  I might do something like this:

 

Session["CurrentCustomer"] = CustomerDAL.GetCustomer( Request.QueryString["customerid"]);

 

Here I’m getting the customer object from my Data Access layer based upon the Customer’s primary key, which was passed on the query string.  On subsequent pages of the wizard, I simply have to pull the customer back out:

 

this.CurrentCustomer = (Customer)Session["CurrentCustomer"];

 

This way, I can go back and forth in my wizard and use the same customer object, update it multiple times without persisting anything to the database.  I merely reference the Customer object I have in session.  Everything is great.

 

Well, sort of.  Imagine this scenario:

 

Larry is a customer service representative.  He’s using our system and needs to update some information for a customer named “Bob Smith.”  On the page that lists all the customers he sees the link to the customer “Bob Smith”.  But Larry doesn’t want to with for the customer list to load each time he needs to edit something, so he right clicks on this link and the uses “Open In New Window” feature of their browser.  This pops him into the Edit Customer Wizard and Larry merrily edits some of the customer fields in the wizard.  He is just about to click the “Next” button when his boss comes running by and asks him to look at Sally Jones’ customer record.

 

So Larry holds down “ALT+Tab” (apparently Larry is a power user) and switches windows to the Customer listing page again, right clicks on Sally Jones and open another new browser window.  Keep in mind that the session variable holding our customer object now changes to store Sally’s data.  Larry and his supervisor review Sally Jones’ information.  Satisfied that everything looks fine, they close her window.

 

At this point, Larry goes back to what he was doing.  With another “ALT+Tab” he’s back to Bob Smith’s window.  Larry sees all of Bob’s information that he just finished editing.  Knowing that this is all he needed to change, Larry clicks the “Next” button a couple of times and moves to the last step of the wizard, which is the review page.  Then he hits the save button.  Bad things  happen.

 

What’s going on?  Well, unbeknownst to Larry, he just saved Sally’s customer object, but with Bob’s FirstName, LastName, Email and Password in it.  This is because when Larry switched back to Bob’s window, the web server was actually still holding Sally’s customer object in session.  When Larry clicked the “Next” button, the web form populated the customer object with the data in the textboxes, which were already holding Bob’s information, not Sally’s.  Worse, when Larry clicks “Save” at the last step of the wizard, these fields will be overwritten in the database too.

 

This has actually happened in a few applications that I’ve written.  We’ve had to train users not to open a new window on certain screens (especially wizards) and not to use the Back/Next buttons in their browser.  Tabbed browsing could become off limits as well.  This limits the user’s ability to take advantage of the capabilities of the browser and is less intuitive.  Part of the problem is that the web was never meant to be stateful.  It was meant to browse static documents.  Any attempt to get around this tends to have certain drawbacks.

 

So I’ve started to become wary of using Session as it can come back to bite you later on.  However, I’ve recently come up with a technique that works around this problem.  This solution might be very basic common sense to many developers, but I’m hoping some of you will find it useful.

 

Instead of having one single session variable to hold the Customer object called Session[“CurrentCustomer”], make the key more unique.  The key could be based upon the primary key of the record in the database, which would be passed to each step of the wizard on the query string.  So the name of the session variable would change to Session[“CurrentCustomer_” + Request.QueryString[“CustomerID”].  This creates a unique key for each customer in the database. 

 

I’ve come up with a sample web project using this technique, as well as other methods I use when passing objects around between pages in a wizard.  I’ll go over the highlights of the code here and you can download the whole thing as a zip file.  I’ve left out a database for simplicity, and instead merely mimicked a data access layer that would pull the business objects out of the database, saturate their fields and return them to the presentation layer.

 

The first thing I do in the sample code is have all the pages of the wizard inherit from a base class.  This base class holds all the code for pulling the business object out of the data layer, creating a unique key for the session and putting the object into the session.  You can put any other code in this base class that is common to all steps of the wizard.


To start with, I made a property that creates the unique key:

 

private string CustomerKey

{

      get

      {

            return "EditCustomer_" + Request.QueryString["customerid"];

      }

}

 

This is pretty simple stuff here.  I assume that the customerID is passed on the query string.  In a real application, you might want to add some code that checks whether this query string parameter exists. 

 

Next I make a property to pull the customer the right place.  If the Customer object already exists in session, I pull it from there.  If its not already in session, I pull the object from the DAL and store it in session.  On each of these steps I use the CustomerKey property above.

 

protected Customer CurrentCustomer

{

      get

      {

            //if this customer isn't in session, pull it from the DAL

            if (Session[this.CustomerKey] == null)

            {                      

                  Session[this.CustomerKey] = CustomerDAL.GetCustomer( new Guid( Request.QueryString["customerid"] ));

            }

 

            return (Customer)Session[this.CustomerKey];

      }

}

 

Every page of the wizard can then reference this property to get the Customer from session, based upon the unique key we created in the previous step. 

 

Note that in this example I used Guids for the primary keys which were generated by the business object itself when it is first created.  I also didn’t create an example of adding a new customer, only editing them. 

 

If you’re using identity fields for your primary keys, and it’s a new customer being created, you would have to create fake identity keys using negative numbers or something and pass those on to the query string.  This way if the user creates more than one new Customer at the same time, they still use separate session variables.  After new customer objects get saved, they could then update their key with the real identify field.

 

Of course, when the user is done editing the customer, you’ll need to clear that key out to conserve memory. So this method should get fired whenever the user clicks “Cancel” or “Save.” 

 

protected void ClearCustomer()

{

      Session.Remove(this.CustomerKey);

}

 

Each step of the wizard then just needs to interact with the CurrentCustomer property that it inherited from the base class above.  For example, on the first step of the wizard, I databind to the CurrentCustomer object in my HTML to populate all the text boxes:

 

 

<form id="Form1" method="post" runat="server">

<H2>Step 1: Basic Customer Info</H2>

<P>Customer ID#:<BR>

<asp:Label id="CustomerIDLabel" text='<%# this.CurrentCustomer.CustomerID.ToString() %>' runat="server" Width="464px"></asp:Label></P>

<P>Customer

Name:<BR>

<asp:TextBox id="FirstNameText" text='<%# this.CurrentCustomer.FirstName %>' runat="server"></asp:TextBox>&nbsp;

<asp:TextBox id="LastNameText" text='<%# this.CurrentCustomer.LastName %>'  runat="server"></asp:TextBox></P>

<P>Email Address:<BR>

<asp:TextBox id="EmailText" text='<%# this.CurrentCustomer.Email %>'  runat="server"></asp:TextBox></P>

<P>Password:<BR>

<asp:TextBox id="PasswordText" text='<%# this.CurrentCustomer.Password %>'  runat="server"></asp:TextBox></P>

<P>

<uc1:BackNextControl id="backnextcontrol" runat="server"></uc1:BackNextControl></P>

 

 </form>

 

Later I take all the basic information from the text boxes and update the customer object when the user moves to the next step:

 

private void PopulateObject()

{

      this.CurrentCustomer.FirstName = this.FirstNameText.Text;

      this.CurrentCustomer.LastName = this.LastNameText.Text;

      this.CurrentCustomer.Email = this.EmailText.Text;

      this.CurrentCustomer.Password = this.PasswordText.Text;

}

 

Remember, when I perform these steps I’m using the CurrentCustomer property each time which looks up the Customer Object in session, not from a local field.

 

That’s pretty much it.  The user can pop open new windows to their hearts content, and each instance of the wizard will have its own session variable based upon its own unique URL. 

 

There are a couple drawbacks to this method, or at least to the code example that I have worked on so far:

 

  • There is a memory hit because every instance of the wizard for every user holds a business object, especially if that business object has a large object graph.  Hopefully your users will not open too many separate instances of the wizard, but don’t count on it.
  • I mentioned that if the user finishes the wizard and saves the Customer object, or if they cancel out of the wizard, you should call the ClearCustomer() method to remove that object from session since it is no longer being used.  However, if the user simply navigates to a new site or another page, you’ll have to invent some other way of cleaning out these objects.
  • Session variables are still basically global variables, even when using this technique.  Global variables are bad. There is no guarantee that some other part of your application or some other developer working on your team might not accidentally override these session variables by using the same key in some other part of the application.  To get around this, I tend to create a single class in my web application to manage all session variables, rather than putting the properties into each of the pages like I did in this example.  This class acts as a broker for any session access and all developers on this project should agree to update this class whenever they need to create any new session variables.  This way there is only one place in the code to see what keys are already in use.

 

So don’t go crazy with Session.  It can be very convenient but you can love it a little too much.

Published Saturday, October 01, 2005 8:51 PM by Fregas
Filed Under: ,
Attachment(s): BetterSession.zip

Comments

 

snook.ca - a collection of tips, tricks and bookmarks in web development said:

Although the article is .NET specific, the concept can be applied to any language. The problem is, how to retain certain information from one page to the next. The example really explains it all....
April 27, 2006 8:47 AM
 

bennage said:

How about using Dicitonary<string,Customer> where the string is the primary key?  It might save a little bit of the plumbing code.
I also have a situation where I want to store page specific information in the Session.  As long as the user is clicking around on the same page, the state stays in the Session.  When the user nagivates to another page the session variables that handle the page state are cleared.  I have code that checks the url with each request and if the page has changed, it disposes of the relevant variables.
April 27, 2006 9:34 AM
 

Ellis Web » Items of Interest: 2006.05.05 said:

May 6, 2006 3:23 PM
 

session variable in vb net said:

May 20, 2008 3:56 PM
Anonymous comments are disabled

About Fregas

Craig is currently the Lead Developer in Fort Worth, Texas for Enilon Group, a web development firm. He has been programming since 3rd grade (using the Commodoore PET) and professionally for the past 7 years. He has written several articles for ASPToday.com and co-authored the book "Beginning Web Programming using VB.NET and Visual Studio .NET" Currently, his favorite programming language is C#, but he has programmed in Visual Basic, T-SQL, Ruby, ColdFusion, ASP 3.0/VBScript, ASP.NET, Javascript, Java and even Pascal. Besides programming, Craig is best known for his cooking and his somewhat offbeat sense of humor.

This Blog

Post Calendar

<October 2005>
SuMoTuWeThFrSa
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

Syndication

Powered by Community Server, by Telligent Systems