Page #61 (Chapter 8 - Maintaining State in IIS Applications)

Chapter 8 - Maintaining State in IIS Applications

Visual Basic Developers Guide to ASP and IIS
A. Russell Jones
  Copyright 1999 SYBEX Inc.

Caching Information
So far in this chapter, I've concentrated on storing user state, but there's another side to state maintenance—application state. The requirements for storing user state are often very small, but some applications require quick access to large amounts of data. Some application state needs are shared between all sessions; others are specific to a given session.
For example, suppose you have an inventory database. One of the tables in the database contains parts information and descriptions. Every person using the application needs to see and interact with the parts list. Assume that the users are distributors, who typically order dozens or hundreds of parts at once. For each distributor, you'll also need to display and maintain the state of the order they're building in the current session. The volume of data makes it impractical to store state on the client; therefore, you'll need to use a database. The volume of information and the infrequent changing of some tables also makes it inefficient to retrieve the parts list and the current order records from the database for each request. In such cases, you need a way to cache information on the server. You want to store the information such that:
  You don't have to make a database query for each request.
  You don't have to store ActiveX objects in a Session variable.
  You have robust programmatic access to the data just as you would with recordset-based data.
  You can retrieve and (sometimes) update information easily.
In the rest of this chapter, I'll show you several options for caching larger amounts of data on the server. You'll see how to use each one and the advantages and disadvantages of each option.
You can't store VB ActiveX objects as Session or Application variables. I know I've said that before, but you need to remember it. Well, OK, you can, but it's a bad idea because when you do that, you force the server to use the same thread to service all requests from the client, which severely impacts scalability. That leaves two options for storing multiple values in memory: strings and arrays. Both work quite well for storing multiple values. In the preceding section, you saw how easy it is to store a string, retrieve it, turn it into a Dictionary object, change the values, then re-store the string. You can do the same thing with arrays. Before we go any further, you need to remember that the Session object doesn't actually store anything except Variants. No matter which type of data you save in a Session variable, it's saved as a Variant.
Luckily, ADO Recordset objects have two methods that can help you store them easily: the getString method and the getRows method. The getString method, by default, returns a string with the fields separated by tabs and the rows separated by carriage returns. You can alter the separator characters in the call to getString if you wish. For example, if you create a connection and retrieve a recordset of all the authors in the pubs sample database, then call the getString method, you get a listing that looks like this:
712-45-1867 del Castillo    Innes   615 996-8275    2286 Cram Pl. #86   Ann Arbor   MI  48105   -1
722-51-5454 DeFrance    Michel  219 547-9982    3 Balding Pl.   Gary    IN  46403   -1
etc…
The getString method is convenient, but data in the form of a raw string has several problems:
  Getting to any particular record is difficult—you have to count the carriage returns to find a record.
  After finding a record, you have to parse the string into fields.
  The method doesn't store the field names in the first "row," which makes it easy to think you're accessing one field when you're actually accessing another one.
  Note The Microsoft pubs database comes with SQL Server. If you don't have SQL Server, you can download a Microsoft Access database containing the tables of the pubs database from the Sybex Web site.
  Note To download code, navigate to http://www.sybex.com. Click the Catalog button and search for this book's title. Click the Downloads button and accept the licensing agreement. Accepting the agreement grants you access to the downloads page for the book.
The Recordset.getData method returns data in a two-dimensional array, which solves the first two problems, but still doesn't deliver the field names.
One solution to these problems is to create a data structure that provides the information you need, while still using Variants that you can store in a Session or Application object. Consider the problem: You need programmatic access to individual data fields by name in a two-dimensional array of records. To get that, you need a list of the field names—in sequence, the data itself, and the number of records. Given those three pieces of information, you should be able to get any individual value.
Writing a StoredRecordset Class
The answer to storing recordsets in usable form is to store the data as a Variant array that contains:
  A Variant array of the field names
  The two-dimensional data array obtained by the call to getData
  A third array containing other meta-information you want to store about the recordset, for example, the number of rows, the last activated row, etc.
