Aaron Skonnard |
Client Persistence |
Download the code (23KB)
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. |
Figure 1: Browsing Book Catalog |
Figure 2: Viewing Shopping Cart |
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). |
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). 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. |
#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. |
<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). |
<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). |
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. |
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.