Fitch & Mather Stocks: Web Application Design

Scott Stanfield
Vertigo Software, Inc.

July 1999

Summary: Describes the design and implementation of the Fitch & Mather Stocks Web application. (17 printed pages)

Overview

The Fitch & Mather Stocks (FMStocks) Web application uses Active Server Pages (ASP) to build a site driven by dynamic database content. The main functional groups of the site include:

FMStocks uses the FMStocks_Bus.dll Business Logic Layer (BLL) component, which in turn uses the FMStocks_DB.dll data access layer (DAL) to access the Microsoft SQL Server™ 7.0 database. The Web application consists of the top tier of our Microsoft Windows® Distributed interNet Applications (DNA) architecture Web application.

Web Site Navigation

Figure 1 illustrates the rough FMStocks Web application navigation and layout. This article will explain how the ASP use the BLL component to build an integrated Windows DNA application.

Figure 1. FMStocks site navigation

For a guided tour of an application that covers Web site graphics, prototyping, GIF images, and cascading style sheets, please read the Fitch & Mather Expense Report (FMCorp) Web site documentation at http://www.fmcorp.com/. I built upon tips and techniques I learned from writing FMCorp.

Types of Pages

The table below gives you a rough idea of the types of pages used by FMStocks. The files and sections in bold are discussed in the rest of this document.

Categories Pages
Static HTML News.htm
Static HTML candidates
(uses server-side includes)
Home.asp
401k.asp
About.asp
Login handling Default.asp
Logout.asp
Read-only dynamic
(ticker lookup)
TickerList.asp
TickerDetail.asp
Read-only dynamic
(account information)
Portfolio.asp
AccountSummary.asp

SellStock.asp
SellStockReceipt.asp

Transaction-enabled pages that write to the database BuyStock.asp
NewAccount.asp
SellStockAction.asp
Cascading Style Sheet and client-side JavaScript jscripts.js
Styles.css
Framework: include files and templates Template.asp
t_begin.htm
t_end.htm
t_head.asp
strings.vbs
Debugging _runsql.asp
_version.asp

Framework

All pages in FMStocks are based on the same template that drives our page framework. In rough terms, the upper-left area holds a GIF image, whose colors bleed seamlessly into the light-blue menu that spans the top part of the page. Along the left side of the page appears a column of rollover menus. The large lower-right quadrant hosts the content of each page. See Figure 2.

Figure 2. Example of Fitch & Mather Stocks page

The rollover menu code was taken straight from FMCorp. Client-side Microsoft JavaScript™ code in jscripts.js detects when the mouse moves over a particular table row. Dynamic hypertext markup language (DHTML) code changes the background color for the whole cell, simulating a mouse-over menu activation. Using this technique allows us to have dynamic menus without needing a lot of small GIF images.

Template.asp

One of the best programming practices that grew out of FMStocks was the creation of a simple template and server-side include files that make it very easy to add new content to the site.

Adding a new ASP page to the project is easy. First, we make a copy of Template.asp, modify the <TITLE> tag, and then add the content between two server-side includes. The Template.asp file makes it easy to add new pages to the site, since it handles all the security and navigation aspects.

After we figured out how to code the page layout, the common parts were separated into individual files (t_head.asp, t_begin.asp and t_end.asp). Then template.asp was created as a guide to show how to combine them back together. The complete source for template.asp is shown below in the color-coded Figure 3.

Figure 3. Template.asp source

Every ASP page on the site starts from a copy of this template.asp file. When creating a new page, we simply change the title, replace "HELLO WORLD" with our content, and insert any server-side pre-processing in the <HEAD> section. The home.asp page, as shown in Figure 1, is the page users see after login.

Figure 4. Home.asp created from a copy of Template.asp

t_head.asp

The first server-side include in template.asp pulls in this code:

<!--#include file="strings.vbs"-->

<script SRC="jscripts.js" LANGUAGE="JavaScript"></script>
<meta HTTP-EQUIV="Expires" CONTENT="Tue, 04 Dec 1993 21:29:02 GMT">
<link rel="stylesheet" href="styles.css">