I call this structure a StoredRecordset. A StoredRecordset DLL encapsulates this structure and has properties and methods to retrieve the data easily. The DLL exposes one public object, called CStoredRecordset.
Here's the code to create a StoredRecordset:
' assume you have a populated Recordset object (rs)
dim SR as new CStoredRecordset
call SR.StoreRecordset(R)
To store the structure as a Session variable, you call the StoredRecordset property:
Session("SR") = SR.StoredRecordset
To reconstitute the CStoredRecordset object from the data stored in the Session object, use the following:
dim SR as new CStoredRecordset
SR.StoredRecordset = Session("SR")
The code is too long to include in its entirety, but you can download it from the Sybex Web site. Table 8.1 contains a list of the properties and methods of the CStoredRecordset class.
Table 8.1: CStoredRecordset Class Methods and Properties
Name
Type
Description
StoredRecordset
Property Get
Retrieves the Variant structure containing the stored recordset data. Call this method to obtain the structure you store in an Application variable or in a Session variable between requests.
StoredRecordset
Property Let
Initializes the CStoredRecordset object from a Variant containing a stored recordset structure. Call this method when you want to reconstitute a CStoredRecordset object from data stored in an Application or Session variable.
StoreRecordset
(R as Recordset)
Sub
Stores the field names and data from a Recordset object into a Variant structure suitable for storing in an Application or Session object.
Fields
Property Get
Returns a list of field names as a Variant array.
Data
Property Get
Returns the data from the Recordset object. Essentially the same as getData, except null values have been transformed to null strings.
RecordCount
Property Get
Returns the number of records in the stored recordset.
FieldCount
Property Get
Returns the number of fields or columns in the stored recordset.
HTMLTableString
Property Get
Returns the stored recordset formatted as an HTML table.
FieldNumber
(fieldName as string)
Property Get
Returns the integer index of the field that matches the fieldName parameter.
Value
(fieldNameOrNumber as variant, aRecord-
Number as long)
Property Get
Returns a Variant containing the value of the column matching the fieldNameOrNumber parameter from the record corresponding to the value parameter aRecordNumber.
Find (fieldsAndValuesDictionary As
Dictionary)
Function
Returns True if a record is found that matches the field names and values passed in the fieldsAndValuesDictionary Dictionary object, False otherwise.
IsStoredRecordset
(anSR As Variant)
Property Get
Returns True if the Variant parameter anSR contains a valid stored recordset structure, False otherwise.
Rows
(rowNumberArray As Variant)
Property Get
Returns a new CStoredRecordset object containing the rows corresponding to the row numbers contained in the rowNumberArray parameter.
FindRowNumbers
(fieldname As String, fieldValue As Variant)
Property Get
Returns a Variant array of the row numbers where the value of the field specified by the fieldname parameter matches the value specified in the fieldValue parameter.
Filter 
(fieldname As String, fieldValue As Variant)
Property Get
Returns a new CStoredRecordset object containing the rows where the value of the field specified by the fieldname parameter matches the value specified in the fieldValue parameter.
Column 
(fieldname As String, allowNulls As Boolean)
Property Get
Returns a Variant array of the values in the column specified by the fieldname parameter. If allowNulls is True, the method returns all the values. If allowNulls is False, the method returns only those rows that do not have a null-string value.
RowToDictionary
Function
Returns a Dictionary object containing the data for the current row of the stored recordset. The keys are the field names and the values are the field values.
MoveFirst
Sub
Moves the stored recordset index to the first row (row 0).
MoveLast
Sub
Moves the stored recordset index to the last row (RecordCount).
MoveNext
Sub
Moves the stored recordset index to the next row.
MovePrevious
Sub
Moves the stored recordset index to the first row.
MoveTo 
(aRowNumber As Long)
Sub
Moves the stored recordset index to the specified row.
Move(offset as long)
Sub
Moves the stored recordset index pointer by the number of rows specified in the offset parameter.
EOF
Property Get
Returns True if the recordset index has moved past the last record.
BOF
Property Get
Returns True if the recordset index has moved in front of the first record.
AbsolutePosition
Property Get
Returns the current recordset index.
You can see from the methods and properties available in Table 8.1 that much of the functionality of a recordset is available through the CStoredRecordset class—and it's relatively easy to extend the class methods to enable searching and sorting. The most data-intensive work is all done when you initially load the recordset—iterating through the recordset to find the field names and change null values to empty strings. The class changes the nulls to empty strings because I originally created it for displaying data with ASP pages. If you need the null values or you'd prefer to write code to test for null values, you can easily eliminate part of the initial processing as well.
Once initialized, the CStoredRecordset class is fast because it works directly from the data stored in the arrays. That's not to say that the class methods are completely optimized—with a little work I'm sure you can make the class even more efficient. Shuttling the reference to the Variant containing the arrays back and forth between the Session object and the class is fast because it's just a variable reference—there's no data copying to do.
Writing a StoredDictionary Class
A stored recordset works well for multiple records, but often you need to store multiple single values. Of course you can store them directly in Session variables, but often such values are grouped. This leads to problems because Session variable names must be unique.
For example, suppose you have three people for whom you want to store data in each Session variable. Each person has a first and last name. To store the six values directly in Session variables, you would have to make up unique names, like Person1LastName, Person2LastName, etc. It would be much more convenient (and extensible) to store each person's information as values of a Dictionary object. Then you could retrieve the data as Session("Person1").LastName.
Unfortunately, as you probably have realized by now, you shouldn't store a Dictionary object in a Session or Application variable. You can, however, use the same principles you saw in the preceding section to create a Dictionary-like object that you can load and store easily. Just like the CStoredRecordset class, you use arrays to store the data, then load the arrays into an ActiveX object called CStoredDictionary when you need to access the data. The goals are:
  Create a class with properties and methods that are similar to those of the Dictionary object.
  Create methods to store and load instances of the class from arrays stored in Session or Application variables.
  Once initialized, the object should load very quickly.
