Page #60 (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.

Exploring the State Maintenance Options
All that explanation was interesting, but there's nothing like building a project to show how things really work. In this project, you'll implement all the state maintenance options. After working with them, you'll have a good basis for deciding which to use in your own applications.
Using Session Variables to Maintain State
Create a new IIS project and rename it StateMaintenance. Rename the Designer WebclassState, then create a new directory called StateMaintenance and save the project. Create a new custom WebItem called Counter. Delete the default WebClass_Start event code and replace it with this code:
Private Sub WebClass_Start()
    Set NextItem = Counter
End Sub
Place this code into the Counter_Respond event:
With Response
     .Write "<html>"
     .Write "<body>"
     .Write "<h1><font face=""Arial"">"
     .Write "State Maintenance Demo</font></h1>"
     Session("Counter") = Session("Counter") + 1
     .Write "<p>You have visited this page " & _
        Session("Counter") & " times.</p>"
     .Write "</body>"
     .Write "</html>"
End With
This simple application counts the number of times a user visits the Counter page. The application stores the count in the Session("Counter") variable. The Counter WebItem increments the counter whenever you refresh the page. Go ahead and run the project. Your browser should look like Figure 8.3.
Each time you refresh the page, the counter will increment. You're saving state in a Session variable. That's why Session variables are popular: They're simple to use. Close your browser, restart it, and navigate to the starting URL for your WebClass again. Don't stop the WebClass, though—leave the project running. When you reach the page in the new browser, the counter will have been reset to 1. That's because you lost the Session cookie (and thus the connection to the Session object on the server) when you closed the browser. Closing the browser is a good way to force a new Session. When you did that, you also saw how the WebClass maintains a separate Session value for each active Session. If that's not clear to you yet, open three more (or ten more) browsers simultaneously and navigate to the same page in each one. Start refreshing the browsers randomly. What happens?
Using Cookies to Maintain State
You aren't limited to storing state in any one method. Let's add a cookie and use that as a second Counter variable. Change the code in the Respond event for the Counter WebItem as follows:
Dim iCookieCounter
If Request.Cookies("Counter") = "" Then
   iCookieCounter = 0
Else
   iCookieCounter = CInt(Request.Cookies("Counter"))
End If
iCookieCounter = iCookieCounter + 1
Response.Cookies("Counter") = CStr(iCookieCounter)
With Response
          .Write "<html>"
          .Write "<body>"
              .Write "<h1><font face=""Arial"">"
          .Write "State Maintenance Demo</font></h1><br>"
          Session("Counter") = Session("Counter") + 1
          .Write "You have visited this page:<br>"
          .Write "Session counter: " & Session("Counter") & _
              "<br>"
          .Write "Cookie counter: " & CStr(iCookieCounter) _
              & "<br>"
          .Write "</body>"
          .Write "</html>"
End With
Using QueryString/URLData Variables to Maintain State
If you didn't have Session variables or cookies, you'd need to maintain state using QueryString/URLData or hidden form variables. These aren't quite as convenient as either cookies or Session variables. Change the code in the Counter _Respond event to append a URLData value to the URL and add a link that refreshes the page. Each time you click the link, the server will retrieve the QueryString counter value, increment it, then redisplay the page with the new value. The easiest way to implement this is to use the URLData property of the WebClass to format the QueryString data.
I'm not going to list the entire Respond event this time, just the code you need to add. Stop the WebClass and add the following code into the Respond event. Dimension another integer variable to hold the QueryString count:
  Dim iQueryStringCounter As Integer
Retrieve or initialize the value:
  If URLData = "" Then
      iQueryStringCounter = 0
  Else
      iQueryStringCounter = CInt(URLData)
  End If
  iQueryStringCounter = iQueryStringCounter + 1
  URLData = CStr(iQueryStringCounter)
Display the value:
     .Write "QueryString counter: " & _
         CStr(iQueryStringCounter) & "<br>"
Write the anchor tag for the link:
     .Write "<a href=" & URLFor(Counter) & _
         ">Increment QueryString Counter</a><br>"
