After you have completed the code changes to the Signon WebClass, you should be able to sign on. You'll need to change what happens for a successful sign-on. In this case, you want execution to continue, but in a different WebClass. You can't use the NextItem property of the Signon WebClass, for example, Set NextItem= Reports, because the Signon WebClass doesn't know anything about the Reports WebClass.
Remember, the server instantiates WebClasses via a Server.CreateObject request from an ASP page, so to activate any particular WebClass, you need to execute the ASP page associated with that WebClass. Change the code in Signon_frmSignon as follows.
Original Code:
With Response
.Write "You are signed on." & "<BR>"
.Write "Signon=" & aSignon & "<BR>"
.Write "Password=" & aPassword & "<BR>"
.Write "Click <a href='" & URLFor(TestSecurity) _
& "'>Test Security</a> to "
.Write "go to the secured page."
End With
New Code:
With Response
.Redirect "Reports.asp?WCI=SelectAccount"
As with any redirection, you lose some efficiency because the browser needs two round trips to the server to retrieve a page. The browser sends a request, the server responds with a redirect header, then the browser sends a second request to the new URL. Only then will the server respond with the page. You should keep that process in mind. It's not a problem unless you start redirecting between WebClasses often.
If you haven't tried it already, run the program. Try to sign on. If you can sign on properly, you'll get an error when the Reports WebClass receives the Select-Account request, because it doesn't exist yet. When everything seems to be working, create a new HTML template file and name it SelectAccount.htm.
Tip
Remember to create a Templates subdirectory of the AccountInfo directory. Save the original copies of any HTML template files there to keep VB from renaming them with numeric extensions as you import them into your WebClasses.
Listing 7.2 contains the HTML code for the SelectAccount HTML template.
Listing 7.2: Code for the SelectAccount HTML Template
<html><head><title>Select Account</title></head>
<body>
<center>
<h2>
Accounts List For: <WCACCOUNTHOLDER></WCACCOUNTHOLDER>
The two replaceable markers hold the positions for the customer name and the list of accounts belonging to that customer. You'll replace the <WCACCOUNT> </WCACCOUNT> marker with table rows containing anchor tags that display the account type and number. Each anchor is a link to an AccountOptions options page that displays a list of actions a customer can take—in this case, inquire about the account balance or view the account history. The SelectAccount_Process-Tag event code handles the replacements:
Private Sub SelectAccount_ProcessTag _
(ByVal TagName As String, TagContents As String, _
SendTags As Boolean)
Dim R As Recordset
Dim i As Integer
Select Case LCase(TagName)
Case "wcaccountholder"
TagContents = Session("FirstName") & " " _
& Session("LastName")
Case "wcaccount"
' show the list of accounts for the signed-on user
' create a table row for each account
If getAccountList(R, _
CLng(Session("CustomerID"))) Then
While Not R.EOF
URLData = R!AccountNumber.value
TagContents = TagContents & _
"<tr><td><a href=" _
& Chr$(34) & _
URLFor("AccountOptions") _
& Chr$(34) & ">" _
& R!accountType & " (" & _
R!AccountNumber & ")</a>" _
& "</td></tr>"
R.MoveNext
Wend
R.Close
End If
End Select
End Sub
You should notice several things in this code. Replacing the <WCACCOUNT-HOLDER> tag is straightforward, but replacing the <WCACCOUNT> tag with the list of accounts is not. Because it seemed likely that I might want to get the list of accounts in other places in the program, and because retrieving the list requires database access, I wrote a separate subroutine. The getAccountList subroutine takes a Recordset parameter and a CustomerID. The subroutine sets the Recordset parameter to a list of accounts by looking in the CustomerAccounts table to find which account numbers are associated with that CustomerID, then selecting those rows from the Accounts table.
The most important thing to notice in the SelectAccount_ProcessTag event code is that for each account, the program sets a WebClass property called URLData. The URLData property accepts a string, which the WebClass subsequently appends to all URLs generated by the URLFor method. When used in this way, URLData is equivalent to QueryString data on an ASP page. When a user clicks on an anchor tag created by URLFor, the browser navigates to the URL, and also returns the URLData that the WebClass appended to the href parameter in the anchor tag.
Here's how it works:
1.
You set the URLData property to a string, "XXX" for example.
2.
You use the URLFor method to create a URL for an item or method in your WebClass.
3.
The WebClass appends the string "&WCU=XXX" to the URL.
4.
The browser returns XXX to the WebClass when the user clicks the anchor tag containing the URL.
5.
You can retrieve the value by checking the URLData property.
Although this is by no means the only way to pass data from page to page, it is convenient, partly because it saves you from having to format the URL in code. You'll see more about passing information from page to page in Chapter 8, "Maintaining State in IIS Applications."
One other convenient side effect of using the URLData property rather than hand-formatting QueryString data is that the WebClass will append the URLData to every link that it finds in an HTML template when you call the WriteTemplate method that contains a WCU parameter. In other words, you neither have to know the data that you want to send in advance nor add it in the ProcessTag event for that template; the WebClass will do that for you. All you need to do is make sure that each URL for which you want to return the URLData ends with either ?WCU= or &WCU= (the equals sign is optional).
In this application, the URLData parameter serves to maintain a property page sequence. It ensures that the user must select an account before getting a report, because you can check the URLData property in the report pages and redirect to the AccountOptions WebItem if the property is blank. Here's the relevant code:
' check for out-of-sequence inquiry
If Len(URLData) = 0 Then
Response.Redirect URLFor(AccountOptions)
End If
The report pages are custom WebItems. Both of them check to ensure that the user has selected an account before displaying the information.
An unfortunate side effect of using URLData to pass information from page to page is that the value is visible in the browser's address bar. For example, you would see a URL similar to this after selecting an account: http://localhost/ AccountInfo/Reports.asp?WCI=AccountOptions&WCU=451-586-67. Therefore, you can't use URLData to pass sensitive information unless you encrypt the information. In this case, you're passing an unencrypted bank account number—something you might not want to do in a real application.
To guard against the possibility of a person requesting information about an account that they do not own, the application also performs a database lookup in the Respond event for each report. If the user identified by the sign-on and password does not own the requested account, the report code returns a message stating that the user is not authorized to view the account.
Figure 7.4 shows the SelectAccount page for a typical customer.
Each of the links constructed in the SelectAccount WebItem points to the AccountOptions WebItem. As soon as the user selects an account, the WebClass fires the AccountOptions_Respond event. At that point, you can retrieve the value of the URLData property to find out which account the user selected.
Private Sub AccountOptions_Respond()
On Error GoTo Err_AccountOptions_Respond
methodName = "AccountOptions_Respond"
Session("CurrentAccount") = URLData
Dim R As Recordset
Set R = conn.Execute("SELECT * FROM Accounts " & _
The AccountOptions_Respond event performs a database lookup on the requested account number and caches the account information in Session variables. You'll use those Session values to display specific information about the selected account on the report pages. After caching the account information, the AccountOptions.WriteTemplate command causes the WebClass to process the AccountOptions template. Figure 7.5 shows the Account Options page for a typical customer.
The reports themselves are relatively straightforward. At this point, you've set up a sequence of three conditions that the request must pass before the WebClass will display either the Balance Inquiry or the Account History report:
1.
The user must have signed on with a valid sign-on and password.
2.
The user must have selected an account.
3.
The request must be for one of the accounts owned by the user.
If all the conditions have been met, the browser displays the requested report for the account number contained in the URLData property. Looked at another way, in pseudocode, the tests look like this:
If (the user is not signed on) Then
Redirect to the signon page
End If
If (the user has not selected an account) Then
Redirect to the account selection page
End If
If (the account requested does not belong to the user) Then
Display an "unauthorized" message
End If
The first check is in the BeginRequest event for the WebClass, because users should not be able to reach any item in the Reports WebClass until they have signed on successfully. The other two checks are within the Respond events for the BalanceInquiry and AccountHistory. I chose to create the reports as custom WebItems, but there's no reason why they couldn't be HTML templates just as easily. Here's the code for the BalanceInquiry_Respond event:
SQL = "INSERT INTO Transactions (AccountNumber, " & _
"CustomerID, TransactionDate, " & _
"TransactionType, Amount, Balance) "
SQL = SQL & "VALUES('" & _
Session("AccountNumber") & "', "
SQL = SQL & Session("CustomerID") & ", '" & _
CStr(Now) & "','Inquiry', 0, " & _
CDbl(Session("Balance")) & ")"
conn.Execute SQL
Exit_BalanceInquiry_Respond:
Exit Sub
Err_BalanceInquiry_Respond:
Err.Raise Err.Number, Err.Source & _
methodName, Err.Description
Resume Exit_BalanceInquiry_Respond
End Sub
Just to keep things realistic, querying the account balance writes a transaction inquiry record. The AccountHistory WebItem doesn't add a transaction record, it just writes all the applicable rows from the Transactions table to the browser, in reverse date order:
Private Sub AccountHistory_Respond()
On Error GoTo Err_AccountHistory_Respond
methodName = "AccountHistory_Respond"
Dim R As Recordset
Dim F As Field
Dim dAccount As Dictionary
Dim d As Dictionary
Dim V As Variant
Dim s As String
Dim authorized As Boolean
' check for out-of-sequence inquiry
If Len(URLData) = 0 Then
Response.Redirect URLFor(AccountOptions)
End If
' check for unauthorized inquiry
For Each V In Session("AccountList")
If V = URLData Then
authorized = True
Exit For
End If
Next
If Not authorized Then
Response.Write "You are not authorized to " & _
"view this account."
Exit Sub
End If
Set R = conn.Execute("SELECT * FROM Accounts WHERE " _
Set R = conn.Execute("SELECT * FROM Transactions " & _
"WHERE AccountNumber='" & URLData & "' ORDER " & _
"BY TransactionDate DESC", , adCmdText)
With Response
.Write "<html><head><title>Account History " & _
"</title></head>"
.Write "<body><center>"
.Write "<table width='90%' align='center' " & _
"border='1'>"
.Write "<tr><td align='center' bgcolor=" & _
"'#80FFFF'><font size='5'><b>Account " & _
"History</b></font><br>Account: " & _
dAccount("AccountType") & " " & _
dAccount("AccountNumber") & "</td></tr>"
.Write "<tr><td align='left'>This report " & _
"shows your entire account history, " & _
"beginning with the most recent " & _
"transactions.</td></tr>"
.Write "</table>"
' display a table
s = RecordsetToTable(R, "90%", "center", 1, _
True,TransactionID,TransactionDate, _
TransactionType,Amount,Balance")
s = Left$(s, Len(s) - Len("</table>"))
s = s & "<tr><td colspan='5' align='right'>" & _
"Starting Balance: " & " " & _
Format$(dAccount("StartingBalance"), _
"Currency") & "</td></tr>"
s = s & "</table>"
.Write s
R.Close
Set R = Nothing
Set d = Nothing
.Write "</body></html>"
End With
Exit_AccountHistory_Respond:
Exit Sub
Err_AccountHistory_Respond:
Err.Raise Err.Number, Err.Source & _
methodName, Err.Description
Resume Exit_AccountHistory_Respond
End Sub
When you run the program, the reports look similar to Figures 7.6 and 7.7.
Despite the convenience of URLData, it isn't a complete replacement for Query-String-formatted data, and it doesn't stop you from using QueryString data if you desire—even in conjunction with the URLFor method. It's perfectly acceptable to write code like this:
If you were to look at the result of that code by selecting View Source in the browser, the href parameter of the anchor tag would look something like the following:
Back on the server, you can retrieve all the QueryString parameters by using the Request.QueryString collection. Note that you can retrieve the URLData using WCU as the variable name, for example:
myVar=Request.QueryString("WCU")
After you've run the program and seen how it works, remember that there are several ways that users can get to a URL. Try saving one of the pages as a favorite. Then, close your browser, open a new instance of the browser, and try navigating to the favorite. What happens? The program should re-route you to the Signon page. You should also check to see what happens if you sign on as one person, but try to change the URL in the address bar to look at another person's account. You should get an unauthorized message, similar to Figure 7.8.
Using the URLData value and some authorization checks, you've controlled the flow in this program. Users cannot reach a report page without moving through the Signon, Account Selection, and Account Options pages. But what if you had a program, like a linear story, in which you wanted people to be able to move only one page at a time, either forward or backward. In other words, if you had 10 pages, a user should always start at page 1, then see page 2. From page 2, the user could either back up to page 1, or move forward to page 2, but could not access page 3.
To create such a program, write the program rules first, then implement them in code. The rules are as follows:
•
All users must start on the first page.
•
From the first page, users may quit or go to the next page.
•
From the last page, users may quit or go to the previous page.
•
For all other pages, users may move only to the next or the previous page, or quit.
You can visualize this programmatically as if the user's position were a pointer into an array. The array items are the pages. When the user first begins the program, the array pointer is empty; therefore you redirect to the first page, regardless of which page was requested. Figure 7.9 shows all the possible valid requests for the program.
You store the current page in a Session variable, in a cookie, in a URLData or QueryString variable, or in a hidden form field. Each time the user requests a page, you:
1.
Find the user's current position in the array, as stored in your variable.
2.
Find the position of the requested item in the array.
3.
If the two items are not contiguous—that is, if they're not next to one another, and the request is not a Quit request, you deny the request.
Exactly what happens when you deny a request is up to you and depends on the requirements of the program. The most likely scenario is that you simply ignore the request and redisplay the current page, but you might decide to display an invalid page message. Alternately, you could determine the direction from the relative positions of the user's current position and the request and move one page in the requested direction.
In this chapter, you've seen how to repurpose WebClasses, use more than one WebClass in an application by redirecting between WebClasses, and find and control the user's progress through your application. In the next chapter, you'll extend and generalize these concepts to maintain a user's state in your application.