<%
   if not Response.IsClientConnected then
      Response.End()
   end if

   Dim g_AccountID 
   Dim g_strError
   Dim g_bUseMenu
   
   g_bUseMenu = true

   VerifyLogin() 
%>

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
function VerifyLogin()

   if not Response.IsClientConnected then
      Response.End()
   end if

   'Check to see if they are logged in or are at the login page.
   if Request.Cookies("Account") <> "" then
      g_AccountID = Request.Cookies("Account")("AccountID")
   else
      g_AccountID = 0
   end if
   
   If g_AccountID = 0 then
      dim strASP   ' Current ASP page
      strASP = ucase(Request.ServerVariables("Script_Name"))
      
      if instr(1, strASP, ucase("_runsql.asp")) = 0 and _
         instr(1, strASP, ucase("_version.asp")) = 0 and _
         instr(1, strASP, ucase("default.asp")) = 0 and _
         instr(1, strASP, ucase("NewAccount.asp")) = 0 and _I
         instr(1, strASP, ucase("about.asp")) = 0 then
            Response.Redirect("logout.asp")
            Response.End
      end if
   end if
end function

function BuildErrorMessage()
   g_strError = g_strError & "<p><b>Actual Error Message:<br></b>" & _
     Err.description & "</p>"
   g_strError = g_strError & "<p><b>Source/Call Stack:<br></b>" & _ 
     Err.source & "</p>"
   g_strError = g_strError & "<br><br><br>"   
end function

' Helper functions.

function rw(s)
   Response.Write s
end function

function rwbr(s)
   Response.Write s & "<br>"
end function

function rwp(s)
   Response.Write "<p>" & s & "</p>"
end function

function nbsp(n)
   for i = 0 to n
      rw("&nbsp")
   next
end function
</SCRIPT>

The first line includes a set of common string constants from the file strings.vbs.

The next three lines include the style sheet and mouse-over Microsoft JScript® routines. The meta tag tells the browser that this page has already expired, forcing the browser to make a round-trip when instructed to refresh. This is actually a good thing because Web applications, by their very nature, are dynamic and should require round-trips.

The first block of server-side script sets up three global variables. g_AccountID will hold the currently connected AccountID. g_strError holds the current error message, if any. g_bUseMenu is true or false, depending on if the page wanted the menu to be rendered (false only for default.asp and NewAccount.asp).

VerifyLogin() is called to determine if the user is allowed to access this page, if they are not logged in. We know if they are logged in because the login ASP logic sends the client a cookie that holds the AccountID. VerifyLogin queries for that cookie. If it exists, the value is stashed away in the global variable g_AccountID. Otherwise, we look to see if the current page allows a non-validated user to view it. Of course, the login page (default.asp) and new account page (NewAccount.asp) should not require the user to be logged in to view them. Page processing is redirected to logout.asp if we detect an illegal page access.

BuildErrorMessage is a central helper function that builds a consistent error string. It is used only by pages that use the BLL.

The final small functions are shortcut ways of emitting code or non-breaking spaces.

t_begin.htm

This include file is inserted into the page right after the <BODY> tag. It creates two sets of tables to manage the dynamic menus and to give the page-specific content a place to go. If g_bUseMenu is false, it doesn't emit the menus.

t_end.asp

This file is meant to be included right before the closing </BODY> tag. It closes the table tags opened by t_begin.htm. Next, it reserves space at the bottom of the page to display the error message, if there is one. The whole file is shown as follows:

   <!-- end -->
   
   </td></tr>
</TABLE>

<table>
<tr><td width=10><td>
<div class=smalltext>Copyright &copy; 1999 Microsoft Corportion.
   <br>Developed by 
   <a style="color:gray; cursor:hand">Vertigo Software, Inc.</a></div>
<BR>
<div class=error><%=g_strError%></div>
</table>

Note   Both t_begin.htm and t_end.htm, despite their suffix, contain ASP code. The server-side include mechanism doesn’t care what their suffix says. Originally these files were pure HTML, until I included code to turn dynamically turn off the menu and to emit error messages.