Start the WebClass again and click the link a few times. Refresh the page without clicking the link. Note that the values are independent—the Session and cookie values increment no matter which method you use to refresh the page. You're storing different values in different places.
Using Form Variables to Maintain State
At the beginning of this event, the code either retrieves the cookie value or sets a default counter value of zero. It then creates or overwrites the cookie and displays the two counters. Both counters should display the same number as you refresh the page—and they do if you refresh the page slowly. If you refresh quickly, however, the client-side cookie counter gets behind. I suspect this is because the browser doesn't bother parsing the cookie header if you tell the browser to refresh before the previous response arrives.
Let's add a hidden form variable counter to the page as well. I won't list the entire Respond event this time either, just the code you need to add. Stop the WebClass and add the following code into the Respond event. Dimension another integer variable to hold the form count:
Dim iFormCounter As Integer
Each time you submit the form, retrieve the value from the Request.Form collection and increment it.
iFormCounter = Request.Form("FormCounter")
iFormCounter = iFormCounter + 1
Display the counter:
.Write "Form counter: " & CStr(iFormCounter) & "<br>"
Redisplay the form with the incremented value.
.Write "<form name='frmCounter' method='post' action=" _
     & WebClass.URLFor(Counter) & ">"
.Write "<input type='hidden' name='formCounter' _
     & "value='" & CStr(iFormCounter) & "'>"
.Write "<input type='submit' value='Increment _
     & "Form Counter'>"
.Write "</form>"
Note that the Form counter returns to 1 if you click the Increment QueryString Counter link, but that the QueryString counter increments when you click the IncrementForm Counter button. That's because the browser sends the Query-String data when you post the form, but the browser does not send the form data when you click the link. This should reinforce the hardest part of using form variables to maintain state: You have to submit the form to have access to the form values on the server.
Using Database Tables to Maintain State
Now that you've seen the Session variable and client-side (cookies, QueryString/ URLData, and form) options for maintaining state, you should implement state in databases as well. I saved this option until last not only because it's the most complicated, but because the concepts in this section are also the most useful.
You'll need a database and a table for storing the information. If you download the code for this project, you'll see an Access database called StateMaintenance .mdb that you can use, but if you want to create your own, create a new database called StateMaintenance and create one table, called SessionState.
The table has three columns, all of which are required:
ID  A 50-character varChar field that holds an ID you'll generate for the current Session—not null
Position  Holds an integer value that tells you the sequence of that row for that ID—not null, default 0
State  A 255-character text field in Access (varChar field in SQL Server)—not null, default is an empty string
You won't use the Position field in this example. I've included it just so you can see how easy it would be to extend this concept to any reasonable number of strings, as long as no single string exceeds the maximum length of the field. Using this method, you aren't limited to 255 characters for state information even if you're using a database (such as Access or SQL Server 6.x) with a maximum string length of 255 characters. I've excluded the use of long binary or memo fields for performance reasons.
How to Create a DSN
You'll need to create a Data Source Name (DSN) for the database so that you can access it easily. DSNs are the easiest (but not the best) way to connect to a database source. You use the ODBC Data Source Administrator program to create a DSN. This program is a wizard, so you'll find it easy to use. To find the ODBC Data Source Administrator program, click Start Settings Control Panel, then double-click the ODBC32 icon. You'll see the screen in Figure 8.4.
Any DSN you use through an Anonymous connection must be a System DSN. Click the System DSN tab on the dialog, then click the Add button. You'll see a list of ODBC drivers installed on your system (see Figure 8.5).
Figure 8.5: ODBC Create New Data Source dialog
Select the appropriate ODBC driver for your data source, then click Finish. What happens next depends on the type of driver you select:
Microsoft Access  You need to select the database. For file-based databases such as Microsoft Access, make sure that the IUSR_MachineName and IWAM_MachineName accounts have Change (RWXD) access to the directory in which the database file resides.
SQL-Server or other ODBC database  You must select or enter the server name and the database name, and provide a sign-on and password. Follow the steps in the Wizard until you see a Finish button. Be sure to click the Test button to test the connection.
The rest of this project uses the steps outlined in the "Maintaining State in a Database" section earlier in this chapter. Recall step 1: In the Session_OnStart event in the global.asa file, you create a new row(s) for the new SessionID or sign-on.
Note that the code for this step isn't part of the WebClass; it's code that you place into the global.asa file. Find the global.asa file for the StateMaintenance project—IIS creates the file in your project directory.
The code in the global.asa file runs on the server, so the first line in the file is
<script language="vbscript" runat="server">
And the last line is
</script>
The rest of the code in the file goes between the <script> tags. You'll need access to the connection string for your database (in this case, the StateMaintenance DSN) throughout the project, so a good place to store it is in an Application variable. You can create this variable when the application starts.
Sub Application_OnStart()
   Application.Lock
   Application("DB") = "StateMaintenance"
   Application.Unlock
