This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


MIND

Aaron Skonnard

Client Persistence

Download the code (23KB)

H
TTP is completely stateless. The protocol itself does not offer any built-in mechanism for keeping track of users and their activity within a Web application. If the user makes two HTTP requests within a few seconds of each other, HTTP has no way of associating them. The process of identifying and grouping distinct HTTP requests from the same client is what most developers call session management. As long as you can recognize that an HTTP request comes from a particular user, you can begin to associate state with that user for a given period of time.
      Developers who want to maintain session state for their users must use a higher-level technique that piggybacks the HTTP protocol. It's up to the developer, however, to decide how to identify a user session, how long it should last, and where the session state should be stored.
      Consider the typical shopping cart functionality. Most online shopping sites offer their users the notion of a shopping cart for keeping track of different items they want to purchase. A user will browse through the site, find products to buy, and add them to the shopping cart. At any point during the session, the user can view this shopping cart and see everything that's been added from previous HTTP requests.
      So, the question remains, how do you manage user sessions to implement something as typical as an online shopping cart?

userData Client-side Shopping Cart

      As an introduction to the discussion of userData and other state techniques in this column, I've included a sample application that illustrates how to create a client-side shopping cart using the userData behavior. You can download the sample from the link at the top of this article.
      The sample application lets users browse a catalog of books and add them to a shopping cart (see Figure 1). At any point in a session, the user can view the contents of the shopping cart along with the total price of the order (see Figure 2). When the user is ready to check out, the contents of the shopping cart are sent to the server and processed by an ASP page (see Figure 3).

Figure 1: Browsing Book Catalog
      Figure 1: Browsing Book Catalog

Figure 2: Viewing Shopping Cart
      Figure 2: Viewing Shopping Cart

Figure 3: Processing a Purchase
      Figure 3: Processing a Purchase

      The code for this sample is consolidated within three files: browse-inventory.htm, shopping-cart.htm, and checkout.asp. Browse-inventory.htm (see Figure 4) manages the process of viewing the inventory data, which also happens to be stored as XML, and adding books to the shopping cart. Shopping-cart.htm (see Figure 5) manages the process of viewing the shopping cart data and posting it to the checkout.asp file. Finally, checkout.asp (see Figure 6) simply processes the shopping cart data and returns a response to the client.