The CStoredDictionary object contains a list of names (the Dictionary keys) and values (the Dictionary items). Each name corresponds to a single value. The keys are strings and the items are Variants. The class needs to be fast; therefore, it sacrifices a small amount of memory to avoid re-dimensioning the arrays each time you add an item. Instead, it creates space for 10 items (by default) at a time. When you add an item, the class re-dimensions the arrays only if adding the item would exceed the current array size. You can set the increment with the ExpandIncrement property.
VB can quickly scan strings for the position of substrings —much more quickly than it can iterate through an array and make comparisons—so the class stores the keys as a comma-delimited list of fixed-length strings. By default, the maximum key length is 30 characters. You can expand this if necessary, but only before you add values. To find a given key, the class pads the requested key with spaces and adds a comma, then calls VB's Instr function. If the key exists, the index of the value will be the integer value of the position returned from the call to Instr divided by the maximum length of the keys, plus one for the comma. For example, if there are two keys, and the maximum key length is 20 characters, the key string looks like
",key1                ,key2                ,"
Therefore, to find the string "key2", the class would pad the string to
"key2                ,"
A call to the Instr function for the padded string will return 22. The integer value of 22 divided by 21 (length of the key plus the comma) is 1, which is the proper zero-based array index for the second item.
As with all collections, there are trade-offs depending upon the requirements for the collection. This collection is extremely fast for small collections—up to 100 items. It's reasonably fast up to about 500 items. After that, the performance drops off due to VB's problems with concatenating large strings. You can expand this performance limit somewhat by reducing the size of the keys (see the MaxKeySize property in Table 8.2). Adding items is already relatively fast, but you can improve that as well by changing the ExpandIncrement property, thus limiting the number of times the class needs to re-dimension the data array.
Table 8.2 lists the properties and methods of the CStoredDictionary class.
Table 8.2: CStoredDictionary Class Properties and Methods
Name
Type
Description
Add
Sub
Adds a key-value pair to the CStoredDictionary object. The key must be a string; the value may be any data type, including Object.
AsVariant
Property Get
Returns the CStoredDictionary as a single Variant, suitable for storage as an Application or Session variable.
CompareMethod
(aMethod As VbCompareMethod)
Property Let
Controls whether the CStoredDictionary keys are case sensitive. Valid values are vbBinaryCompare and vbTextCompare.
CompareMethod
Property Get
Returns the current case-sensitivity setting, either vbBinaryCompare or vbTextCompare.
Count() as Long
Property Get
Returns the number of items in the CStoredDictionary object.
Exists
(aKey As String) As Boolean
Property Get
Returns True if the specified key exists, False otherwise.
ExpandIncrement
(expandValue As Integer)
Property Let
Sets the increment value for re-dimensioning the data array.
InitFromVariant
(V As Variant)
Sub
Initializes the properties of a StoredDictionary from a Variant. Typically, you store the StoredDictionary in a Session or Application variable using the AsVariant method, then restore it using InitFromVariant.
Item
(anIndex As Variant) As Variant
Property Get
Retrieves the value associated with a specific key or numeric index position.
Items() As Variant
Property Get
Retrieves all the values in the CStoredDictionary as a Variant array.
Key
(index As Variant) As String
Property Get
Retrieves the key at the specified numeric index position.
Keys() As Variant
Property Get
Returns all the keys in the StoredDictionary as a Variant array.
MaxKeyLength() As Integer
Property Get
Retrieves the maximum length of a CStoredDictionary key.
MaxKeyLength
(aLength As Integer)
Property Let
Sets the maximum length of a CStoredDictionary key.
StoreRecord
(R As Recordset)
Sub
Stores the contents of the current record from the Recordset parameter R. The method uses the field names as keys and the field values as values.
Remove
(aKey As Variant)
Sub
Removes the key and value pair associated with the parameter aKey from the CStoredDictionary.
Using the CStoredRecordset and CStoredDictionary Classes
As an example of how to use the StoredRecordset and StoredDictionary DLLs, you'll build a short project called DataCache. You'll need to download the Stored-Recordset and StoredDictionary projects from the Sybex Web site to build this project. Start a new IIS project and rename it DataCache. Name the WebClass Designer DataCacheTest. Add the CStoredRecordset and CStoredDictionary classes to the project.
  Warning If you don't add the CStoredRecordset and CStoredDictionary classes to your project, you can still run the DataCache project in design mode by adding references to the StoredRecordset and StoredDictionary DLLs. However, it (probably) won't work in compiled mode. You'll see why in Chapter 11, "Using ActiveX DLLs from WebClasses," when you build ActiveX DLLs for use with WebClasses. For now, add the class files to your WebClass project.
