Share via


Visual Basic Developer's Guide to ASP and IIS

 

A. Russell Jones, Ph.D.

SYBEX, Inc.

Reproduced from Visual Basic Developer's Guide to IIS and ASP, by A. Russell Jones, Ph.D., by permission of SYBEX Inc. ISBN 0-7821-2557-3. Copyright 1999, SYBEX Inc. All rights reserved. For further information, please contact info@sybex.com, or call 1-800-227-2346, or visit their Web site at http://www.sybex.com.

Buy this book

Introduction

Because WebClass-based applications are closely tied to Microsoft's Active Server Pages (ASP) technology—they are, in fact, essentially compiled ASP applications—you need to thoroughly understand the object model exposed by the ASP engine to make efficient use of WebClasses.

In this chapter, you'll see how to set up an ASP Web site and how IIS maintains user data for an application. You'll also build two projects that illustrate the flexibility and power of ASP development. After you have completed these projects, you'll be ready to move on to WebClass development in Chapter 4, "Introduction to WebClasses."

Understanding the Structure of an ASP Application

An application, to the ASP engine, is the set of all files and subdirectories within a directory that contains a global.asa file. Most ASP applications consist of ASP files and include (.inc) files, both of which can be any mixture of HTML, code, and graphics files; however, you can freely intermix ASP files with HTML files or any other file type that the server understands.

Figure 3.1 shows the directory structure for a typical ASP application.

Figure 3.1: A typical ASP application directory structure

The top-level directory of the structure contains the global.asa file. The global.asa file defines the root directory for an application. The first time a user requests an ASP file in any directory in the application, the ASP engine traverses the tree upward until it finds a global.asa file (or until it reaches the root Web directory). This is important, because if you nest applications, the global.asa file that will be run for any particular user's request depends on which file the user requests first. Because you can't control a user's first request, don't nest ASP applications unless you have a good reason.

On IIS 4, you need to take an additional step to create a Web application: You must tell IIS that the virtual directory containing the global.asa file is the root of a Web application. You'll see more about that later in the section "Creating a Self-Modifying ASP Application."

The global.asa file always runs first, regardless of which file was requested. At this point, you can gain control of the request by redirecting the browser to the page of your choice in the global.asa file. As people use your application, they're likely to save bookmarks or favorites. These bookmarks may or may not point to the starting file in your application. If your application depends on users starting at a particular point, or if you have security requirements, you should route users to the appropriate page by redirecting them in global.asa.

The application shown in Figure 3.1 contains several ASP files as well as two subdirectories, images and include. No single directory structure fits all applications; you can put all the files in a single directory if you wish. In practice, though, it's much easier to build and maintain the application if you arrange files according to their function.

Note   The virtual Web root and the ASP application root do not have to be the same.

For example, you might have a single global.asa file that applies to several ASP applications. You could place that global.asa file in a directory and then define each subdirectory in that directory as a virtual root, named according to the application. The directory structure in Figure 3.2, for example, contains four applications: 401k, Paycheck, Retirement, and Timesheet.

Figure 3.2: Directory Structure with a shared global.asa file

The highest-level directory, called HR Applications, contains the global.asa file. Whenever a user attaches to any of the four applications, the ASP engine climbs the directory tree until it reaches the HR Applications level, where it finds the global.asa file. One reason to set up an application in this manner might be that all four sites share security arrangements. Another reason might be that all four applications share a common database connection or other data, and you want to initialize that information in the global.asa file.

Using Include Files

To reduce the amount of repetitive code or HTML in your ASP pages, you can include external files inside your ASP page. An include file is code from an external file that the server places in the ASP page at runtime. Include files have an .inc extension. You control where the server places the content of an include file with an include directive. An include directive is formatted as an HTML comment, so servers that don't understand the directive will ignore it, as will browsers. The entire process is exactly the same as if you were to cut and paste the contents of the file into your ASP page. Here's an example of an include directive:

<!—- #INCLUDE FILE="c:\ include\myInclude.inc" -—>

The include directive tells the server to replace the include directive with the contents of the file—in this case, myInclude.inc. There are two forms of include directives: #INCLUDE FILE and #INCLUDE VIRTUAL. The FILE form requires a physical path for the file, whereas the VIRTUAL type references a file in a virtual directory.

Regardless of which form you use, the ASP engine performs all include replacements before it processes any code. Therefore, you cannot use code to determine which file to include or whether to include a file. The following code will not work as intended—the ASP engine will include both files.

<%
if myVar=True then
   %>
   <!—- #INCLUDE FILE="c:\include\myInclude.inc" -—>
   <%
else
   %>
   <!—- #INCLUDE FILE="c:\include\yourInclude.inc" -—>
   <%
end if
%>

When the ASP engine parses this file, it will replace the two include directives with the contents of myInclude.inc and yourInclude.inc—and it will make the replacement before it runs the code for the if…end if structure. This is a common misconception, so I'll repeat. Once again, with emphasis: You cannot use code to determine which file to include and you cannot use code to determine whether a file will be included.

Despite this, with a little planning, you can still take advantage of include files. Using the preceding example, suppose myInclude.inc contains the HTML to display the graphic HappyBirthday.gif and that yourInclude.inc contains HTML to display the graphic HappyAnniversary.gif. In this case, the code would work even though the ASP engine replaces both include directives. This time, although the ASP engine inserts the HTML code for both files, the If…Then structure ignores one of them, depending on the value of the myVar variable. Therefore, only one of the files displays on the browser, which is the intent.

Include files are useful for inserting often-used functions or subroutines into an ASP page. By placing the code in an include file, you can update all pages that reference the code by making changes in only one place. Using include files in this way is like placing your favorite routines in a module in VB.

Include files are not limited to code. I've seen many sites that use them for toolbars, common graphics, common sections of text, etc. You can create include files that are all code, all HTML, or a mixture of both—just like any other ASP page. You can and should use include files to enforce consistency and reuse code in your application.

Understanding Language Independence