Login and Site Security

The first page every user hits is default.asp which happens to be our login page, as shown in Figure 5.

Figure 5. Login page (default.asp)

The page is really a simple two-field form that asks the user for an e-mail address (their login) and a password. The <FORM> tag posts the information right back to default.asp for processing.

When I first started doing ASP programming, I thought this technique was a little strange. How does the page know if it should be processing the login request or just show the page? Take a look at the top of the page:

<BODY LANGUAGE=javascript onload="return window_onload()">
<%
dim m_msg, m_email, m_password
ProcessLogin
%>
<!--#include file="t_begin.htm"-->

After the initial <BODY> tag, we set up three page-level variables. The next line calls a server-side function called ProcessLogin. The source to which is shown here:

<%
sub ProcessLogin

   ' By default, show the page with the default login test account "ta"
   ' and password "ta1". Otherwise, show what the user most recently
   ' typed in.
   m_email = trim(Request("login"))
   m_password = trim(Request("password"))
   if m_email = "" then m_email = "ta1" end if
   if m_password = "" then m_password = "ta" end if

   ' Are we trying to log in?   
   if Request("LoginButton") <> "" then
      ' We're on this page because the Login button was pressed.
      ' Attempt to login
      
      on error resume next
      dim objAccount
      dim AccountID, FullName, bIsValid
            
      set objAccount = Server.CreateObject("FMStocks_Bus.Account")
      
      ' Common deployment error: this error handling block catches the 
      ' common case where the FMStocks_Bus component isn't registered on 
      ' the app server.
      if Err.number <> 0 then
         BuildErrorMessage()   
         m_msg = "<div class=error>" & CREATE_OBJECT_FAILED & "</div>"
         Exit sub
      end if      
   
      bIsValid = objAccount.VerifyLogin(m_email, m_password, FullName, _
                 AccountID)
      set objAccount = nothing
      
      ' Common database error: if the DSN is incorrect, or the database 
      ' is down or not reachable, this is the first place 
      if Err.number <> 0 then
         BuildErrorMessage()
         m_msg = "<div class=error>" & DATABASE_ERROR & "</div>"
         exit sub
      end if
      
      if bIsValid then
         ' Login was successful. Pass back the AccountID in a cookie for 
         ' later.
         Response.Cookies("Account")("AccountID") = AccountID
         Response.Redirect("home.asp")   
      else
         m_msg = "<span class=error>" & LOGIN_PASSWORD_FAILED & _
                 "</span><br>"
      end if      
   end if
   
   ' Setup the default message if we're here because a new account was  
   ' just created
   if Request("NewAccount") = 1 then
      m_msg = "<span class=info>" & LOGIN_NEWACCOUNT_CREATED & _
              "</span><br>"
      m_password = ""      
   end if
      
end sub
%>

The first four lines pull the form variables out of the Request object and store them in page-scoped variables. Now the first time you access this page, the Request object will be empty, because no form has been posted back to the page. That’s all right—the second two lines set the default to “ta1”, the first test account. If Request(“LoginButton”) is empty, the rest of the processing is skipped. This means the form has not yet been posted back to the page for processing.

If Request(“LoginButton”) is not an empty string, it means the button was pressed and the user is trying to log in. The rest of the code attempts to validate the e-mail account and password against the business logic layer (BLL).

The BLL has a class called Account that supports a method called VerifyLogin. We pass it an e-mail address and password; it returns True if they match, along with two output parameters, the full name and the AccountID. The Server.CreateObject line creates an instance of the object. Since this is the first line of server-side code that deals with MTS COM objects in general, it’s worth a small side trip to discuss error handling.

One more thing: t_head.asp includes the file, strings.vbs, that contains localizable error strings. Using a string resource file allows localization experts to “port” a site without having to modify any ASP code blocks. Strings.vbs predefines about a dozen VBScript strings and initializes them to English messages. A few of these string “constants” are used in the login page above (LOGIN_NEWACCOUNT_CREATED, CREATE_OBJECT_FAILED, etc.)

Error Handling