End Sub
Now you can write the Session_OnStart code:
Sub Session_OnStart()
   dim conn
   set conn = Server.CreateObject("ADODB.Connection")
   conn.ConnectionString=Application("DB")
   conn.mode=3
   conn.open
   conn.execute "INSERT INTO SessionState " _
      & "(ID, Position, State) VALUES " _
      & "('" & Session.SessionID & "', 0, '')"
   conn.close
   set conn = nothing
End Sub
Next write the code for step 2: In the BeginRequest event code for each WebClass in your application, you read the cookie, then retrieve the appropriate row(s) from the database.
For now, you're using the Session.SessionID value as the cookie. In the WebClass_BeginRequest event, you want to open the connection and retrieve the rows of data associated with the SessionID. After you retrieve the recordset, you need to decide how to represent the data inside the WebClass.
I elected to store the data in a comma-delimited string. Each item in the string consists of a key and an associated value, separated by an equals sign. Here's an example:
Counter=1,LastName=Jones,FirstName=Bill
So to retrieve the data, you need to concatenate the records, then parse the resulting string into a Dictionary object so you can use the data easily during the request. The BeginRequest event calls a getState function, which retrieves the recordset:
Private Sub WebClass_BeginRequest()
    Call getState(Session.SessionID)
End Sub
Private Function getState(anID As String)
    Dim conn As ADODB.Connection
    Dim R As Recordset
    Dim s As String
    Set conn = New Connection
    conn.ConnectionString = Application("DB")
    conn.Mode = adModeReadWrite
    conn.Open
    Set R = New Recordset
    R.Open "SELECT Position, State FROM SessionState " _
       & "WHERE ID='" & anID & "' ORDER BY Position", _
       conn, adOpenForwardOnly, adLockReadOnly, adCmdText
    While Not R.EOF
        s = s & R("State")
        R.MoveNext
    Wend
    Set R = Nothing
    Call parseStateString(s)
End Function
The parseStateString function parses the string into a Dictionary dimensioned as a module-level object variable. You'll need to add a project reference to the Microsoft Scripting Runtime Library before you can use the Dictionary object from VB.
' in Declarations section
Dim dState as Dictionary
Private Function parseStateString(s As String)
    Dim arr As Variant
    Dim i As Integer
    Dim association As Variant
    Set dState = New Dictionary
    dState.CompareMode = TextCompare
    If Len(s) > 0 Then
        arr = Split(s, ",")
        For i = 0 To UBound(arr)
            association = Split(arr(i), "=")
            If UBound(association) = 1 Then
                dState.Add association(0), association(1)
            End If
        Next
    End If
End Function
You'll need two additional functions to set a state variable and retrieve a state variable:
Private Function getStateValue(aKey As String) As String
    If dState.Exists(aKey) Then
        getStateValue = dState(aKey)
    End If
End Function
Private Function setStateValue(aKey As String, Optional aValue As String = "")
    dState(aKey) = aValue
End Function
Now, step 3: During page processing, you add or remove state information as appropriate.
So, just like with all of the other state maintenance options, you retrieve the dState("Counter") variable, increment it, store the new value, and display that value. Place the following code in the Counter_Respond event.
    ' Dimension variables at start of routine
    Dim aVal As Variant
    Dim iDBCounter As Integer
    aVal = getStateValue("Counter")
    If aVal <> "" Then
        iDBCounter = CInt(aVal)
    Else
        iDBCounter = 0
    End If
    iDBCounter = iDBCounter + 1
    Call setStateValue("Counter", CStr(iDBCounter))
    ' display code for other state options
    .Write "Database counter: " & CStr(iDBCounter) & "<br>"