The project retrieves a list of authors from the Authors table in the pubs sample database. If you don't already have one, you'll need to create a DSN using the ODBC Data Source Administrator applet (see directions earlier in this chapter, in the "How to Create a DSN" sidebar). Put the code in Listing 8.2 into the Start event of the WebClass.
Listing 8.2: Sample Code for the StoredRecordset and StoredDictionary Classes (DataCacheTest.dsr)
Private Sub WebClass_Start()
    Dim sr As CStoredRecordset
    Dim sd As CStoredDictionary
    Dim conn As Connection
    Dim R As Recordset
    Dim vField As Variant
    Dim i As Integer
    With Response
        .Write "<html>"
        .Write "<body>"
    End With
    Set sd = New CStoredDictionary
    Set sr = New CStoredRecordset
    If IsEmpty(Session("SR")) Then
        Response.Write "Retrieving data from database<br>"
        ' if the recordset is not already stored,
        ' get a recordset
        Set conn = New Connection
        conn.Open "dsn=Pubs;uid=sa;pwd="
        Set R = conn.Execute("SELECT * FROM Authors", _
             , adCmdText)
      
        ' store the first record in a StoredDictionary
        Call sd.RecordToStringDictionary(R)
      
        ' set a session variable to cache the data
        Session("SD") = sd.StoredDictionary
      
        ' store the entire recordset in a StoredRecordset
        Call sr.StoreRecordset(R)
      
        ' set a session variable to cache the data
        Session("SR") = sr.StoredRecordset
    Else
        Response.Write "Retrieving data from " _
           & Session variables<br>"
        ' reconstitute the classes from the stored variant
        sd.StoredDictionary = Session("SD")
        sr.StoredRecordset = Session("SR")
    End If
    ' print the data
    With Response
        .Write "<h3>StoredDictionary</h3>"
        For i = 0 To sd.Count - 1
            .Write sd.Key(i) & "=" & sd.Item(i) & "<BR>"
        Next
        .Write "<p>"
        .Write "<center>"
        .Write "<h3>StoredRecordset</h3>"
        .Write sr.HTMLTableString()
        .Write "</center>"
        .Write "</body>"
        .Write "</html>"
    End With
End Sub
Save the project and run it. You should see the screen shown in Figure 8.6.
The first time you run the project, the code creates a CStoredDictionary and a CStoredRecordset, retrieves the data from the pubs database, initializes the objects, then stores them as Variants in Session variables. Set a breakpoint at the top of the Start event code, then refresh the page. This time, you'll see that the method retrieves the data from the Session object, initializes the CStoredDictionary and CStoredRecordset objects, then displays the data again.
You could just as easily store either the data from these objects in Application variables. Just remember, always use the methods that return the data as Variants; don't store the objects themselves directly in Session or Application variables.



Visual Basic Developer[ap]s Guide to ASP and IIS
Visual Basic Developer[ap]s Guide to ASP and IIS
ISBN: 782125573
EAN: N/A
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net