A.T.G. Consulting, Inc.
Many programmers quail at the sight of
source code for "active server pages". The source code is a clumsy
hybrid of at least four dissimilar 每 even incompatible 每 languages.
And because ASP tends to be treated as a somewhat "informal"
language, people sometimes play fast and loose with standards and
conventions. In general we believe that you will have much better
results in ASP if you follow these simple guidelines:
- Many people tend to think of ASP as just
a way of inserting custom elements into a pre-existing HTML page;
instead you should try to think of your ASP code as a computer
program that you are designing to output HTML code. It might sound
like a minor quibble or touchy-feely mind game, but we truly
believe that this mental attitude is at the heart of your success
or failure as an ASP coder.
- Always keep track of where you are. ASP
lets you mix and match server-side code and client-side code in a
fearful mess. You should always remember where you are.
Techniques that sometimes help include writing server-side and
client-side script in different languages wherever practical.
- Treat your server-side code like a
computer program. Always declare your variables, always indent
your code, always make it as readable and as clear as possible.
Split wide lines of code, and continue them on another line so
that they can be read without scrolling. Use white-space,
indentation, and headers judiciously to make your programs as
clear as possible. Remember that someday, if everything goes
perfectly, someone will have to figure out what you meant... It
might even be you.
- Avoid wizards, design-time-controls,
code-writing tools, drag-n-drop design tools, and anything else
that creates code elements you can't understand, can't debug, and
can't control. You may think these tools will save you time -- but
you will waste more time in the long run tracing and fixing
unexpected anomalies and bugs in someone else's tools. Avoid these
things wherever possible.
- Know your target browser audience, and
test your pages religiously -- in as many different browsers and
versions as possible. Browser inconsistencies always creep in
slowly, but eventually become ingrained. If you catch them early
you will waste less time in the long run. And you should not be
lulled into complacency in single-browser environments: point-rev
changes between versions of IE have often broken our pages.
If you do these things your life won't be
perfect, but you won't waste as much time as you would otherwise.
In general we tend to follow these
recommendations during development:
- VBScript code is indented in keeping with
standard indenting practices. Although it might be nice, it is not
necessary or important to indent any HTML or client-side
JavaScript code included within the VBScript to match the
VBScript's indentation.
- All active server pages are commented
profusely with VBScript comments 每 enclosed inside "<% %>"
tags so that they are only visible to server-side developers. In
general we try to keep downloaded HTML code (including JavaScript)
free from user-readable comments.
- Module comments should include a section
at the beginning (top) of the module that identifies the name and
purpose of the module.
- In-stream comments throughout the code
describe the functions being performed, explain any unusual
techniques, and tell why any surprising techniques were performed
the way they were. At the same time it is not important to
comment every line, or every common technique each time it is
used. If something is obvious there's no reason to comment it.
- Any internal "Sub" or "Function" routines
in the code should contain comments at the top that identify the
name and purpose of the routine as well as a description of any
calling parameters and return values
- In general during the initial
implementation no "change dates" and "changed by" information are
attached to code or code comments, although that is a very useful
technique for future maintenance programmers.
- Server-side VBScript code is always
declared as "OPTION EXPLICIT" at the top of the primary .asp file.
Note: It is not appropriate to put "OPTION EXPLICIT"
at the top of .inc ("include") files since they always follow the
primary .asp files in order.
- Since "OPTION EXPLICIT" is in effect, all
VBScript variables must be declared using "Dim" statements. This
is important even though all VBScript variables are data type
"variant" so they are never declared with a type descriptor.
Variable names are typically prefixed with letters that indicate
the primary "type" of data that they are designed to hold 每 rather
than their own "data type" (which is always variant). For example,
Dim nCount ' integer
Dim sTemp ' string
Dim oList ' list object
- There are a few exceptions to this rule.
The first is for data elements that are received in "QueryString"
or "Form" input. In general we prefer to prefix those variables
with the letters "rq" to indicate that they refer to information
received from the input stream. For example:
Dim rqStartDate ' start date
Dim rqContractorID ' contractor ID
Dim rqSupplierKey ' supplier key
- A second exception to the naming rule is
for simple loop counters. For loop counters we allow the use of
the single-letter variable names in the range of "i" through "n".
Agreed, it's not according to "formal" standards, but everyone
knows what you mean. For example:
Dim i, j
For i = LBound(arrX) To UBound(arrX)
For j = UBound(arrY) To LBound(arrY) Step -1
If arrY(j) < arrX(i) Then
ProcessFunction(i, j)
End If
Next
Next
- The last exception to this rule is for
common ADO and FSO object types. Rather than the generic "o" or
"obj" prefixes, we use common two- or three-letter prefixes that
identify the specific object type (such as rs, com, and cmd). In
fact sometimes we also allow the unqualified use of the
prefix itself. For example:
Dim rsWhatever ' recordset
Set rsWhatever = Server.CreateObject("ADODB.Recordset")
SomeSubroutine rsWhatever
Sub SomeSubroutine(rs)
If rs.State = 1 Then
Response.Write "Recordset is open."
Else
Response.Write "Recordset is not open."
End If
End Sub
- Adherence to hard and fast rules about
the specific prefixes to use (for instance, "s" vs. "str" for
string) is less important that the ability to easily identify the
use and expected data type of a particular variable. Nevertheless,
we tend to use the entries in the "short style" column as follows:
String |
s --
sWhatever |
str
-- strWhatever |
Fixed
Point Number |
n --
nWhatever i -- iWhatever |
lng
-- lngWhatever int -- intWhatever |
Floating Point Number |
d --
dWhatever s -- sWhatever |
dbl
-- dblWhatever sng -- sngWhatever |
Boolean |
b --
bWhatever |
bln
-- blnWhatever |
Arrays |
arr
-- arrWhatever( ) |
ary
-- aryWhatever( ) |
Variants |
v --
vWhatever |
var
-- varWhatever |
Objects |
o --
oWhatever |
obj
-- objWhatever |
Received Input |
rq
-- rqWhatever |
typically not distinguished
|
Common Objects (ADO & FSO)
|
con -- conWhatever rs --
rsWhatever cmd -- cmdWhatever fso -- fso ts
-- tsWhatever |
typically not distinguished
|
Loop Index |
single-letter i through n |
typically not allowed
| |
Active server pages ".asp" files will
contain declarations of currently-scoped variables in open code, as
well as in-stream VBScript code, HTML, and JavaScript.
In general if an active server page performs
repetitive or package-able tasks unique to its own function, those
repetitive or package-able tasks should be moved to "Sub" or
"Function" routines. Those "Sub" and "Function" routines should be
contained in a separate source code file, named with the same prefix
as the active server page ".asp" file, with file extension ".inc" or
suffix "_include.asp". That "#include" file should be incorporated
into the primary .asp file using a "#include" directive such as this
include for subroutines used by the "supplieredit.asp" page: <!--#INCLUDE FILE="supplieredit.inc"-->
or, alternatively <!--#INCLUDE FILE="supplieredit_include.asp"-->
Security Issue!
While "include" files are pre-processed by the IIS Active Server
Page interpreter when downloaded as part of ".asp" files, it is
possible under certain conditions for a user to directly download
the text of ".inc" files and view the entire file in plain text. For
that reason it is extremely important that no sensitive information
每 such as passwords 每 is included in "include" files with a ".inc"
suffix, or in comments inside these "include" files. This is not a
problem for files with ".asp" or ".asa" suffixes. For that reason it
is often preferred to use the suffix ".asp" for include files.
Repetitive or package-able tasks common to
multiple functions, or used by several pages, should be moved to
common "include" files.
Configuration parameters such as connect
strings, passwords, and other elements that can vary from
implementation to implementation should always be defined as
constants that are contained and isolated inside their own
configuration file -- which should be inside its own directory
wherever possible. This allows system operators and IT operations
personnel to independently manipulate these parameters without
modifying any executable code.
Clarity and maintainability of an
application is enhanced by standard and straightforward source code.
Wherever practical complex multi-language techniques should be
avoided in favor of packaged routines with an approachable calling
structure. For example, a standard call to begin a table might use
following HTML syntax: <TABLE BGCOLOR="<%=COLTABLEBORDER%>" BORDER=0
CELLSPACING=0 CELLPADDING=0 BORDERWIDTH=0>
<TR><TD><TABLE BORDER=0
CELLSPACING="<%=COLTABLEBORDERWIDTH%>"
CELLPADDING=1 BORDERWIDTH=0>
Rather than repeating that complex syntax
every time you want to begin a table it would be better to package
the syntax as a subroutine that can be more easily called from
VBScript: G_TableBegin
The simpler calling structure lets you to
include the complex syntax more easily, allows you to easily modify
the structure of all tables by simply changing the included common
routine, and makes the code self-documenting to a limited
extent.
Except where explicitly indicated otherwise,
each active server page module should contain the following major
sections: <%@ LANGUAGE="VBSCRIPT" %>
<% OPTION EXPLICIT %>
<%
'-------------------------------
' module: [whatever].asp
' block-style comments should explain
' what each page does and
' how it does what it does
'-------------------------------
%>
<!--#INCLUDE FILE="Adovbs.inc"-->
<!--#INCLUDE FILE="commonfuncs.inc"-->
<!--#INCLUDE FILE="[whatever].inc"-->
<%
Response.Buffer = True
Response.ExpiresAbsolute = 0
G_CheckSecurity
'-------------------------------
' declare local variables
'-------------------------------
[declare variables
here]
[process input
here] %>
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" content="text/html"
charset="iso-8859-1">
<TITLE>Page Title</TITLE>
</HEAD>
<%
<SCRIPT LANGUAGE="JavaScript">
[insert local
JavaScripts here] <%
'-------------------------------
' each client-side JavaScript should be
' commented with server-side VBScript comments
' that do not get transmitted to clients
'-------------------------------
%>
</SCRIPT>
<%
'-------------------------------
' proceed with page body
'-------------------------------
G_BodyStartMenu "document.f1.FriendlyName"
%>
<FORM NAME=f1 METHOD=POST>
<%
'-------------------------------
' form fields
'-------------------------------
%>
[insert form fields
here] <%
'-------------------------------
' custom display logic
'-------------------------------
%>
[insert display
logic here] '-------------------------------
' end of module
'-------------------------------
%>
</BODY>
</HTML>
<%
Response.End
[insert instream
server-side scripts here] %>
The sample also demonstrates the use of
several of the other elements that were described earlier in this
document, such as the use of include files and the liberal use of
comments throughout the code.
There are a few other techniques that assist
in the modular handling of pages:
- Retrieve user input into local variables
and manipulate it within those variables. In addition to improving
performance, this will allow you to assign default values to
undefined or unselected elements, and will allow you to use the
same input section to handle input forms for new records, input
forms for update requests, and input forms redisplayed due to
validation errors.
<input type="text" name="foo"
value="<%=rqFoo%>">
- Perform input validation on both the
client and the server. Client-side validation assists the user,
improving performance by eliminating the need for extra round
trips back and forth to the server just to handle simple errors.
This does not eliminate the need for server-side validation,
however. Server-side validation is necessary to protect the server
-- both from clients who have disabled client-side scripting, and
from malicious users who will try to hijack your scripts to do
things you don't intend.
The pseudo-conversational mode of web-based
interaction makes it difficult to maintain state information or to
pass that information between pages. One method to maintain state is
with the use of "Session" variables. These variables persist for the
lifetime of the user*s application session. Sessions persist from
the time the user requests their first page in the application*s
directory until either
- 20 minutes elapse with no transactions
requesting a page within the application, or
- The session is explicitly terminated with
a "Session.Abandon" command.
You should consider carefully before
implementing sessions in your pages. There are several reasons that
you might not want to use them:
- Sessions only work if the visitor has
enabled cookies in their browser, and
- Sessions do not let your applications
scale to multi-server "farm" implementations without the use of
other mechanisms for sharing state between the servers.
In general even if you do allow the use of
Session variables it is a good practice to avoid storing any OLE
objects within sessions.
Session start and end are marked with events
in the application's "global.asa" file. Bear in mind that this does
not always work to your advantage. For example, in shared
environments a single "global.asa" may need to be used by multiple
web applications. In that type of environment you may want to find
techniques other than global.asa to persist data.
Session variables are variant data elements
named with a string. For example, to keep a counter within the
session, you might create a session variable in global.asa: Session("counter") = 0
You could then increment the counter at the
beginning of each page: Session("counter") = Session("counter") + 1
There are two important issues associated
with session variables:
- You should be able to gracefully recover
if they are misplaced 每 for example if the user leaves their
terminal unattended for more than twenty minutes, then comes back
and clicks an "Apply" button. All of the session variables will
have been deleted when the session timed out, so you will lose
many aspects of application state. Our recommended technique is
to put a "save state" routine into the Session_OnEnd routine that
writes out all persistent session variables to the customer*s
USER_ACCOUNT record in the database. Then when the user is
prompted to log back in, the persistent session variables can be
easily restored. (Note that there are some
interesting aspects to handling events inside global.asa.
For one thing, the "Session_OnEnd event does not have access to
the client computer, so no user information or "cookies" can be
written at that time.)
- You should keep track of the names of all
session variables in a single place so that you don*t run into
name conflicts. Our
recommended technique is to list all known session variables and
their uses in comments located in the "global.asa" file.
It is important to handle errors in code
judiciously. When unhandled errors occur, an error message is
written to the output stream. When these errors occur in places that
write data to the screen, they appear in the middle of output like
this:
Microsoft VBScript runtime error '800a000d' Type
mismatch: 'cLng' /projname/requisitionlist.asp, line 110
This can cause confusion to users, terminate
execution of further portions of the page, and result in incomplete
or unrecoverable error conditions. These errors can also occur in
places that do not write data to the screen (for example, if a
VBScript error occurs while writing a section of JavaScript code);
in that case there may be no external indication of a problem at
all. The user may think the application has "hung", or they may not
even be aware of a problem.
Unlike standard VBA, VBScript does not allow
"On Error GoTo [label]" commands, so you cannot implement a common
error detection and correction routine. Use of a blanket "On Error
Resume Next", however, could mask real error conditions in your
application and lead to equally undesired results. For these reasons
one of the best techniques for error handling is to isolate critical
error-prone functions inside callable subroutines that do not
implement internal error handling, then call these routines from a
top-level routine with a testable "On Error Resume Next" in place.
Routines to read and display a complex database record, example,
could be called from the top-level .asp code: <%
On Error Resume Next
ReadRecord rsRecord, nKey
If Err.Number <> 0 Then
G_ShowError G_CollectErrors
Else
DisplayRecord rsRecord
If Err.Number <> 0 Then
G_ShowError G_CollectErrors
End If
End If
On Error Goto 0
%>
As soon as an error occurs inside any of the
callable routines, control "bubbles up" to the first level on which
a "Resume Next" is active. This simplifies testing for errors and
responding to them appropriately.
Closely associated with error handling is
the handling of synchronized database updates. If an error occurs in
one part of a multi-part logical chain of updates it is important
that these updates be implemented as a unit. For example, the
creation of a contractor record might consist of updates to a
"CONTRACTORS" table, a "PEOPLE" table, and multiple "MAPPING" and
"VALUE" tables. If for some reason part of the update fails all
portions of the update should be backed out. This is implemented by
means of database transactions.
Transaction processing logic makes use of
the error-handling techniques described above. A transaction is
initiated at an upper level of the hierarchy, then 每 with a "Resume
Next" statement active 每 a subroutine is invoked to perform the
chained updates. If any error occurs at a lower level, control
passes back to the top level, where the error condition is caught
and the transaction is rolled back. If no error has occurred by the
time control returns to the top level, the transaction can be
committed. Here is a fragment of how this might typically be
implemented inside a subroutine named "WriteRecord": ' ----------------------------------
' create required recordset objects
' to be used by subroutines
' ----------------------------------
Set rsS = Server.CreateObject("ADODB.Recordset")
Set rsM = Server.CreateObject("ADODB.Recordset")
Set rsB = Server.CreateObject("ADODB.Recordset")
Set rsP = Server.CreateObject("ADODB.Recordset")
Set rsR = Server.CreateObject("ADODB.Recordset")
On Error Resume Next
' ----------------------------------
' initiate transaction
' ----------------------------------
nLevel = 0
nLevel = Session("db").BeginTrans
If nLevel <> 1 Then
' ----------------------------------
' was a failed transaction in process?
' ----------------------------------
Session("db").RollbackTrans
nLevel = Session("db").BeginTrans
End If
If nLevel = 1 Then
' ----------------------------------
' call subroutine to process update
' ----------------------------------
On Error Resume Next
WriteSub rsS, rsM, rsB, rsP, rsR, id
If Err.Number <> 0 Then
' ----------------------------------
' an error occurred in subroutine
' ----------------------------------
WriteRecord = "Unable to write record. " & _
G_CollectErrors
' ----------------------------------
' cancel all updates
' ----------------------------------
rsS.CancelUpdate
rsM.CancelUpdate
rsB.CancelUpdate
rsP.CancelUpdate
rsR.CancelUpdate
' ----------------------------------
' close all tables
' ----------------------------------
rsS.Close
rsM.Close
rsB.Close
rsP.Close
rsR.Close
' ----------------------------------
' rollback the transaction
' ----------------------------------
Session("db").RollbackTrans
Else
' ----------------------------------
' success! okay to
' commit the transaction
' ----------------------------------
Session("db").CommitTrans
End If
Else
' ----------------------------------
' we get here if there was already a
' failed transaction pending and we
' could not back it out
' ----------------------------------
WriteRecord = "Synchronization error. " & G_CollectErrors
End If
' ----------------------------------
' clear object references
' ----------------------------------
Set rsS = Nothing
Set rsM = Nothing
Set rsB = Nothing
Set rsP = Nothing
Set rsR = Nothing
The essential cleanup functions need to be
isolated and called at this level, and a descriptive string is
passed back upstream to the routine*s initial caller. Note that it
is important that lower-level routines which encounter processing
problems that do not themselves generate system-level errors must
"Raise" their own error conditions in order to take advantage of
this error-handling logic.
|