ASP pages can be written in several scripting languages because the ASP engine is a scripting host, not a language. Currently, VBScript and JScript (Microsoft's implementation of ECMAscript) are included with the ASP engine installation, but you can also use Practical Extraction and Report Language (Perl), Restructured Extended Executor (REXX), or any language that conforms to the Microsoft debugging protocol. Almost all the code examples in this book use VBScript (this is, after all, a book about VB). However, you will see some JavaScript as well because you're going to write browser-independent applications, and you can't use VBScript for client-side script unless you're running Internet Explorer.

The ASP engine even supports pages written in more than one language. Each ASP page has a primary language, designated by the <%@ LANGUAGE= %> directive at the top of each page.

Within a page however, you can use other languages by wrapping the code in <script></script> tags, for example:

<script language=someLanguage runat=server></script>

Each page in an ASP application can set the default language for that page.

If you use multiple languages on a single page, or for any server-side script contained in <script></script> tags, remember to include the runat=server parameter and value. Otherwise, the parsing engine will think you're writing client-side code. You must always keep in mind whether you're writing code that will execute on the browser or on the server. On the server, you have access to the ASP objects—the Server, Application, Session, Request, and Response objects. On the client, you have access to the document and all its properties and methods.

This can be a tough concept to master, because you probably haven't written code in an environment where you're mixing and matching server-side code, client-side code, and HTML all in a single file.

Always remember:

  • Code inside code-delimiter brackets (<% and %>) runs on the server.
  • Code outside the brackets that is wrapped in<script></script>tags will run on the client unless the<script>tag includes therunat=serverparameter.
  • The ASP engine sends all other text in the page to the browser as part of the response. Usually, this other text consists of HTML tags and content.

Using Cookies with ASP

The ASP engine maintains Session state with cookies; if the user's browser won't accept cookies, the ASP engine cannot maintain Session state for that user. That's because there has to be some sure method of identifying a particular browser. Given the uniqueness of IP addresses, you'd think that they would be perfect for identifying a particular browser, but no such luck. Multiple users often share an IP address or block of addresses. That's what a proxy server does—it maps the user's "real" IP address to one or more public IP addresses. The proxy then makes the request via the shared address. Therefore, it's perfectly possible for two or more browsers to access your application from the same IP address. Enter the cookie.

Cookies are text strings that a browser sends to a Web site each time it requests a page from that site. You can use HTTP headers to manipulate the contents of cookies directly, although it's much easier to use the Response.Cookies collection methods. Of course, that's all that the Response.Cookies collection does—send HTTP headers to add, alter, or delete cookies.

Basically, there are two kinds of cookies:

**In-memory cookies   **These are held in memory, which means they disappear when you close the browser.

**Persistent cookies   **These are persisted to disk, which means they expire when you delete the file or when their expiration date arrives, whichever comes first.

The ASP SessionID cookie is an in-memory cookie.

Because some people don't want to accept any information that might identify them to a Web site, browsers generally have a setting that lets you refuse cookies. ASP sites typically don't work well if the browser doesn't accept cookies, because most ASP sites rely on the cookie so they can maintain session information for you.

The ASP engine writes a SessionID cookie as soon as the user requests the first file from an ASP application. The ASP SessionID cookie is in every way a normal cookie, except that it is usually invisible to your application. If you query the Request object cookie collection, you won't see the ASP SessionID cookie. You can prove it by creating the following ASP page script:

<%@Language=VBScript%>
<html><head><title>List Cookies</title></head>
<body>
<%
Dim V
If Request.Cookies.Count > 0 then
For each V in Request.Cookies
      Response.Write V & "=" & 
   Request.Cookies(V) & "<BR>"
Next
Else
   Response.Write "No Cookies"
End If
%>
</body>
</html>

Name the file and place it in a Web site. If you're using Visual InterDev, create a new Web project and call it ASPProject1, add a new ASP file, and place the code listing in it. Now use your browser to navigate to the page. You will see the text No Cookies. What happened?

Now, add one more line of code to the file, just before the end-of-script marker—the percent sign and right bracket (%>):

Response.Write "HTTP_COOKIE=" &
Request.ServerVariables("HTTP_COOKIE") & "<BR>"

This line of code displays the "raw" contents of the HTTP_COOKIE variable, without the filtering and organization applied by the ASP engine for the Request.Cookies collection. Run the file again (by refreshing the page). This time, you'll see No Cookies, and another line as well, that probably looks something like this:

HTTP_COOKIE=ASPSESSIONIDGGGQGQYE=DILDAIIBIFLCKEJCBLPCFCNI

That's the ASPSESSIONID cookie. It's a meaningless, semi-random ID that is highly likely to be unique. The ASP engine doesn't show it to you because you didn't generate it—the ASP engine did. I guess Microsoft thought it would be confusing to request the cookie count and get a 1 in response before ever setting a cookie yourself.

Consider one more interesting fact before we move on to a larger ASP project. Add this line to the end of the code listing:

Response.Write Session.SessionID

Refresh the page again. This time, you should see one additional line containing a long integer number like 411057027. The SessionID property doesn't return the SessionID cookie value at all! So will the "real" Session identifier please stand up? It turns out that the real SessionID is the long integer. The ASP engine generates the cookie as a long string of text for security reasons. It's much harder for a hacker to guess the long cookie string value than to guess the long integer value with which the cookie string is associated on the server.

Using the Scripting.Dictionary Object

A Scripting.Dictionary object is similar to a Visual Basic collection object, but faster and much more flexible. The documentation describes it as similar to a Perl associative array, if that helps you. Both the VB collection object and the Dictionary object have keys, values, and Add, Remove, and Item methods; however, for the Dictionary Add method you must supply the key first, then the value. The Add method for a VB collection requires the value first, then the key. I find the Dictionary object syntax more natural. In addition, the Dictionary object has methods to get the list of keys or values, and has an Exists method, which lets you find out whether a specified key exists.

In VB, to find out whether a Dictionary key exists, you would use code like this:

Dim d as new Dictionary
d.Add "Name", "Bill"
Debug.Print d.Exists("Name") ' prints True

In contrast, to find out whether a key exists in a VB collection object, you have to use On Error Resume Next, attempt to retrieve the value, and check for errors.

Sub keyExists(c as Collection, aKey as string) as Boolean
   Dim V as variant
   On Error Resume Next
   V = c(aKey)
   KeyExists = (Err.Number <> 5)
End Sub

**Note   **The preceding code explicitly checks for Err.Number=5 rather than just checking if any error occurred. This is because if the key exists, but the associated value is an object variable rather than a scalar variable, you will get Error 450 instead of Error 5.

The keys are strings. The values are variants, which means that they can be any data type, including objects. You can imagine that a Dictionary object looks like a table with two columns, as shown in Table 3.1.

Table 3.1: Dictionary Object Keys and Values Example

Key Value
"FirstName" "John"
"LastName" "Davis"
"City" "Albuquerque"
"State" "New Mexico"
"Address" "1723 Candelaria"
"Age" "42"
"Telephones" (Array) "555-555-5555","555-555-5556"

The keys must be unique. Having unique keys enables the Dictionary object to keep a sorted key list so that it can find any specific value with a fast binary lookup. This makes the Dictionary object larger than an array of the same size but much more efficient at finding values quickly.

Several other features of the Scripting.Dictionary object are worth noting. Unlike a VB collection object, you don't have to use the Add method to add new items; if you assign a value to a key that doesn't exist, the Dictionary creates a new key for you.

You can change the values of associations in the Dictionary through simple assignment—you don't have to remove the key and then add it again with a new value. Here's a VBScript example:

Dim d
Set d = Server.CreateObject("Scripting.Dictionary")
d("newKey") = "This is a new key"
Response.Write d("newKey") ' displays "This is a new key"
d("newKey") = "This is a changed value"
Response.Write d("newKey") ' displays "This is a changed value"
Response.Write d("NewKey") ' fails to display

When this code runs, it creates a new Dictionary object with one key, newKey, and one value, the string "This is a new key", which displays in the browser. The code then assigns a new value to the association with the key newKey, and displays that. Finally, to show that keys in the Dictionary object are case sensitive by default, the program tries to display the key NewKey. That statement fails to display because the Dictionary object can't find the key, but unlike referencing missing keys in a VB Collection object, it doesn't cause a runtime error.

You can control how a Dictionary object treats case sensitivity in keys by using the CompareMode property. There are three CompareMode constants defined in the Microsoft Scripting Runtime Library: BinaryCompare, Text-Compare, and DatabaseCompare, which are equivalent (and equal in value) to the VBScript CompareMode constants vbBinaryCompare, vbTextCompare, and vbDatabaseCompare. One restriction: You must set the CompareMode property before adding any items to the Dictionary; otherwise, a runtime error occurs. To avoid having to define the Scripting CompareMode constants, use the built-in VBScript CompareMode constants. Alter the code so it looks like this:

Dim d
Set d = Server.CreateObject("Scripting.Dictionary")
d.CompareMode = vbTextCompare
d("newKey") = "This is a new key"
Response.Write d("newKey") ' displays "This is a new key"
d("newKey") = "This is a changed value"
Response.Write d("newKey") ' displays "This is a changed value"
Response.Write d("NewKey") ' displays "This is a changed value"

Now, when you browse to the page, the browser displays all three lines.

Although the Object Browser lists the DatabaseCompare constant as one of the available CompareMode constants, the documentation for the Dictionary object doesn't include it as a valid value. Using it doesn't raise an error, though. As an experiment, set the CompareMode property of the Dictionary object to vbDatabaseCompare and run the file. Running it the first time appears to cause the Dictionary to treat keys as case insensitive; however, if you refresh the file, the third line disappears. Add a line to display the CompareMode property. The first time, the file properly displays 2, the value of the vbDatabaseCompare constant. If you run the file again, though, it will display the CompareMode property as 0, which corresponds to the vbBinaryCompare constant. I don't have a good explanation for this behavior (yet). Don't set the CompareMode property to vbDatabaseCompare.

Remember that, by default, Dictionary keys are case sensitive. This case sensitivity, coupled with the lack of a runtime error when you reference missing keys, goes against the case-insensitive theme of VB and VBScript and caused me several hours of frustrated debugging before I realized the reason. I don't like using the Dictionary in case-sensitive mode. If you don't either, you can avoid this problem altogether by wrapping the code that creates the Dictionary object in a function:

Function newDictionary()
   Set newDictionary = Server.CreateObject _
      ("Scripting.Dictionary")
   newDictionary.CompareMode = vbTextCompare
End Function

The function returns a case-insensitive Dictionary object.

**Note   **Like VB, VBScript uses the name of the function to return values; it creates a local variable with the same name as the function. You can often use this feature to make your code more readable, save yourself typing, and avoid the overhead of creating an extra local variable.

Table 3.2 shows the complete list of methods and properties for the Scripting.Dictionary object.

Table 3.2: Scripting.Dictionary Object Methods and Properties

Name Type Description
Addkey, value Method Adds a new string key to the Dictionary associated with the specified value. If the key already exists, an error occurs.
CompareMode (CompareMethod) Property Get, Let Controls the way the Dictionary object compares keys. Sets or returns one of the CompareMethod enumeration constants.

vbBinaryCompare(0) (case-sensitive)

vbTextCompare(1) (case-insensitive)

vbDatabaseCompare(2) (N/A)

Count Property Get, (read-only) Returns the count of the number of associations in the Dictionary object.
Existskey Property Get (read-only) Returns a Boolean value that shows whether the specified key exists.
Itemkey or index Property, Get, Let, Set Returns or sets a value associated with the specified string key or integer index.
Items Method Returns a variant array of all of the values currently stored in the Dictionary.
Keykey Property Let (write-only) Changes a string key from one string to another.
Keys Method Returns a variant array of all of the keys currently stored in the Dictionary.
Removekey Method Removes the specified key, if it exists.
RemoveAll Method Removes all keys.

**Note   **The Microsoft VBScript documentation doesn't list the vbDatabaseCompare as a valid CompareMode value for a Dictionary object. Although it doesn't raise an error if you use it, it exhibits some strange behavior, so don't use it.

A (Very) Brief Introduction to HTML and Forms

OK, I told you this book was not for beginners, but those of you who aren't experienced with HTML and forms will appreciate this (I promise) brief introduction before you move on to the other projects in this chapter.

Developers originally used HTML files for read-only display of information. Very quickly though, they realized that they needed a way for people to interact with the pages—specifically, to enter form data, such as names and e-mail addresses, in a way that they could be collected on the central server. It's important to realize that the display portion of a form requires nothing beyond standard HTML; however, to save the information generated by a form requires a program running on the server.

All HTML files begin with an <html> tag and end with an </html> tag. This tag is a containing or block tag—a tag that contains other tags. Following, or "inside" the HTML tag, you always have <head></head> tags. Any tags appearing between the <head> and </head> tags are in the head section. The head section contains browser directives, but most importantly, it contains the document title. The title of the document goes inside <title> and </title> tags. You should be seeing a pattern here. The </tag> form ends a tag begun with the <tag> form. Tags are not case sensitive in HTML, but they are in XML, SGML, and other markup languages, so you should work on making yourself write case-sensitive HTML right from the beginning. I'm afraid I'm guilty of mixing case in tags, so don't do as I do, do as I say.

After the head section comes the body section, which (you guessed it) is delimited by <body> and </body> tags. All the information displayed by the browser belongs in the body section except for the title, which is in the head section.

Believe it or not, you now have enough information to write a simple HTML file. Here's an example:

<html>
<head>
<title>
   Extremely Simple HTML
</title>
</head>
<body>
   Enter your name, then click the Submit button.
</body>
</html>

Figure 3.3 shows how this file looks in a browser. To run it, enter the preceding code listing into Notepad or an HTML editor. Save the file, then navigate to it in the browser. I recommend you create a virtual Web site rather than simply saving the file to disk and browsing to it, because you're going to write server-side code next, which requires the Web server.

Figure 3.3: Extremely simple HTML file

Wait—there's no Submit button! To display a button using standard HTML, you need to create a form. (Sounds like VB, doesn't it?) An HTML form begins with a <form> tag and ends (like most containing HTML tags) with a </form> tag. The form tag can take several parameters; these are the most common:

**Name   **The name of the form. Although this parameter isn't required, it's good practice to name your forms so you can refer to them easily in client-side scripts.

**Action   **The URL to which the form will submit data. You may include additional URL-encoded variables in the value portion of the Action parameter (the part after the equals sign), but only if the Method parameter is Post. If you don't specify an action, the form will post itself to the originating filename (in other words, it posts to itself).

**Method   **You can use either the Post or the Get method. If you use the Get method, the browser will create a URL string that consists of the action URL (minus any explicit parameters) with URL-encoded form data appended. You'll see an example of this in a minute.

Here's a very simple form:

<form name="frmTest" METHOD="POST"
   action="testform.asp?submitted=true">
   <input type="text" name="Text1" value="">
   <input type="submit" name="Submit" value="Submit">
</form>

Copy the form code and insert it after the line that ends with click the Submit button. Save the file as TestForm.asp. Now navigate to the file in your browser. The page should now look similar to Figure 3.4.

Figure 3.4: The TestForm.asp file.

Enter some text in the text field and click the Submit button. Nothing happens. That's because you need to write a program to make something happen. Let's do that. Go back into the TestForm.asp file you just saved and add the following code above the <html> tag. Be sure to enter the <% and %> code delimiters.

<%@ Language=VBScript %>
<%
if Request("Submitted") = "true" then
   Response.Write "Request(Text1) = " & _
      Request("Text1") & "<BR>"
   Response.End
