Object Oriented ASP: Using Classes in Classic ASP

by Richard Quinn


Do you know how much of a nightmare some ASP pages can
be?
Do you know how disastrous it is to intermingle HTML with program code?
Yes, you do, I know you hate it too! Been there, seen that, you’ve probably
got some cool tricks to reduce the clutter in your own work. Perhaps you
use functions extensively, putting away code from out of your HTML and into
an include file? Well here’s an article showing you how you can take this
principle (called information hiding) to a new dimension!

The bookstore example.

Lets take a hypothetical web site, its a library’s site
detailing information about books! Books are a good example, really, because
everyone can see how online bookstores work from a customer perspective.

Books have certain attributes, here I’ll list some:

  1. ISBN Number
  2. Author(s)
  3. Title
  4. Subtitle
  5. Number of Pages
  6. Binding Type
  7. Publisher’s Name
  8. Published Year

Of course this list is not exhaustive, try
here
for an exhaustive list of attributes you can collect about books!
For our purposes it’s enough. If you don’t want to store your book data as
XML, a relational database is a good idea.

Before looking at how you want to store this information,
it’s worthwhile modeling the objects first. You can see that I’ve identified
two separate objects, books and authors. Publishers or binding types are certainly
other candidates, but they would complicate the example a little too much.

Book Class Diagram
Figure 1: Book Class Diagram

As you can see, both the book and the author classes have
a common set of operations: LoadFromID, Store and Delete. Real OO purists
would start decomposing the classes here and make them share, or inherit,
these operations from another class. But VBScript can’t do this, so I’ll leave
this explanation for another day 🙂 Another necessary feature is the attribute
ID. Imagine you have thousands of books and tens of thousands of authors.
A convenient way to identify individual entities is by using some form of
self-generating ID, which I’ve modeled here.

Here is a diagram which shows some of the data entities
you may consider when modeling your database:

Book Entity Relation Diagram
Figure 2: Book Entity Relation Diagram

The main point to notice is that the data is partially
normalized, author data is stored separately from book data. An association
table is used to create relationships between books and authors, whereby
an author may have written several books, and a book may have been written
by several authors. This principle can also be applied to the binding type
column or to the publishers name column, in practice you will want to normalize
this data too.

You may or may not like the way I name the tables and
the columns, but that’s your problem 🙂 This scheme has been very useful
for me in the past, and I’m sticking with it. Tables are named in the singular,
since they represent an entity from the real world. Columns have a prefix
denoting the data type of the field: lng: Long Integers, str Strings, dtm:
DateTime. The column prefixes are only intended to be useful to humans who
can immediately see what data type goes in the field and can make educated
guesses about how to use or format the data there. The suffix: ID is reserved
for fields which form a primary or foreign key. For a more detailed discussion
of primary and foreign keys, indexes and IDENTITY fields, you should see
the documentation which comes with your make of database.

An interesting side note is that these diagrams are as
equally useful to librarians as they are to programmers or to database administrators.
True, each group may get something additional from the diagram, but the
terms used and the relationships shown are based on real world scenarios
and need little interpretation. Avoid cryptic names and unclear associations,
they are more trouble in the long run.

The application.

OK, now we have fairly detailed information about the
application we want to build, we’ve even shown it to the client and had
them sign that they agree that it says what they want them to say! So lets
get down to building the application, making the Database, the HTML and
ASP pages and testing it all! Right? Wrong!

The first thing we should be doing is building the classes
which represent the two objects we’ve identified. This well help is to clarify
what we’re doing, any changes we come up when building the objects can be
easily accommodated, since neither the database nor the ASP pages yet exist,
and therefore do not need to be re engineered.
I’m assuming you’ve heard of VBScript classes, and know that they are formed
within this code construct:

Class cClassName
'some code....
End Class

OK, here comes an example attribute of the book class, its ISBN number:

'Private, class member variable
Private m_ISBN
'Read the current ISBN value
Public Property Get ISBN()
      ISBN = m_ISBN
End Property

'store a new ISBN value
Public Property Let ISBN(p_Data)
      m_ISBN = p_Data
End Property

This was the ISBN attribute. The actual value of the ISBN is stored in a
private variable, called m_ISBN. Here I also use prefixes to identify the
variable as a member of the class. Doing this allows us to check that values
written to or read from the class are never badly formatted or just plain
wrong. They are doorways to the hidden insides of the class, we can check
what’s coming in and going out. Here is
the partial book class containing all of its attributes
.