Step 4: In the EndRequest event code for each WebClass, you update the database with any changed state information.
To update the database, you need to reverse the parsing process—taking the updated values out of the Dictionary and concatenating them into an array of strings, none of which may be more than 255 characters long. Then it deletes all the existing state rows for the SessionID and inserts the new state strings into the database. Because you wouldn't want to lose state, wrap the process of deleting and inserting the data in a transaction. If any error occurs, you can roll back the transaction. The EndRequest method calls a function called saveState. The code is in Listing 8.1.
Private Sub WebClass_EndRequest()
    Call saveState(Session.SessionID)
End Sub
Listing 8.1: The SaveState Function Saves Session State in a Database
Private Function saveState(anID As String)
    Dim conn As Connection
    Dim R As Recordset
    Dim sArr() As String
    Dim sArrCount As Integer
    Dim V As Variant
    Dim s As String
    Dim i As Integer
    Dim association As String
    Dim recordCounter As Integer
    On Error GoTo Err_SaveState
    For Each V In dState.Keys
        association = CStr(V) & "=" & CStr(dState(V))
        If Len(s) + Len(association) < 255 Then
            s = s & association & ","
        Else
            ReDim Preserve sArr(sArrCount) As String
            sArr(sArrCount) = s
            sArrCount = sArrCount + 1
            s = ""
        End If
    Next
    ReDim Preserve sArr(sArrCount) As String
    sArr(sArrCount) = s
    sArrCount = sArrCount + 1
    s = ""
    Set conn = New Connection
    conn.ConnectionString = Application("DB")
    conn.Mode = adModeReadWrite
    conn.Open
    conn.BeginTrans
    conn.Execute "DELETE FROM SessionState WHERE ID='" & anID & "'"
    Set R = New Recordset
    R.Open "SELECT ID, Position, State FROM SessionState WHERE ID='" & _
    anID & "' ORDER BY Position", conn, adOpenStatic, adLockBatchOptimistic, _
    adCmdText
    For i = 0 To sArrCount - 1
        R.AddNew
        R!ID = anID
        R!Position = i
        R!State = sArr(i)
        R.Update
    Next
    R.UpdateBatch
    R.Close
    Set R = Nothing
    conn.CommitTrans
    conn.Close
    Set conn = Nothing
Exit_SaveState:
    Exit Sub
Err_SaveState:
    conn.RollbackTrans
    Set R = Nothing
    conn.Close
    Set conn = Nothing
    Err.Raise Err.Number, "saveState", Err.Description
End Function
Step 5: In the Session_OnEnd event in global.asa, you delete the row(s) from the database.
You want to delete all the records that belong to the Session by deleting all records where the ID is equal to the Session.SessionID value.
Sub Session_OnEnd()
   dim conn
   set conn = Server.CreateObject("ADODB.Connection")
   conn.ConnectionString=Application("DB")
   conn.mode=3
   conn.open
   conn.execute "DELETE FROM SessionState " & _
      "WHERE ID='" & Session.SessionID & "'"
   conn.close
   set conn = nothing
End Sub
Save and run the WebClass. You should see the database counter increment just like the other counters. Try storing some other state values in the database. That is a lot of work for a counter, but there is a point to it. By this time, you should have a good idea of which type of state maintenance you want to use for simple values such as counters. You should also recognize that only the Session variable loses its value if you start and stop the WebClass, not the cookie, QueryString, form, or database values. It's obvious why the cookie, QueryString, and form values don't lose their data, but it's less obvious why the database value remains constant. The reason is that the browser retains a Session cookie. The ASP engine recognizes that and creates a new Session with the same SessionID, thus the data exists in the database and therefore remains constant.
One more interesting quirk: Click a dozen or so times on the Increment Query-String Counter link. Now use your browser's History list to step backward several pages. The browser pulls the page from its cache, so all the values appear to decrease. Now refresh the page. Notice that the QueryString and form values increment only by one when you do that, whereas the other values immediately return to their previous values. That's because the QueryString and form values increment based on data they receive from the client, whereas the other counter values are stored on the server. Using the browser's History list does not affect the server counters, but it resets the client counters. Make sure you understand why this behavior occurs because it should definitely affect where you store state. In general, the rule is: Store state on the server unless the state data is specifically associated with the current page of the browser.
Storing State across Sessions
You can easily extend the database method to store state between sessions, but you'll need to use an ID other than the SessionID. Whenever you need unique values in an application, think GUID. A GUID is a Globally Unique Identifier. Here's an example of a GUID:
{05589FA1-C356-11CE-BF01-00AA0055595A}
You've probably seen plenty of GUIDs already; if not, look in the Registry—Microsoft uses them to uniquely identify classes and other objects, because they're practically guaranteed to be unique across all computers in the world. This unique property makes them ideal for many situations in which you need to ensure that values in disparate locations don't collide when you merge the values.
  Warning Don't change the Registry unless you're sure you know what you're doing.