end if
%>

Refresh the file, enter some text into the text box, and submit the form again. This time, the server should display the text you typed.

You've just written your first ASP program. If you're not familiar with ASP and forms, I encourage you to experiment with this form a little before moving on to the next project. Specifically, you should try these tasks:

  • Change the form method fromPostto Get. What happens? (Hint: Look at the address line of your browser.)
  • TheActionparameter is optional. What happens if you delete theActionparameter from the<form>tag? What if you enter justaction=?submitted=trueas the parameter?
  • What values besides theTextvalue does the form submit to the server?

Creating a Self-Modifying ASP Application

Using interpreted ASP code has many advantages. For instance, because the code lives in text files, it's easy to change. Also, after the ASP engine has been installed, you don't have to install any more DLL installations, registrations, or support files. In this project, you'll see some of the power of ASP files by writing a self-modifying application. Don't worry if you don't completely understand everything in the project at this time—you will by the time you finish this book.

Using Visual InterDev or your favorite HTML editor, start a new text file. This file will consist of a form with a drop-down selection list. When you select a color from the list and submit the form, the file will rewrite itself to reflect your choice, then redisplay itself.

Listing 3.1 shows the entire code.

Listing 3.1   Self-Modifying ASP File (selfMod.asp)

<%@ Language=VBScript %>
<% option explicit %>
<%Response.Buffer=True%>
<%
dim submitted
dim backcolor
dim fs
dim ts
dim s
dim afilename
dim colors
dim curColor
dim V
dim aPos
dim i
set colors = server.CreateObject("Scripting.Dictionary")
colors.Add "Black", "000000"
colors.Add "Red", "FF0000"
colors.Add "Green", "00FF00"
colors.Add "Blue", "0000FF"
colors.Add "White", "FFFFFF"

submitted=(Request("Submitted") ="True")
if submitted then
   Session("CurColor")=Request("newColor")
   curColor = Session("CurColor")
   afilename=server.MapPath("selfMod.asp")
   Response.Write afilename & "<BR>"
   Response.Write Session("CurColor") & "<BR>"
   set fs = server.CreateObject _
      ("Scripting.FileSystemObject")
   set ts = fs.OpenTextFile(afilename,ForReading,false)
   s = ts.readall
   ts.close
   set ts = nothing
   aPos = 0
   do
      aPos = instr(aPos + 1, s, "bgcolor", _
         vbBinaryCompare)
   loop while (mid(s, aPos + 7, 1) <> "=")
   aPos = aPos + 9
   if aPos > 0 then
      s = left(s, aPos) & Session("CurColor") & _
         mid(s, aPos + 7)
      set ts = fs.OpenTextFile _
         (afilename,ForWriting,false)
      ts.write s
      ts.close
      set fs = nothing
      Response.Redirect "selfMod.asp"
   else
      set fs = nothing
      Response.Write "Unable to find the position " & _
         "in the file to write the new color value."
      Response.End
   end if
else
   if isEmpty(Session("Curcolor")) then
      Session("CurColor") = "FF0000"
   end if
   curColor = Session("CurColor")