A Cookie, Please?

      Cookies have long been the standard technology for identifying users across distinct HTTP requests. A cookie is simply a hidden piece of data that Web servers can send to a client via HTTP through the Set-Cookie header. Browsers must also support cookies (and the user has to have this support turned on) for this to work. As long as the browser supports cookies, it will save the cookie data to persistent storage upon receiving the HTTP response with the Set-Cookie header. Every time the user revisits the same Web application, the browser will send the saved cookie back to the Web server through the Cookie header. This entire process has been standardized by the W3C as outlined in RFC 2109, HTTP State Management Mechanism (see http://www.w3.org/protocols/rfc2109/rfc2109).
      If the Web server receives a request that contains a cookie, it can use that information to establish context for that given user. If the Web server receives a request that doesn't contain a cookie, it can assume that the request is coming from a new user. It's very common for the cookie to contain a unique session identifier that the server can use to look up the user's state information, which is stored elsewhere (such as in a SQL Server database).
      The cookie could also contain the raw data that represents the current state of the user's session. For example, in the case of the shopping cart, the cookie could contain the book ISBNs that the user wants to order. This offloads the information from your server and simplifies your design because you no longer have to tie up valuable server resources for each user session. Instead, you can take advantage of the client machine's resources without sacrificing functionality.
      As mentioned earlier, cookies are passed between the browser and the Web server through the standard HTTP headers, Set-Cookie and Cookie. These headers contain a list of name/value pairs that represent the cookie data. Let's take a look at an example that illustrates how you can implement a simple shopping cart using cookies.
      The first time a new user accesses the application, the Cookie header will be absent from the HTTP request. At this point, my application will generate a unique session ID for this user (UserID="aarons-00001") and send it back to the client in the Set-Cookie header in the HTTP response:


 HTTP/1.1 200 OK
 Set-Cookie: UserID="aarons-00001"; $Path="/mind"; $Max-Age="1200";
      The $Path and $Max-Age values control the behavior of this particular cookie. $Path="/mind" tells the browser that this cookie only applies to files under this virtual directory. After the browser processes this cookie, all subsequent requests to files under this directory will also contain this cookie. $Max-Age="1200" tells the browser that this cookie should only last for 1200 seconds (20 minutes); this controls the lifetime of the user session. If the $Max-Age value is absent, the browser holds the cookie in memory until the browser session ends.
      At this point, I've established a user session and have a mechanism for identifying the user on all subsequent requests. Let's assume the user browses to the book inventory page and selects a book she wants to purchase. The following HTTP request represents this action:

 POST /mind/add-to-shoppingcart.asp HTTP/1.1
 Cookie: UserID="aarons-00001"; $Path="/mind";

 isbn=0201379368
Notice that the UserID comes across as part of this request. Now I can identify which user this request belongs to.
      At this point, the application will add the specified ISBN to the user's cookie in the HTTP response:

 HTTP/1.1 200 OK
 Set-Cookie: UserID="aarons-00001"; ISBNs="0201379368"; $Path="/mind"; 
     $Max-Age="1200";
From now on, the ISBN will also go across the wire with every HTTP request within the /mind virtual directory. If the user were to add another book to her shopping cart through a separate HTTP request, it would look like this:

 POST /mind/add-to-shoppingcart.asp HTTP/1.1
 Cookie: UserID="aarons-00001"; ISBNs="0201379368"; $Path="/mind"; 

 isbn= 0201634465
Notice that the old ISBN number is still part of the cookie. The newly generated cookie will now contain a list of ISBNs in the HTTP response:

 HTTP/1.1 200 OK
 Set-Cookie: UserID="aarons-00001"; ISBNs="0201379368, 0201634465";
     $Path="/mind"; $Max-Age="1200";
      At this point, if the user wants to purchase all the items in her shopping cart through the /mind/checkout.asp file, she will have access to all the ISBNs that come across as part of the HTTP request:

 POST /mind/checkout.asp HTTP/1.1
 Cookie: UserID="aarons-00001"; ISBNs="0201379368, 0201634465";
     $Path="/mind";
The checkout.asp file simply needs to pull the UserID and the associated ISBNs from the cookie in order to process the book order.

Cookie Restrictions

      While the use of cookies is a common approach for maintaining state on the client machine, it does come with some fairly significant restrictions. The first has to do with the actual cookie description size. Although different browsers can impose different restrictions, RFC 2109 states that user-agents should at least restrict the size of a given cookie description to 4096 bytes. Second, this document states that only 20 cookies should be allowed from a single domain. Third, no more than 300 cookies should exist on the client machine (from all domains combined).
      If you're using cookies to store a logical ID on the client machine that references state stored somewhere else (such as in a database), these three restrictions aren't a problem. However, if you want to employ cookies for maintaining more structured data on the client machine, such as the actual shopping cart data itself, these restrictions will definitely limit your solution.
      As you can see from the previous example, cookie technology requires close cooperation between the browser and the Web server. If the Web server tries to send a cookie to a browser that doesn't support cookies—or even a browser that has cookies turned off—it's completely ignored. While this doesn't cause a problem for the browser, it will cause your Web application to function improperly since the Web server won't be able to identify users across separate requests. The Web application is going to think that every request from a non-cookie browser should get a new session.
      Not surprisingly, most large Web sites that want to serve all users regardless of their browser preference find this unacceptable. While some developers revert to less sophisticated techniques like using embedded query string data or hidden form fields, these solutions leave much to be desired.
      Besides these obvious cookie restrictions, cookies by nature are not structured. This makes it very difficult to map certain types of structured application data (such as objects in memory) to a set of name/value pairs within a cookie. To truly solve this problem, another mechanism is needed for storing state on the client machine that will allow you to maintain the data's structural meaning.

Internet Explorer 5.0 Persistence Behaviors

      Microsoft Internet Explorer 5.0 introduced a set of default behaviors that address this very issue. The Internet Explorer 5.0 persistence behaviors offer mechanisms for storing state on the client machine that help overcome the limitations of cookies. Each of the four persistence behaviors addresses a different, but common, client persistence scenario. Figure 7 describes each of the behaviors and how they may be used.
      As with all behavior technology, you apply these behaviors to HTML elements through the Cascading Style Sheet (CSS) behavior style using standard CSS selectors. You specify a default Internet Explorer 5.0 behavior through the following syntax:


 #default#behaviorname
For instance, to apply the userData behavior to all elements of class userData, you can use the following CSS selector:

 <STYLE>
    .userData  { behavior: url(#default#userData) }
 </STYLE>
 <DIV ID=divData CLASS=userData>This DIV has persistence capabilities</DIV>
      Once you've applied one of these behaviors to an HTML element, you can start taking advantage of the particular behavior's persistence functionality. Let's look at each of these behaviors in more detail and compare them with the more standard cookie technology discussed earlier.

saveSnapshot

      The saveSnapshot behavior is the simplest of all persistence behaviors. saveSnapshot can persist form values, styles, dynamically updated content, and even scripting variables in a <SCRIPT> block. The saveSnapshot behavior is triggered when the user saves the Web page locally through the File | Save As | Web Page menu option. After the user saves the document, the persistent data is actually embedded within the HTML file saved to disk.
      If your Web application makes heavy use of HTML forms and you want to give your users the capability of saving a copy of the filled-out form, saveSnapshot will definitely come in handy. To take advantage of the persistence behaviors, you must include the following <META> tag at the top of your HTML file that tells Internet Explorer what you're trying to do:


 <META NAME="save" CONTENT="snapshot">
Note that userData is the only behavior that doesn't require this. Next, you simply apply the behavior to an HTML form using a CSS selector:

 <HTML>
 <HEAD>
 <META NAME="save" CONTENT="snapshot">
 <STYLE>
    .saveSnapshot {behavior:url(#default#saveSnapshot);}
 </STYLE>
 </HEAD>
 <BODY>
 <INPUT class=saveSnapshot type=text id=oPersist>
 </BODY>
 </HTML>
      If the user types "Hello World" in the oPersist <INPUT> element and then saves the HTML file to disk, the persisted HTML file looks like this on disk (for clarity I've omitted some of the other <META> tags that are generated):

 <HTML>
 <HEAD>
 <META NAME="save" CONTENT="snapshot">
 <STYLE>
    .saveSnapshot {behavior:url(#default#savesnapshot);}
 </STYLE>
 </HEAD>
 <BODY
 <INPUT class=saveSnapshot type=text id=oPersist value="Hello World">
 </BODY>
 </HTML>
This behavior inserts the value attribute into each <FORM> element and sets its value to the data entered by the user.
      The saveSnapshot behavior also makes it possible to persist script variables within an HTML <SCRIPT> element. As long as the <SCRIPT> element has an ID attribute and is bound to the saveSnapshot behavior, all of its string, Boolean, and integer variants persist to the file when the user chooses to save. For example, a variable whose current contents will be persisted into the HTML file on disk when the user saves is shown in the following script block.

 <SCRIPT CLASS=saveSnapshot ID=scriptPersist>
   var data = "";
 </SCRIPT>
Assuming this event handler is executed before the user saves the HTML file to disk

 function button1_onclick() { 
      data = "This text did not exist in the original SCRIPT block";
 }
the following script element would appear in the file that is actually saved to the disk:

 <SCRIPT CLASS=saveSnapshot ID=scriptPersist>
   var data = "This text did not exist in the original SCRIPT block";
 </SCRIPT>
      The saveSnapshot behavior gives you a simple, yet powerful, mechanism for storing state within HTML files that are persisted to disk by users. Once persisted to disk, your users will have access to the saved state when they return to those persisted files in the future.

saveFavorite and saveHistory

      The saveFavorite and saveHistory behaviors work the same way. The only significant difference between them is where the data lives and how the behavior is triggered. Both behaviors share the same set of properties, methods, and events that are accessible through elements using the behavior (see Figure 8).
      The saveFavorite behavior is triggered when the user adds the current page to the browser's Favorites menu. At that point, you can write code which will save data to the favorites shortcut when it's written to disk (through the onsave event). When the user returns to the page via the same favorites shortcut, the persisted data in the shortcut is available to you in the onload event that fires.
      The saveHistory behavior, on the other hand, is triggered when the user navigates away from the current page and when the user returns to the same page that persisted the data. Like saveFavorite, saveHistory also uses the onsave and onload events to identify these actions. However, unlike saveFavorite, saveHistory stores the data in memory, not to disk.
      You can think of the saveHistory behavior as offering similar functionality to the ASP Session object. It allows you to save structured state—which can be accessed from any page within the Web application—to memory. The difference is that you're using the client's memory to free up valuable server resources. Being able to store structured state in memory on the client machine is a very compelling concept because it solves many of the common session management problems that haunt the ASP session management approach (pinning users to a single Web server, for example).
      As you saw with the saveSnapshot behavior, you don't have to write any code to make the <FORM> or <SCRIPT> elements persist to disk. You simply associate the saveSnapshot behavior and everything is persisted automatically when the user saves the file to disk. With saveFavorite and saveHistory, however, you explicitly save the desired data through the setAttribute, getAttribute, and removeAttribute methods (see Figure 8). These methods allow you to associate arbitrary name/value pairs with the given element. If you're using saveFavorite, the name/value pairs will be saved to the shortcut file. If you're using saveHistory, they'll be saved in memory for the lifetime of the browser session.
       Figure 9 shows how you can take advantage of saveFavorite to store a custom property (called myValue) to a favorites shortcut. As you can see, the <INPUT> element (oPersist) is using the saveFavorite behavior and has specified event handlers for both onload and onsave. You load and save the custom name/value pairs in these event handlers. This approach is definitely more flexible than the saveSnapshot behavior since you can actually specify the data you want to persist. However, simply saving name/value pairs isn't much better than using cookies. Cookies also allow you to save name/value pairs either in memory or on disk.
      If you could save this custom data to disk using a more structured language like XML, then you would have much more flexibility and control over storing more sophisticated application state on the client. Ah, but wait—both of these behaviors expose an XMLDocument property (see Figure 8).
      The underlying mechanism used to supply the setAttribute, getAttribute, and removeAttribute methods is simply an XMLDocument. When you associate these behaviors with a given element, it will have an associated XMLDocument that you can use for your data. When you call setAttribute, you're actually just setting an attribute value in the root node of the associated XMLDocument. Since the XMLDocument is exposed through the XMLDocument property, you can create structured XML documents that more fully describe your application state.
      Take a look at saveHistory in Figure 10, which adds additional child nodes to the XMLDocument associated with the behavior element. The fnSave function is going to create the XMLDocument and save it in memory for the lifetime of the browser session. In this case, the XMLDocument that persists for the oPersist element looks like this:


 <ROOTSTUB>
     <BOOK Title="Essential WinInet" NumberOfCopies="1" 
      ISBN="0201379368">
         <AUTHOR id="aarons">Aaron Skonnard</AUTHOR>
     </BOOK>
 </ROOTSTUB>
      The XML structures you can associate with the different HTML elements give you much more flexibility than traditional cookies. Both saveFavorite and saveHistory expose the XMLDocument property, but the property is only available within the onload and onsave events. This means you can only interact with the XMLDocument object when the page is either loading or saving. For the most flexibility, you'll want to check out the userData behavior.

userData

      The userData behavior is, in my opinion, the most flexible of the persistence behaviors in Internet Explorer 5.0. It's similar to both the saveFavorite and saveHistory behaviors in that it also exposes the getAttribute, setAttribute, and removeAttribute methods, along with the XMLDocument property (see Figure 11).
      The userData behavior allows you to persist arbitrary XML data to disk. This enables you to manage logical user sessions that cross browser session boundaries and could potentially last for longer periods of time. Unlike the saveFavorite and saveHistory behaviors, userData is not tied to the onload and onsave events. You can load or save the XMLDocument at any point within your document through the load and save methods (see Figure 11).
      The userData store is kept under the user's profile on disk. For example, on Windows 2000 I'm currently logged in as Administrator, and the userData store for my session can be found in the following directory:


 C:\Documents and Setttings\Administrator\Application Data\
    Microsoft\Internet Explorer\UserData
The userData behavior is more like cookie technology since you can actually save the data to disk. However, userData is much more flexible than cookies because you can persist the data as structured XML.
      Since userData allows developers to clutter up the user's hard drive with data, it must impose certain restrictions. userData stores can be up to 64KB per page and 640KB per domain. Also, for security reasons, a userData store is available only in the same directory and with the same protocol used to persist the store. Compare these restrictions to the cookie restrictions (4KB per cookie and 20 cookies per domain).
      Conscientious Web developers using the userData behavior should employ the expires property to specify when the userData store should be removed from disk. This property allows you to manage the lifetime of the session represented by the data store.
      Another benefit of userData is that you always have access to the XMLDocument property and you can load and save the XMLDocument at will. With the flexibility and durability of userData, you can create sophisticated solutions that completely manage state on the client machine. The shopping cart example mentioned earlier can be managed completely through XML on the client machine without making any additional round-trips to the server.

Conclusion

      Managing user sessions for a Web application on the client machine makes a lot of sense. Doing so frees up valuable server resources and makes it easier for your application to scale.
      Although cookies have long been the standard technology for implementing such a design, they are fairly limited, not only in terms of the actual data size, but also in structure. This is what my shopping cart data looks like when implemented in a cookie:


 Cookie: UserID="aarons-00001"; ISBNs="0201379368, 0201634465";
         $Path="/mind";
      Since cookies cannot exceed the 4KB limit, it's very difficult to store more information about the books currently in the shopping cart. Furthermore, since cookies are built on name/value pairs, it's impossible to map a book record to a cookie without losing the data's structure. Since all I have in the cookie is a list of ISBN numbers, chances are I'll have to make another round-trip to the Web server to retrieve additional information when viewing the shopping cart.
      Figure 12 illustrates what the shopping cart data looks like for the sample application, which uses the userData behavior to persist XML. The Internet Explorer 5.0 persistence behaviors gives you the flexibility needed to implement sophisticated state management schemes on the client machine.

From the December 1999 issue of Microsoft Internet Developer.