If FMStocks_Bus.dll is not registered on the server, and we didn’t enable our own error handling code via on error resume next, Figure 6 shows the message you’d get when logging in. Not very pretty.

Figure 6. Default ASP error message

If we anticipate and handle potential errors caused by our middle-tier components, our site will appear more robust. Using on error resume next allows us to query the Err object to see if an error or exception was recently thrown. The first error handling block is shown below:

      set objAccount = Server.CreateObject("FMStocks_Bus.Account")
      
      ' Common deployment error: this error handling block catches the 
      ' common case where the FMStocks_Bus component isn't registered on 
      ' the app server.
      if Err.number <> 0 then
         BuildErrorMessage()   
         m_msg = "<div class=error>" & CREATE_OBJECT_FAILED & "</div>"
         Exit sub
      end if      

If the Err.number property is not 0, then something bad has happened during the Server.CreateObject call. The most common cause is that someone forgot to register FMStocks_Bus. BuildErrorMessage() is a helper function from t_head.asp that builds a nicely formatted error message, containing all kinds of gory details that only interest programmers. It’s placed in a page-scoped variable and spit out at the bottom of the page in t_end.htm.

The m_msg variable is used by this page to place a user-friendly message in the body of the page. If we trigger the error again, the resulting page looks like the one shown in Figure 7.

Figure 7. Two levels of error handling.

The first block of red text is for the end-user; the bottom red text is for the developer. This message is emitted by t_end.htm, so it could be turned off once the site is deployed, and re-enabled during debugging. A nice side effect of this method is that you can set the page-level variable g_strError to any string you want, and it will show up at the bottom of the page.

The next chunk of code tries to use the object’s VerifyLogin method. If it fails, there’s probably something wrong with the database connection or the underlying query. If I attempt to log in while the database is paused, the resulting error message looks like the one shown in Figure 5.

      bIsValid = objAccount.VerifyLogin(m_email, m_password, FullName, _
                 AccountID)
      set objAccount = nothing
      
      ' Common database error: if the DSN is incorrect, or the database 
      ' is down or 
      ' not reachable, this is the first place 
      if Err.number <> 0 then
         BuildErrorMessage()
         m_msg = "<div class=error>" & DATABASE_ERROR & "</div>"
         exit sub
      end if

Figure 8. Error messages displayed when the database is paused.

Other common error messages that pop up during development occur when you are caught passing incorrect arguments to a stored procedure or violation a table constraint.

While error handling does work in ASP, it’s a bit tedious to code for each case. I’d prefer to have a single error handling block, like you can have with VBA. Future versions of VBScript (later than VBScript 5.0) may incorporate this feature.

State Management

Continuing our walk through the ProcessLogin method, we finally come to the block that handles a successful login:

      if bIsValid then
         ' Login was successful. Pass back the AccountID in a cookie for 
         ' later.
         Response.Cookies("Account")("AccountID") = AccountID
         Response.Redirect("home.asp")   
      else
         m_msg = "<span class=error>" & LOGIN_PASSWORD_FAILED &  _
                 "</span><br>"
      end if

Here’s the extent of our state management. We simply take the corresponding AccountID and pass it back in a session cookie. A session cookie (not to be confused with the native ASP Session object) is removed when the user navigates away from the site or shuts down the browser—it never hits the disk.

Warning   We made some simplifying assumptions during the development of FMStocks and made a conscious decision to go with this serious security hole. You would never want to deploy a solution that passes back the key that would unlock a user’s account in an unencrypted session. Use SLL for your own login page and for subsequent pages that need access to that key. The native ASP Session object is secure, but it wasn’t used in FMStocks because we didn’t want to incur any extra ASP overhead for our performance testing.

The next line in this thread simply redirects processing to home.asp, completing the login scenario.

About the Author

Scott Stanfield is the president of Vertigo Software, Inc. Vertigo Software is a San Francisco Bay Area-based consulting firm that specializes in the design and development of Windows DNA applications and components. He can be reached at scott@vertigosoftware.com or on the Web at http://www.vertigosoftware.com/.

For More Information