end if
%>
<html>
<head>
</head>
<body>
<form name="frmColor" method="post"
   action="selfmod.asp?Submitted=True">
<input type="hidden" value="<%=curcolor%>">
<input type="hidden" value="<%=Session("curcolor")%>" id=hidden1
   name=hidden1>
<table align="center" border="1" width="80%" cols="2">
   <tr>
      <td align="center" colspan="2" bgcolor="#FF0000">
         <% if curColor = "000000" then %>
            <font color="#FFFFFF">
         <% else %>
            <font color="#000000">
         <% end if %>
         Select A Color
            </font>
      </td>
   </tr>
   <tr>
      <td align="left" colspan="2" bgcolor="#FFFFFF">
         Select a color from the list, then click the 
         "Save Color Choice" button.
      </td>
   </tr>
   <tr>
      <td align="right" valign="top" width="20%">
         <b>Color</b>:
      </td>
      <td align="left" valign="top" width="80%">
         <select name="newColor">
            <%
            for each V in colors
               Response.Write "<option "
               if colors(V)=curColor then
                  Response.Write "selected "
               end if
               Response.Write "value='" & _
                  colors(V) & "'>" & V
               for i = 1 to 12
                  Response.Write "&nbsp;"
               next
               Response.Write "</option>"
            next
            %>
         </select>
      </td>
   </tr>
   <tr>
      <td align="center" valign="bottom" colspan="2">
      <input type="submit" value="Save Color Choice" 
         id=submit1 name=submit1>
      </td>
   </tr>
</table>
</form>
</body>
</html>
<%set colors = nothing%>

Web development environments such as Visual Basic and Visual InterDev create Web applications for you automatically, but if you're not using one of these tools, you'll need to create one manually.

If you're still using IIS 3, ignore this procedure. If you're using IIS 4, though, you must create a Web application before IIS will run the global.asa file. If you ever notice that an application isn't running any of the code in global.asa, you should check to make sure that the virtual directory containing your global.asa file is marked as a Web application. To mark the virtual directory for your project as an application, open the Internet Service Manager, select or create your application's virtual directory, then right-click the virtual directory name in the left-hand pane.

Look at the button to the right of the Name field in the Application Settings portion of the Virtual Directory properties dialog. If the button's caption is Create, click the button to create a new Web application, then enter the name for the application in the Name field. If the button is called Remove, then the directory is already marked as a Web application, and you don't need to do anything (see Figure 3.6).

Figure 3.6: Internet Service Manager Virtual Directory properties

Setting Up the Project in Visual InterDev

To set up the project outlined in this section in Visual InterDev, you should reference the Microsoft Scripting Runtime Library. You'll also need to allow write access to the virtual directory through the Internet Service Manager program before the project will work. To specify write access, open the Internet Service Manager program, find your virtual directory in the Default Web Site, and right-click it. Select Properties. In the dialog box, make sure the Write check box is checked. For IIS 4, the dialog box looks like Figure 3.6.

In the rest of this chapter, I'll explain the code in the selfMod.asp file.

<%@ Language=VBScript %>