As an example, suppose you have a large group of local databases from which you periodically send new and updated data to a central database. You need to be able to identify the rows to update and you also need to ensure that new rows don't collide with any existing rows. Unless you can guarantee control of the local database, you can't use numeric (such as AutoNumber or Identity) values, because there's no way to ensure that the locally generated value won't conflict with a value generated on some other local database. You could generate random values, but again, you can't be absolutely sure that the value generated on one computer won't conflict with the value generated on another computer. If you use GUIDs, you can be as sure as it is currently possible to be that the values won't conflict.
You create a GUID with two API calls, one to create the GUID (CoCreateGUID2) itself, which is a 16-byte value, and a second (StringFromGUID2) to transform it into the familiar string form.
Private Declare Function CoCreateGuid Lib _
     "ole32.dll" (buffer As Byte) As Long
Private Declare Function StringFromGUID2 Lib _
     "ole32.dll" (buffer As Byte, ByVal lpsz As Long, _
     ByVal cbMax As Long) As Long
I've wrapped the two calls in a single, easy-to-use function called getGUID:
Private Function getGUID() As String
    Dim buffer(15) As Byte
    Dim s As String
    Dim ret As Long
    s = String$(128, 0)
    ret = CoCreateGuid(buffer(0))
    ret = StringFromGUID2(buffer(0), StrPtr(s), 128)
    getGUID = Left$(s, ret - 1)
End Function
The function returns a GUID in string form. You'll use the GUID returned from this function as a unique identifying cookie value. By providing an expiration date, you tell the browser to store the cookie on the local computer's hard drive. Until the cookie expires (or the user deletes it), the browser will retrieve and send the cookie to your application even if the user closes the browser. You will use the GUID cookie as the key to store and retrieve the information from the database.
To test the GUID cookie, you'll need to change the WebClass_BeginRequest and EndRequest subroutines to retrieve the database row with a GUID key rather than the Session.SessionID key used in the previous section of this chapter.
Private Sub WebClass_BeginRequest()
    ' get the GUID cookie
    Dim GUID As String
    GUID = Request.Cookies("GUID")
    If GUID = "" Then
        GUID = getGUID()
        Response.Cookies("GUID") = GUID
    End If
    Call getState(GUID)
End Sub
Private Sub WebClass_EndRequest()
    Dim aGUID As String
    aGUID = Request.Cookies("GUID")
    Call saveState(aGUID)
End Sub
Finally, the original Session_OnEnd event in global.asa deletes the database rows associated with the Session.SessionID for that session. Although the code will no longer work (because you're storing information with a GUID rather than a SessionID key), you should still clean up the code. To do so, you can delete the entire subroutine.
Eliminating Obsolete Data
Another problem associated with storing state data across sessions is that it's difficult to know when the data you're storing is obsolete. For example, suppose a person visits your site one time and never returns. You may not want to keep that data forever. One way to solve the problem is to update an expiration date whenever you access a row. At the beginning or end of the application, you can delete the rows that have an expiration date earlier than the current date. I haven't included the code to do that, both because it requires a change to the database and because there are many other valid solutions. Other possible solutions when the data expires include sending e_mail to the user asking whether you should delete the data (assuming you obtain an e_mail address), moving the obsolete records to archive tables, and storing the data outside the database in a file.



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