Now I can show you an example of how the class could be used. This is old
style, questionably useful ASP code mixed in with the HTML:

<tr><TD><%=MyRecordest.Fields(1).Value%></td></tr>

Of course, you needed reams and reams of supporting code to build the connection,
get the data out of the DB and loop through the records, outputting the values
directly into the HTML. Well here is how we create an object from our book
class:

<% Set MyBook = new cBook %>....

<tr><TD><%=MyBook.Title%></td></tr>

You can see an improvement in the code already, it is self describing. You
can see immediately that the book’s title is being placed in within the table
cell, no more remembering what is in which field of your recordset. Also,
of course, if the book doesn’t yet have a title, you might need to do something
like this in old-style ASP:

<tr><TD><%=
   strTitle = MyRecordest.Fields(1).Value

   if strTitle = "" then strTitle = "no title
yet"
   response.write strTitle
%></td></tr>

In an OO approach, you can put this behaviour inside the class, specifically,
in the property attribute accessor method:

Public Property Get Title()
    If m_Title = "" then
         Title = "not title
yet"
    Else

         Title = m_Title
    End If
End Property

This cleanly separates the business rules ("if there is no title display
a message saying so") from the presentation of your information. Your
improved ASP code:

<tr><td><%=MyBook.Title%></td></tr>

…will conform to the business requirements without messing up your presentation.
If the business rules change, and they often do, you can make the modification
in one location within your class, and not hunt through every HTML page for
every instance of where you have printed the title. Life is starting to get
much easier, but there is more!

You run into real difficulties when your DB code for reading, updating and
deleting the data is repeated all through your web site, whereever you needed
to display it. If you change the database you may need to modify lots of web
pages to reflect the changed database structure. Wouldn’t it be easier if
the DB code were stored directly in the object? You would only need to write
it once, store it in your class file and reuse it wherever you use those objects
in your web site? In the object diagram above, the place to store this code
is in the operations LoadFromID, Store and Delete. I’ll show you first how
this functionality can be easily used in your web pages, and then how it is
implemented in the class file.

Creating and loading a book object in ASP, then displaying the book’s information
within HTML:

<%
'Create an empty book object,
'Load the object's data based on an ID you get from
the request
,

'Display the books's information.
Set MyBook = new cBook
MyBook.LoadFromID(Request("bookid"))
%>....

<tr><TD>Title: <%=MyBook.Title%></td></tr>
<tr><TD>SubTitle: <%=MyBook.SubTitle%></td></tr>

<tr><TD>Publisher: <%=MyBook.PublishersName%></td></tr>
<tr><TD>Year: <%=MyBook.PublishedYear%></td></tr>

<tr><TD>Format: <%=MyBook.BindingType%></td></tr>
<tr><TD>ISBN: <%=MyBook.ISBN%></td></tr>

No database code, no worries, everything is hidden away in the implementation
of the LoadFromID operation.
I have created a new database for this example using Microsoft Access, "vboo_pubs",
based on the example pubs database in SQL Server. It has 3 tables which interest
us, book, author and authortobook, which happily
directly correspond to the three tables in the entity relationship diagram
above. This is how I would implement the LoadFromID operation, using the vboo_pubs
sample database:

Public Function LoadFromId(p_Id)

Dim rs, conn, strSQL
Set Rs = Server.CreateObject("ADODB.Recordset")
Set conn = Server.CreateObject("ADODB.Connection")

conn.open "vboo_pubs"

strSQL = " SELECT T.title_id, T.title, T.type, "
strSQL = strSQL & " T.pub_id, P.pub_name, T.pubdate, T.notes "
strSQL = strSQL & " FROM titles T INNER JOIN publishers P ON T.pub_id
= P.pub_id "

strSQL = strSQL & " WHERE (T.title_id = '" & p_Id &
"') "

rs.open strSql, conn, 1,3

if rs.recordcount =1 then
Me.ID = rs.fields("title_id").Value
Me.Title = rs.fields("title").Value
Me.PublishersName = rs.fields("pub_name").Value

Me.PublishedYear = rs.fields("pubdate").Value
Me.BindingType = "" ' No data available!
Me.SubTitle = rs.fields("notes").Value
Me.ISBN = rs.fields("pub_ID").Value

LoadFromId = Me.ID
mvarSaveOnTerminate = true
else
err.raise 1, "Item with ID " & p_ID & " was not found"

End if

rs.close
Set rs = nothing
conn.close
Set conn = Nothing

End Function

To get this example working you need to have an ODBC data source set up named
"pubs", which the object uses to connect to the database.
You may have noticed that there is no error handling in this code, this is
purposely bare – bones so that you can deal with errors your self. The code
actually raises an error if it can’t find the record being searched for, this
is for your web pages to deal with appropriately. Sometimes not being able
to fin a record is unimportant, other times it is critical. The class does
not know in which circumstance it is being used, and makes no assumptions
about how to deal with errors.

The LoadFromID operation is essential if you want to get the data from your
database into the object, and you have an ID. But what if you don’t have an
ID, but lets say the ISBN? Well, you would create a LoadFromISBN function.
Since the two functions are different only in the SQL statement they produce,
it would make sense to start refactoring here, and put the database access
code somewhere separate from the SQL generation code. Here is the refactored
LoadFromID along with the LoadFromISBN operation.

'Loads this object's values by loading a record based
on the given ID
Public Function LoadFromId(p_Id)

dim strSQL, MyID
MyID = clng(p_Id)
strSQL = "SELECT lngBookID, strTitle, strSubTitle, strISBN, strBindingType,
"
strSQL = strSQL & " strPublishersName, strPublishedYear, strPageCount
FROM Book "
strSQL = strSQL & " WHERE (lngBookID = " & MyID & ")
"

LoadFromId = LoadData (strSQL)
End Function

'Loads this object's values by loading a record based on the
given ISBN
Public Function LoadFromISBN(p_Id)
dim strSQL
strSQL = "SELECT lngBookID, strTitle, strSubTitle, strISBN, strBindingType,
"

strSQL = strSQL & " strPublishersName, strPublishedYear, strPageCount
FROM Book "
strSQL = strSQL & " WHERE (strISBN = '" & SingleQuotes(p_Id)
& "') "

LoadFromISBN = LoadData (strSQL)
End Function

Load Functions

The LoadData function is quite complicated. Since this is not a tutorial
about database connections, I have left it to be shown in the complete class
listing. Both of these functions return the ID of the book which has been
loaded.

The Store and Delete operations do exactly what you expect. Suppose you want
to create a new record in the database, how would you do this in an OO way
in ASP? This is very easy to do, the hard part is coding the SQL statement,
so I’ll do that for you. The interesting thing to note here, is that the Store
operation is called not just when creating a new database record, but also
when saving changes which may have been made to an existing record. You can
now do inserts and updates from a single ASP page entitled "Create /
Update Book Information":

  1. If the page is called with a book ID, load the book object, otherwise
    create an empty book object
  2. Show the editing form, and fill it with the values from your book object.
    The fields are either empty or they show the existing data
  3. When the user submits the form, again see if you have received an existing
    ID. If yes, load that book object, otherwise create an empty book object
  4. Assign the form values to the object’s properties
  5. Call store on your book object. It runs an update or an insert operation
    automatically

Here is the Store operation:

Public Function Store()
Dim strSQL
'If the book has an existing (Autogenerated!) ID, then run an insert
if Me.ID < 1 then

Dim ThisRecordID, arr1, arr2
arr1 = Array("strTitle", "strSubTitle", "strISBN",
"strBindingType", "strPublishersName", "strPublishedYear",
"strPageCount")

arr2 = Array(Me.Title, Me.SubTitle, Me.ISBN, Me.BindingType, Me.PublishersName,
Me.PublishedYear, Me.PageCount)
Me.ID = InsertRecord("Book", "lngBookID", arr1, arr2)

'Otherwise run an update
else
strSQL = strSQL & " UPDATE Book SET "

strSQL = strSQL & " strTitle = '" & Me.Title & "',"
strSQL = strSQL & " strSubTitle = '" & Me.SubTitle &
"',"

strSQL = strSQL & " strISBN = '" & Me.ISBN & "',"
strSQL = strSQL & " strBindingType = '" & Me.BindingType
& "',"

strSQL = strSQL & " strPublishersName = '" & Me.PublishersName
& "',"
strSQL = strSQL & " strPublishedYear = '" & Me.PublishedYear
& "',"

strSQL = strSQL & " strPageCount = '" & Me.PageCount &
"'"
strSQL = strSQL & " where lngBookID = " & Me.ID

RunSQL strSQL
End if
Store = Me.ID
End Function

And here is the delete:

Public Function Delete

strSQL = "DELETE * FROM Book WHERE lngBookID = " & Me.ID
RunSQL strSQL
End Function

So we’ve done the hard work, and made the class. Hard work? Yes and no. It
will initially seem like a lot of work, defining the member variables, writing
attributes to access them, then writing the DB code and testing it all (hopefully
using a unit testing framework like ASPUnit).
And all this without any HTML, no web pages, nothing. But that’s the beauty!
Now you can give a HTML Dude a list of the Objects attributes, and he can
make the HTML pages, whilst you go on to make and test the next class file!
And of course, because each class file looks almost exactly the same as the
previous one, you can automate the production of them! My favourite is to
define an XML structure containing all of the attributes of the class, and
then write an XSL to transform the XML into a valid VBScript class. Making
this kind of system shouldn’t take more than a day, writing the XML files
for your application’s classes no longer than a further day, leaving you weeks
and weeks to make funky HTML 🙂

So here is some of that funky HTML. Earlier, I showed a simple page used
to display a book object using HTML. Here is the page which will allow us
to edit an existing book object or to create a new one:

<!-- #INCLUDE virtual="/zzTest/cBook.asp" -->
<!-- #INCLUDE virtual="/zzTest/cAuthor.asp" -->
<!-- #INCLUDE virtual="/zzTest/cLibrary.asp" -->

<!-- #INCLUDE virtual="/zzTest/i_helper.asp" -->
<%

dim MyBook
Set MyBook = new cBook

if request("bookid")<>"" then

mybook.loadfromid(request("bookid"))
end if

if request("cmd")<>"" then
Mybook.ISBN = Request.Form("isbn")

Mybook.Title = Request.Form("Title")
Mybook.SubTitle = Request.Form("SubTitle")
Mybook.ISBN = Request.Form("isbn")
Mybook.BindingType = Request.Form("BindingType")

Mybook.PublishersName = Request.Form("PublishersName")
Mybook.PublishedYear = Request.Form("PublishedYear")
Mybook.PageCount = Request.Form("PageCount")
mybook.store

end if

%>

<HTML>
<HEAD>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#FF0000"
VLINK="#800000" ALINK="#FF00FF" BACKGROUND="?">

<form name="form1" method="post" action="edit_book.asp">
<table border="1" cellspacing="0" cellpadding="0">

<tr>
<td width="134">Book ID</td>
<td width="12"><input type="hidden" name="cmd"
value="edit"></td>

<td width="598"><input type="hidden" name="bookid"
value="<%=Mybook.ID%>"><%=Mybook.ID%></td>
</tr>

<tr>
<td>ISBN</td>
<td>&nbsp;</td>
<td><input size="50" name="isbn" value="<%=htmlencode(Mybook.ISBN)%>"></td>

</tr>
<tr>
<td>Title</td>
<td>&nbsp;</td>

<td><input size="150" name="Title" value="<%=htmlencode(Mybook.Title)%>"></td>
</tr>
<tr>

<td>SubTitle</td>
<td>&nbsp;</td>
<td><input size="150" name="SubTitle" value="<%=htmlencode(Mybook.SubTitle)%>"></td>

</tr>
<tr>
<td>Binding Type</td>
<td>&nbsp;</td>

<td><input size="50" name="BindingType" value="<%=htmlencode(Mybook.BindingType)%>"></td>
</tr>
<tr>

<td>PublishersName</td>
<td>&nbsp;</td>
<td><input size="50" name="PublishersName" value="<%=htmlencode(Mybook.PublishersName)%>"></td>

</tr>
<tr>
<td>Published Year</td>
<td>&nbsp;</td>

<td><input size="50" name="PublishedYear" value="<%=(Mybook.PublishedYear)%>"></td>
</tr>
<tr>

<td>´Number of pages</td>
<td>&nbsp;</td>
<td><input size="50" name="PageCount" value="<%=htmlencode(Mybook.PageCount)%>"></td>

</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp; </td>
<td><input type="reset" name="Submit2" value="Reset">
<input type="submit" name="Submit" value="Submit"></td>

</tr>
</table>
</form>

<a href="./default.asp">List</a>
</BODY>

</HTML>

You can download the whole project
here.

Navigating from Authors to Books.

As we saw above, instantiating a book object and loading its attributes based
on an ID number is very simple. But how do we navigate from our book object
to the authors who wrote the book? Well, if we look at the class diagram above,
the "0..*" text on the line between books and authors says that
a book refers to zero or more authors. Also, an author has a relationship
with zero or more books. Strictly speaking, we don’t really want to be managing
the data of authors who haven’t written anything in our library, but the situation
occurs often in real life, when for example the author’s first book has not
yet been published.

The book object has a property, called "Authors". Using object
notation we can reference the first author in the collection of the books
authors like this: Mybook.Authors.Items(1).LastName

This is how the property is built:

Public Property Get Authors()
if not isobject(m_Authors) then
set Library = New cLibrary

Call Library.GetAuthorsByBookID(Me.ID)
set m_Authors = Library.Authors
Set Library = nothing
end if
Set Authors = m_Authors
End Property

This uses the "Lazy Load" design pattern. The book object does
not contain any authors until you actually request author information. The
property checks to see if the m_Authors collection has been initialised, if
not it initialises a new Authors object. The Authors collection is provided
by the library. The library does not have a persistent representation in the
database, it has no attributes which we want to display just yet. It does
have a large number of operations, representing the functions that a library
performs.

In our case, the operation "Library.GetAuthorsByBookID(Me.ID)"
asks the library to return all of the authors associated with a particular
book as a collection. The library uses some simple SQL to retrieve the requested
records from the database. For each record returned, the library creates an
author object and initialises the author’s attributes. The author object is
then added to the collection. The library informs the book about the number
of authors created, and stores the collection in its "Authors" property.
The book requests the Authors from the library object then stores the author
collection internally for speedy future access: "Set m_Authors=Library.Authors()".

Navigating from Authors works in exactly the same way, the Author object
has a property called "Books" which represents all the books written
by the author. Requesting "Author.Books" returns a collection object,
internally the library services the request by retrieving the data from the
database, creating book objects and returning them as a collection.

It may be becoming clearer now, that writing ASP classes to model the real
world is a lot of effort up-front. This effort can be justified:

  • The classes produced are independent of HTML.
  • The classes can be thoroughly tested and verified before being used by
    your HTML people.
  • The classes can be coded parallel to screen design or HTML prototyping
    activities, allowing previously sequential project activities to be performed
    in parallel.
  • The SQL is hidden away where the DBA / Developer can find it, the designer
    never sees it.
  • The code produced is reusable. In the real world this small example may
    take up to a whole day to code and test, but can then be used by several
    people to actually build a real world application.
  • Your users get a higher quality application (faster, more reliable, more
    easily modified, errors are localized more quickly, easily maintained, fewer
    errors).

Discussion

This tutorial has given you an insight into how ASP classes are built. Attributes
and operations have been discussed. Navigation between classes has been explored
as one of the fundamental abilities of Object-Based systems. The classes presented
here use an "Active Record" persistence pattern, each object represents
one record in an associated table in the database. The Library object was
introduced as a non persistent class which services the request of other objects.

This tutorial presents a domain model. That is, real world objects and their
associations are called the problem domain, our database, classes and web
pages are part of the solution domain. Our objects closely resembled real
world objects. There are other ways of architecting applications than by using
a domain model, but this way really does leverage the power of object based
systems.

The book and Author classes are responsible for instantiating their own data,
but as you may have seen, the library class also instantiates books and authors
en masse when creating collections. This is a conceptually "dirty"
way of doing things. It would be much nicer if the code for assigning field
values to object attributes were only in one place, the risk of bugs would
be much reduced. Unfortunately, the need for performance means that an entire
table of records must be initialised in a single step.

On the performance side, things could be improved dramatically. Each book
is instantiated separately from each author. A minimum of two database calls
are necessary to first retrieve the book data and then retrieve the authors
data. Folding this into a single call with responsibility for instantiating
both book and author data would bring noticeable performance and scalability
advantages.

There is a questionable resolution of the problem originally presented: separation
of code and HTML. In order to output the object’s attributes, a fair amount
of scripting is still necessary. True, we have moved away from scripting the
database and the HTML to scripting objects and HTML, but a cleaner separation
could be achieved if each object had a "Render(OutputFormat)" operation
which could make a visual representation for display in the desired output
format, for example HTML.

For a fuller understanding of how the navigation between books and authors
is implemented, a good look at the accompanying code is essential. There are
some tricks and goodies in there which will surprise and delight even the
most advanced of ASP authors 🙂

You can download the whole project below.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read