This line is required in every ASP file (but of course, the language doesn't have to be VBScript—remember, ASP files can host multiple languages).

<% Option Explicit %>

Just as in VB, the Option Explicit command forces you to declare variables before you use them. Although the command is not required, you should always use this in both ASP and VB.

<%Response.Buffer=True%>

The server normally writes the HTTP headers immediately, then begins sending output as the ASP engine generates it. If you want to redirect within your code (which requires a change to the HTTP headers), you have to include this line. When you set the Response.Buffer property to True, the server buffers the entire response until page processing is complete. For long requests, this can slow down the perceived response time, because the user has to wait until the entire request has finished processing before the browser can begin to display the result. You should usually use this command in a page only if you plan to redirect, write cookies, or alter the HTTP headers.

set colors = Server.CreateObject("Scripting.Dictionary")

There are several things to explain in this line. First, the Server.CreateObject method is equivalent to the following VB code:

Dim colors as Dictionary
Set Colors = New Dictionary

In an ASP file, you need to enter both the project name (Scripting) and the class name (Dictionary) to obtain a reference to an object.

At any rate, the next five lines simply add the colors to the Dictionary object with the color name as the key and the HTML color string (which is a text representation of a long, or RGB, value) as the value. Each of the six-character strings represents three hexadecimal byte values. Colors are a combination of red, green, and blue. Each color may take a value from 0 to 255 (00 to FF in hex) and requires two characters. You read the value in pairs. The pairs of characters represent the red, green, or blue values, respectively.

colors.Add "Black", "000000"
colors.Add "Red", "FF0000"
colors.Add "Green", "00FF00"
colors.Add "Blue", "0000FF"
colors.Add "White", "FFFFFF"

This form submits to itself by posting the values entered by the user back to the same page that the form came from—unheard of for straight HTML forms, but common as dirt with ASP files. Submitting a form to itself puts all the code that deals with the form in one file, where you can test it easily, and makes one less file to maintain. If the user has submitted the form, you want to process the submitted request; otherwise, you just want to display the form. The following line determines whether the form is being shown for the first time or whether the user has submitted the form. The Request object will contain a string value of "Submitted=True" if the form has been submitted. The following code assigns a local variable called submitted a Boolean value of True if the form was submitted by the user:

submitted=(Request("Submitted") ="True")
if submitted then
   Session("CurColor")=Request("newColor")
   curColor = Session("CurColor")

**Note   **In the preceding code, note that you do not have to dimension Session variables before using them—even if Option Explicit is in effect.

Another way to test whether a user has posted a form is to check the value of Request.ServerVariables("REQUEST_METHOD"). The value will be POST when the user has posted content, and GET when you should display the form. I've used both and prefer using the local variable method. I find that using a local variable is more intuitive and works regardless of whether you use the Post or Get method.

You can think of the Session object as a Dictionary object (although it's not) because it exhibits almost identical behavior:

afilename=Server.MapPath("selfMod.asp")

The Server.MapPath method translates a virtual directory or filename into a physical directory or filename. In this case, when you pass it the name of the current file, it will return the full physical drive:\pathname\filename for the file. On my system, it returns

"c:\inetpub\wwwroot\ASPProject1\selfMod.asp"

The next few lines create two of the other objects in the Microsoft Scripting Runtime Library—a FileSystemObject and a TextStream object. The FileSystem-Object provides methods and properties to work with the native file system. One of those methods is the OpenTextFile method, which returns a TextStream object. The TextStream object lets you read from and write to text files. In this case, open the file in read-only mode, then read the entire file at one time by using the ReadAll method. Finally, close the TextStream.

set fs = server.CreateObject("Scripting.FileSystemObject")
set ts = fs.OpenTextFile(afilename,ForReading,false)
s = ts.readall
ts.close
set ts = nothing

At this point, the content of the string "s" is the same as the file that's running! You can do this because the ASP engine doesn't lock the file while it's executing—it reads the whole file into memory. You're going to search the string for the first occurrence of the word bgcolor that's followed by an equals sign.

aPos = 0
do
   aPos = instr(aPos + 1, s, "bgcolor", vbBinaryCompare)
loop while (mid(s, aPos + 7, 1) <> "=")
aPos = aPos + 9

The code loops to find the last occurrence of the word bgcolor because the first occurrence is inside the loop itself! The final line sets the value of the aPos variable to point to the color string value in the first row of the table—the position after the first pound (#) sign in the file, right before the color value you're going to update.

After it finds the position of the color string for the first row of the table, the code simply substitutes the selected color value and saves the file. It then redirects so that the browser will request the just-altered file.

if aPos > 0 then
   s = left(s, aPos) & Session("CurColor") & _
      mid(s, aPos + 7)
   set ts = fs.OpenTextFile(afilename, ForWriting, _
      false)
   ts.write s
   ts.close
   set fs = nothing
   Response.Redirect "selfMod.asp"
else
   set fs = nothing
   Response.Write "Unable to find the position " & _
      "in the file to write the new color value."
   Response.End
end if

If the form was not submitted, the file provides a default color value of red—FF0000—and assigns it to both a Session variable and a local variable called curColor. In general, you should assign Session variable values to local variables if you're going to use them more than once in a file, because looking up the Session variable value based on the key you provide takes several times longer than simply retrieving the value of a local variable. This follows the same principle you use in VB; always assign objects to local variables if you're going to use them more than once in a routine. Similarly, it's the principle behind the introduction of the With…End With block in VB. Local references are much faster than COM references.

if isEmpty(Session("Curcolor")) then
   Session("CurColor") = "FF0000"
end if
curColor = Session("CurColor")

That's almost all the VBScript code in the file. The remainder of the file displays the table, the drop-down list, and the button inside a <form> tag.

<form name="frmColor" method="post"
   action="selfmod.asp?Submitted=True">

There are several types of input controls. The available input types correspond roughly to text fields, buttons, combo boxes, and list boxes, although they act slightly differently than the equivalent Windows common controls.

You saw the text field and submit button types in the previous project. The drop-down list is slightly different. It's not an <input> tag at all, it's a <select></select> tag. The <select> tag contains a list of <option> tags that contain the list data. Each option tag can take a value parameter that specifies the value returned to the server. By default, the browser returns the option value for the item that's visible in the drop-down list. This is similar to a VB list box, which contains both visible items and itemdata. The ItemData array contains a list of long values, one for each item in the list. In contrast, the <option> tag can take any type of value, although it returns them all as text. You can also preselect a specific option by adding a selected parameter to that option tag. The selected parameter does not require a value.

One special type of input control is hidden. A hidden input doesn't display, so you can use it to pass values from one file to another. In this case, I'm using hidden inputs just so you can view the value by selecting View Source from the browser.

<input type="hidden" value="<%=curcolor%>">
<input type="hidden" value="<%=Session("curcolor")%>">

This is a trivial example that you would never use in practice because you would have problems if more than one person accessed the file. However, it does illustrate two important points about ASP files:

  • Because ASP files are text files, you can change them easily. To update an ASP-based application, you simply update the text files that contain the application code. No registration entries to worry about, no need to stop the server, no DLLs or large executables, and no installation programs to write. That's powerful stuff.
  • ASP files can rewrite themselves. You can't do that in VB (although you can write VB code that writes ASP pages). You can do this because the code contained in the ASP file is loaded into memory for compilation from the ASP file. After the file is loaded, the ASP engine releases the file lock. Depending on your server settings, the ASP engine can cache the file—but it does check to see if the file has changed for each request. Therefore, when you change the file, the ASP engine will display the contents of the changed file for the next request.

In the next section, you'll learn how to cache data in HTML or simple ASP files, rewriting them as needed when the data changes.

Tying It All Together—Caching Table Data

Imagine that, instead of changing a color value, you wanted to display the contents of a table. Sure, you can query a database for each request, but if the table data didn't change often, wouldn't it be nice if you could "cache" the table in an HTML file? Whenever the table data changed, you could re-create the HTML file.

I'm going to present the project here despite the data access requirements. Those of you who are not familiar with ActiveX Data Objects (ADO) may want to return to this example after reading Chapter 8, "Maintaining State in IIS Applications." You can also read up on ADO by taking a look at the VB Developer's Guide to ADO by Mike Gunderloy (Sybex, 1999).

This project consists of one ASP file. Each time you run the file, it lets you select a table from the pubs database. When you submit your selection, the program checks to see whether it already has the table data cached in an HTML file. If the table cache file exists, the ASP file simply returns the contents of the HTML file. If the table cache file does not exist, the program reads the table from the database, writes a cache file, then displays the contents of the file. The program also refreshes cached data if you pass a Refresh=True parameter in the URL. Administrators could use this feature to force the cached data to refresh.

**Note   **The 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 (including the Access database), navigate to http:// www.sybex.com. Click Catalog 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 complete code for the program is in Listing 3.2 at the end of this chapter. Just like the code in the previous project in this chapter, the selectTable.asp file is a form that submits to itself. The first part of this file contains the logic needed to differentiate between a request containing form data (Submitted=True) and an unsubmitted request, before the user has selected a table to display. Unlike the previous project, though, this one gets all its information from a database by using ADO. To read database information, you need to open a connection to the database:

Set conn = Server.CreateObject("ADODB.Connection")
conn.ConnectionString="pubs"
conn.CursorLocation= adUseClient
conn.Mode= adModeRead
conn.Open

In this case, you create the Connection object and set its ConnectionString property to a valid Data Source Name (DSN), pubs. Next, you set the Connection object's CursorLocation property to adUseClient, which tells the Connection object that you're going to use open database connectivity (ODBC) client cursors rather than SQL Server server-side cursors for any recordsets you retrieve using the connection. Because you're only going to be reading data, not updating, you set the Mode property to adModeRead. Finally, you open the connection using the Open method of the Connection object.

Next, you want to retrieve information about the tables in the database so you can populate the drop-down list. To do that, you need to get a recordset from the connection.

set R = Server.CreateObject("ADODB.Recordset")
R.CursorLocation=aduseclient
call R.Open("SELECT Name FROM SysObjects WHERE " & _
   Type='U' ORDER BY Name ASC",conn, _
   adOpenForwardOnly, adLockReadOnly,adCmdText)
end if

This code creates a Recordset object, tells it to use a client-side cursor, and to get a list of all tables in the pubs database. The rest of the code in the file displays the data from the recordset. It's fairly straightforward and similar to the code in the previous project, so I won't spend any time on it.

When the user selects a table from the drop-down list, the code redirects to the relative URL showTable.asp. You'll use redirection extensively in Web applications to provide messages and feedback, and to process requests based on user selections or input. The Response.Redirect method sends a header to the client browser. The header essentially means that the browser can find the information requested at a new address. The browser immediately makes a new request to the server for the specified page. So, (right now) redirection requires a round-trip to the client.

**Note   **Microsoft will soon provide server-side redirection, which won't require the round-trip to the client and therefore will be much more efficient.

The showTable.asp file contains almost no static HTML—just the bare minimum markup and two placeholders for the table data.

<html>
<head>
</head>
<body>
<!—-Start—-><!—-End-—>
</body>
</html>

The first thing the showTable.asp file does is check the TableName parameter. If the parameter is empty, the program redirects the user "back" to the selectTable.asp file:

if Request.QueryString("TableName") = "" then
   Response.Redirect "selectTable.asp"
end if

This code exists because, as I stated earlier, you don't know and can't control the order in which a user might request files from your application. If users simply type the URL to showTable.asp into their browsers, the program wouldn't know which file to display. Simply displaying whichever table the file may currently contain might be confusing; therefore, the code forces the user to make a choice before displaying any table.

Next, it caches the TableName parameter in a local variable:

requestedTablename= Request.QueryString("TableName")

If the requested table name is same as the previous request, the file simply shows the data already cached in the file; otherwise, it reads and formats the table data. It also refreshes the table data if you pass a "Refresh=True" parameter in the URL. You'll need some way to refresh the file if the table data changes. An "optional" parameter such as this gives you the opportunity to refresh the file if, for example, an administrator changes the data. Another way of doing this is to keep track of the data by looking at the highest Identity value (AutoNumber in Access), the date/time at which the table was last changed, or via a trigger that updates a row in a separate table whenever data in the main table is changed.

In this project, you "change" the table data by selecting a different table or by passing a "Refresh=True" parameter to the ASP page:

if requestedTablename <> tablename or _
   Request.QueryString("Refresh") = "True" then
   ' open the file, change the contents to that
   ' of the new table, then save the file.
End if

The process of opening a file is similar each time. You query the Server object by using the MapPath function to obtain the physical path for the file.

set fs = server.CreateObject("Scripting.FileSystemObject")
aFilename = server.MapPath("showTable.asp")
set ts = fs.OpenTextFile(afilename,ForReading,false)
s = ts.readall
ts.close
set ts = nothing

**Tip   **Microsoft's documentation states that one way to speed up sites is to limit or eliminate the use of the MapPath function. For greatest efficiency, you should cache path information in Application or Session variables when appropriate. Don't ever hard-code file paths in a Web application unless you're absolutely sure that the path will never change.

Next, you want to replace the old table name with the selected table name. Find the old table name parameter and value in the file string. Use VB's new replace function to perform string replacements.

s = replace(s, "tablename=" & chr(34) & tablename & _
   chr(34), "tablename=" & chr(34) & requestedTablename _
   & chr(34), 1, 1, vbBinaryCompare)

Now replace the table data. Because browsers ignore comments, you can conveniently use them as markers inside HTML files. I've used the two comment tags <!—Start—-> and <!—End—-> to mark the beginning and end positions for the table data. To make the replacement, you need to find the markers.

do
   startPos = instr(startPos+1, s, _
      "<!—-Start",vbBinaryCompare)
loop while mid(s, startPos + 9 ,1) <> "-"
startPos=startPos + len("<!—-Start") + 3
do
   endPos = instr(endPos + 1, s, _
   "<!—-End", vbBinaryCompare)
loop while mid(s, endPos + 7, 1) <> "-"

To get the data, you create a database connection and read the data from the selected table.

set conn = server.CreateObject("ADODB.Connection")
conn.ConnectionString="pubs"
conn.CursorLocation=aduseclient
conn.Mode= adModeRead
conn.Open
set R = Server.CreateObject("ADODB.Recordset")
R.CursorLocation=aduseclient
set R = conn.Execute("SELECT * FROM " & _
   requestedtablename,,adCmdText)

Whenever you retrieve data, you should check to make sure that the data you think is there is actually there. The Execute method returns a recordset regardless of whether it retrieves any data. Always check the recordset's End-of-File (EOF) property. The property will return True if the recordset is empty—that is, if no rows were retrieved. If the recordset contains data, then you can format the column headers by using the Field.Name property to get the name of each column.

if not R.EOF then
   tableData="<table align='center' border='1' _
   width='95%' COLS='" & R.Fields.Count & "'>"
   sTmp="<TR>"
   for each F in R.Fields
      sTmp = sTmp & "<TD><B>" & F.Name & "</B></TD>"
   next
   sTmp = sTmp & "</TR>"
   tableData = tableData & sTmp
end if

At the end of this loop, the recordset is still on the first row. You loop until the EOF property becomes true, placing each field value in a table cell. Note that this is a nested loop; the outer loop creates the rows while the inner loop fills the columns with data.

while not R.EOF
   sTmp = "<TR>"
   for each F in R.Fields
      if (F.Attributes and adFldLong) = adFldLong then
         if F.Type=adLongVarBinary then
            sTmp = sTmp & _
            "<TD valign='top'>(binary)</TD>"
         elseif F.ActualSize=0 then
            sTmp = sTmp & _
            "<TD valign='top'>&nbsp;</TD>"
         else
            sTmp = sTmp & "<TD valign='top'>" & _
            F.GetChunk(F.ActualSize) & "</TD>"
         end if
      else
         if isNull(F.Value) then
            sTmp = sTmp & _
             "<TD valign='top'>&nbsp;</TD>"
         else
            sTmp = sTmp & _
            "<TD valign='top'>" & F.Value _
            & "&nbsp;</TD>"
         end if
      end if
   next
   sTmp = sTmp & "</TR>"
   tableData = tableData & sTmp
   R.MoveNext
Wend

You need to decide what to do if the recordset does not contain any rows. In this case, the program returns a message in the first table row.

tableData= "There is no data in the table: " & _
   requestedTablename & ".<BR>"

Finally, don't forget to close the recordset and the connection and set them to Nothing. Setting them to Nothing frees up the memory. Strictly speaking, you don't have to do this at the end of a page because the ASP engine destroys the objects and frees the memory for variables created during page processing when the page ends. However, it's good practice for you to clean up explicitly. It also frees the memory somewhat sooner than the ASP engine can.

R.Close
set R = nothing
conn.Close
set conn= nothing

Finally, concatenate the table data into the file string between the start and end position markers in the file, then write the file string to disk.

s = left(s, startPos) & tableData & _
   mid(s, endPos)
set ts = fs.OpenTextFile(afilename,ForWriting,false)
ts.write s
ts.close
set ts = nothing
set fs = nothing

Now the file is ready to display, so you can redirect to the file you just wrote.

Response.Redirect "showTable.asp?TableName=" & _
   requestedTablename

Listing 3.2   Code for Providing Fast Access to Table Data ASP Project (selectTable.asp and showTable.asp)

************************************************* ' The selectTable.asp file ************************************************* <%@ Language=VBScript %> <% option explicit %> <%Response.Buffer=True%> <% dim submitted dim tablename dim R dim conn submitted=(Request("Submitted") ="True") if submitted then Response.Redirect "showTable.asp?TableName=" & _ Request("TableName") Else set conn = server.CreateObject("ADODB.Connection") conn.ConnectionString="DSN=pubs;UID=sa;PWD=" conn.CursorLocation=aduseclient conn.Mode= adModeRead conn.Open set R = Server.CreateObject("ADODB.Recordset") R.CursorLocation=aduseclient call R.Open("SELECT Name FROM SysObjects WHERE Type='U' _ ORDER BY Name ASC",conn,adOpenForwardOnly,adLockReadOnly,adCmdText) end if %>

Select Table
Select a table name from the list, then click the "Display Table" button.
Table: <% do while not R.EOF Response.Write "<option " if R("Name").value = tableName then Response.Write "selected " end if Response.Write "value='" & R("Name") & "'>" & R("Name") & "" R.movenext loop R.close set R = nothing conn.Close set conn=nothing %>
************************************************* ' The showTable.asp file ************************************************* <%@ Language=VBScript %> <% option explicit %> <%Response.Buffer=true%> <% dim submitted dim tablename dim tabledata dim sTmp dim requestedTablename dim afilename dim fs dim ts dim s dim startPos dim endPos dim conn dim R dim F tablename="authors" if Request.QueryString("TableName") = "" then Response.Redirect "selectTable.asp" end if requestedTablename= Request.QueryString("TableName") if requestedTablename <> tablename or _ Request.QueryString("Refresh") = "True" then _ set fs = server.CreateObject _ ("Scripting.FileSystemObject") aFilename = server.MapPath("showTable.asp") set ts = fs.OpenTextFile(afilename,ForReading,false) s = ts.readall ts.close set ts = nothing s = replace(s, "tablename=" & chr(34) & _ tablename & chr(34), "tablename=" & chr(34) & _ requestedTablename & chr(34),1,1,vbBinaryCompare) do startPos = instr(startPos+1, s, _ "<!—-Start",vbBinaryCompare) loop while mid(s, startPos + 9 ,1) <> "-" startPos=startPos + len("<!—-Start") + 3 do endPos = instr(endPos + 1, s, _ "<!-—End", vbBinaryCompare) loop while mid(s, endPos + 7, 1) <> "-" set conn = server.CreateObject("ADODB.Connection") conn.ConnectionString="pubs" conn.CursorLocation=aduseclient conn.Mode= adModeRead conn.Open set R = Server.CreateObject("ADODB.Recordset") R.CursorLocation=aduseclient set R = conn.Execute("SELECT * FROM " & _ requestedtablename,,adCmdText) if not R.EOF then tableData="<table align='center' border='1' " & _ "width='95%' COLS='" & R.Fields.Count & "'>" sTmp="" for each F in R.Fields sTmp = sTmp & "" & F.Name & _ "" next sTmp = sTmp & "" tableData = tableData & sTmp while not R.EOF sTmp = "" for each F in R.Fields if (F.Attributes and adFldLong) = _ adFldLong then if F.Type=adLongVarBinary then sTmp = sTmp & _ "" & _ "(binary)" elseif F.ActualSize=0 then sTmp = sTmp & _ "" & _ " " else sTmp = sTmp & _ "" & _ F.GetChunk(F.ActualSize) & _ "" end if else if isNull(F.Value) then sTmp = sTmp & _ "" & _ " " else sTmp = sTmp & _ "" & _ F.Value & " " end if end if next sTmp = sTmp & "" tableData = tableData & sTmp R.MoveNext wend else tableData= "There is no data in the table: " _ & requestedTablename & ".
" end if R.Close set R = nothing conn.Close set conn= nothing s = left(s, startPos) & tableData & mid(s, endPos) set ts = fs.OpenTextFile(afilename,ForWriting,false) ts.write s ts.close set ts = nothing set fs = nothing Response.Redirect "showTable.asp?TableName=" & _ requestedTablename end if %> <!—-Start-—> <!—-End-—>