Editor's Note

This section of the MSDN Library contains selected chapters of books of interest to developers.

We try to publish books in their entirety whenever possible. (You will find titles of complete books listed in the Books node in the Library table of contents.) However, because of publisher or author restrictions, we sometimes are able to publish only selected portions of books. In making our decision to publish a partial book, we follow two criteria. First, MSDN developers read the book critically and determine that it merits inclusion in the MSDN Library. Second, MSDN developers choose substantial portions of the book and ensure that our selection has value in and of itself, as stand-alone content.


Internet Programming with Visual Basic 5

PUBLISHED BY
M&T Books
A Division of MIS:Press, Inc.
A Subsidiary of Henry Holt and Company, Inc.
115 West 18th Street
New York, New York 10011
http://www.mispress.com

Copyright © 1997 by Steven Holzner

Printed in the United States of America

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage and retrieval system, without prior written permission from the Publisher. Contact the Publisher for information on foreign rights.

Limits of Liability and Disclaimer of Warranty

The Author and Publisher of this book have used their best efforts in preparing the book and programs contained in it. These efforts include development, research, and testing of the theories and programs to determine their effectiveness.

The Author and Publisher make no warranty of any kind, express or implied, with regard to these programs or the documentation contained in this book. The Author and Publisher shall not be liable in any event for incidental or consequential damages in connection with, or arising out of, the furnishing, performance, or use of these programs.

All products, names and services are trademarks or registered trademarks of their respective companies.

First Edition—1997

ISBN 1-55851-558-5

Library of Congress Cataloging-in-Publication Data

MIS:Press and M&T Books are available at special discounts for bulk purchases for sales promotions, premiums, and fundraising. Special editions or book excerpts can also be created to specification.

For details contact:
Special Sales Director
MIS:Press and M&T Books
Subsidiaries of the Henry Holt and Company, Inc.
115 West 18th Street
New York, New York 10011

10 9 8 7 6 5 4 3 2 1

Associate Publisher: Paul Farell

Managing Editor: Shari Chappell Production Editor: Anthony Washington

Editor: Michael Sprague Copy Edit Manager: Karen Tongish

Copy Editor: Betsy Hardinger


VB on the Internet: HTTP and FTP

In this chapter, we'll examine the powerful capabilities of Visual Basic to handle two important Internet protocols: File Transfer Protocol (FTP) and Hypertext Transfer Protocol (HTTP). FTP is usually used to transfer large files; most Internet servers have an FTP area that you can examine and upload files to or download files from. The HTTP protocol is the basis of the World Wide Web and is usually used to fetch Web pages; you get the direct HTML of a Web page using this protocol. Like the FTP protocol, HTTP can also be used to get binary files (such as images or .zip files). Visual Basic's built-in support for these methods makes writing programs around them almost easy. In this chapter, we'll write a program that reads the directory of an FTP site and allows us to get both binary and text data. We'll also create an HTTP program that fetches a Web page and displays its HTML. (This is unlike the WebBrowser control, which displays the Web page without the underlying HTML.)

You can also get creative with these methods. For example, you might use FTP methods in your programs to automatically download documentation for a user who clicks items in the Help menu. Or you could use HTTP methods to write a program that gets all the graphics images from a Web page without having to click each one.

Visual Basic uses the Internet Transfer control to handle these two protocols, so let's take a look at this exceptionally powerful control first.


The Internet Transfer Control

The Visual Basic Internet Transfer control implements both FTP and HTTP protocols for us. You might wonder why Microsoft didn't create two controls: an FTP control and an HTTP control. It turns out that there is no need for that: the Internet Transfer control figures out which protocol to use based on the URL we pass to it (such as ftp://ftp.microsoft.com or http://www.microsoft.com).

This is how we'll use the Internet Transfer control: first, we'll add it to a program using the Project menu's Components item by clicking the entry marked Microsoft Internet Transfer Control 5.0. After the control appears in the toolbox, we'll double-click it to create a new Internet Transfer control in our program (the default name of this control is Inet1). Then all we have to do is to use the OpenURL() or Execute() method to access the Internet. The OpenURL() method gets the data we are trying to fetch, and the Execute() method executes FTP or HTTP commands such as GET or PUT.

There is one more thing you should keep in mind before we start our FTP example: OpenURL() is a synchronous method, and Execute() is asynchronous. This means that the OpenURL() method waits until the data you are requesting is downloaded; the Execute() method returns at once, and the code that follows it in your program is executed while Execute() is working. This arrangement can save time in very long file transfers. We'll begin by creating an FTP program.


Using FTP from Visual Basic

Our first Internet Transfer control example will examine and download files from the Microsoft ftp site, ftp.microsoft.com. This site contains a great deal of free software, readily available for downloading. All we have to do is to use the ftp protocol correctly. Let's name our new program FTPer, and let's have it display the files we download. This means that we'll download text files, so we set up FTPer to display the names of the available text files:

Users select from these listed text files; when they click, say, file2.txt, we download that file and display its contents:

Let's put this to work now. Use the Visual Basic Application Wizard to create a new SDI project (click the SDI option and not the MDI option) named FTPer. We'll need an Internet Transfer control, so select the Components item in Visual Basic's Project menu. Find the entry marked Microsoft Internet Transfer Control 5.0 and click it. We'll display the text of our text files in a rich text box, so add that control by clicking the Microsoft Rich Textbox Control 5.0 entry; then click OK.

Now open the main form of the project, frmMain, and add an Internet Transfer control (Inet1), a rich text box (RichTextBox1), and a Listbox control (List1) for the names of the text files:

At this point, we're ready to begin coding the FTPer program.


Logging On to an FTP Server

The first step is to log on to the FTP server ftp.microsoft.com. In general, there are two types of FTP servers: public and private ones. For both types, you must provide a user name and password. In public FTP servers, such as ftp.microsoft.com, you typically use "anonymous" as your user name, and your email address as the password. The Internet Transfer control makes it easy, because it supplies this information automatically (getting your email address from the Windows registry).

To log on to a private FTP server, you must provide a password and user name, and you do that using the Internet Transfer control's UserName and Password properties. For example, if we wanted to connect to ftp.private.com, we would start by setting the Internet Transfer control's URL property to ftp.private.com. (You don't have to set the URL property if you use the OpenURL() method, because you pass the URL when calling OpenURL().)

Inet1.URL = "ftp://ftp.private.com"
           .
           .
           .

Next, set the UserName and Password properties:

Inet1.URL = "ftp://ftp.private.com"
  —>   Inet1.UserName = "George Washington"
  —>   Inet1.Password = "no_lies"
           .
           .
           .

Then you can the Execute method to execute FTP commands such as GET; in this case, we'll download a file named source.txt from the FTP server and save it as c:\target.txt:

Inet1.URL = "ftp://ftp.private.com"
        Inet1.UserName = "George Washington"
        Inet1.Password = "no_lies"
        Inet1.Execute , "GET source.txt C:\target.txt"
                .
                .
                .

In this way, you use the Execute method to execute FTP commands (just as you might in a command-line FTP program). Finally, we execute a CLOSE command to close the connection:

Inet1.URL = "ftp://ftp.private.com"
        Inet1.UserName = "George Washington"
        Inet1.Password = "no_lies"
        Inet1.Execute , "GET source.txt C:\target.txt"
  —>   Inet.Execute ,"CLOSE"

That's the way to execute FTP commands: with the Execute method. You may be familiar with these commands. They are the backbone of FTP handling, although, as we'll see in a minute, you don't have to use any of these commands to download if you use the OpenURL() method. Table 3.1 shows the supported FTP commands, such as CD, GET, DELETE, PUT, and MKDIR (MKDIR creates a new directory on the Internet host if you have the privilege to do so), and describes what they do. Using these commands, we can download files from and upload files to FTP sites. For example, to change directories and upload a file named source.txt, renaming it to target.txt, you would execute this code:

Inet1.URL = "ftp://ftp.private.com"
        Inet1.UserName = "George Washington"
        Inet1.Password = "no_lies"
        Inet1.Execute , "CD gifs\thegifs"
        Inet1.Execute , "PUT C:\source.txt target.txt"
        Inet.Execute ,"CLOSE"

Table 3.1 FTP commands of the Internet Transfer Control's Execute Method

Command | Does This
CD newdir | Changes directory (e.g., Execute , "CD gifs\thegifs").
CDUP | Changes to parent directory. Identical to "CD ..".
DELETE file | Deletes specified file (e.g., Execute , "DELETE nogood.dat").
DIR [newdir] | Searches current directory or directory specified. Use GetChunk() to get the data (e.g., Execute , "DIR /gifs").
GET source target | Gets file "source" and creates a local file "target" (e.g., Execute , "GET source.txt C:\target.txt").
LS | List. Same as DIR.
MKDIR newdir | Creates new directory newdir (e.g., Execute , "MKDIR /newdir").
PUT source target | Copies local file "source" to remote computer as "target" (e.g., Execute , "PUT C:\source.txt target.txt").
PWD | Print Working Directory. Use GetChunk() method to get the data (e.g., Execute , "PWD").
QUIT | Close the connection (e.g., Execute , "QUIT").
RECV source target | Same as GET.
RENAME oldname | Renames a file (e.g., Execute , "RENAME oldnewname name.txt newname.txt").
RMDIR dirname | Removes directory (e.g., Execute , "RMDIR dirname").
SEND source | Same as PUT.
SIZE filename | Gets the size of specified file (e.g., Execute "SIZE filename.txt").


Controlling Asynchronous Transfers

Execute() works asynchronously, and that means that we need some way of keeping track of what it is doing. In particular, we need to know when the operation is finished (such as when the file we want is downloaded). We do that in the Internet Transfer control's StateChanged event; double-clicking the Internet Transfer control opens this event:

Private Sub Inet1_StateChanged(ByVal State As Integer)
        End Sub

We can determine what is happening in this event by checking the variable passed to us: State. Table 3.2 lists the values that this variable can contain and describes what those values mean. Using this event handler, we can watch as the control connects to the Internet, downloads the data, and finishes.

Table 3.2 The Internet Transfer Control's StateChanged Values

Value | Meaning
icNone (= 0) | No state
icHostResolvingHost (= 1) | Looking up IP address of specified computer
icHostResolved (= 2) | Got the IP address of specified computer
icConnecting (= 3) | Connecting to computer
icConnected (= 4) | Connected to computer
icRequesting (= 5) | Sending request to host computer
icRequestSent (= 6) | Sent the request
icReceivingResponse (= 7) | Receiving a response from computer
icResponseReceived (= 8) | Received a response from computer
icDisconnecting (= 9) | Disconnecting from computer
icDisconnected (= 10) | Disconnected from computer
icError (= 11) | Error occurred
icResponseCompleted (= 12) | Request completed and data received

For example, if we want to determine when a particular document has been downloaded, we place code like this into the Inet1_StateChanged() event handler:

Private Sub Inet1_StateChanged(ByVal State As Integer)
   —>      If(State = icResponseCompleted) Then ReceivedFlag = True
        End Sub

In this way, we're able to determine when a document is fully downloaded when we use the Execute method. (There is no such question when you use the OpenURL method, because that method won't return until the file you have requested has been downloaded.)


Using GetChunk()

When we perform a directory FTP Execute operation such as DIR, LS, or PWD, the data is not returned to us immediately; instead, it is stored in a buffer and we must use the GetChunk() (as in "get a chunk of data") method to read it.

Let's say we performed a DIR operation with the Execute() method. In that case, we use GetChunk() to retrieve the directory data we received. We use GetChunk() like this: Inet1.GetChunk(size, type). Here, size is the number of bytes we want to get from the download buffer, and type can be one of two constants: icString (value = 0, the default) for string data, or icByteArray (value = 1) for binary data. Our goal might be to display the directory data we got in a text box named Text1. We would start in the Internet Transfer control's StateChanged event handler. We wait until the directory data has been downloaded by checking the State variable and making sure it equals icResponseReceived:

Private Sub Inet1_StateChanged(ByVal State As Integer)
  —>        If(State = icResponseReceived) Then  'This constant = 12
                    .
                    .
                    .
  —>           End If
        End Sub

If we got the directory data, we can begin by getting, say, 2K of data from the download buffer:

Private Sub Inet1_StateChanged(ByVal State As Integer)
            If(State = icResponseReceived) Then  'This constant = 12
        —>             Dim VariantData As Variant
        —>             VariantData = Inet1.GetChunk(2048, icString)
                             .
                             .
                             .
                End If
        End Sub

Note that GetChunk() returns a Variant: an untyped value that can be either a string or numeric data. In this case, we'll keep reading from the download buffer until we exhaust it, in which case GetChunk will return an empty string, "":

Private Sub Inet1_StateChanged(ByVal State As Integer)
                If(State = icResponseReceived) Then  'This constant = 12
                       Dim VariantData As Variant
     —>                Dim StringData As String
     —>                Dim DoneFlag As Boolean
     —>                DoneFlag = False
     —>                StringData = ""
                        VariantData = Inet1.GetChunk(2048, icString)
     —>              While Not DoneFlag
     —>                 StringData = Data + VariantData
     —>                 VariantData = Inet1.GetChunk(2048, icString)
     —>                 If Len(VariantData) = 0 Then DoneFlag = True
     —>                Wend
                         .
                         .
                         .
                End If
        End Sub

At this point, the directory data we received is in the string StringData, and we can display it in our text box Text1 this way:

Private Sub Inet1_StateChanged(ByVal State As Integer)
            If(State = icResponseReceived) Then  'This constant = 12
                        Dim VariantData As Variant
                        Dim StringData As String
                        Dim DoneFlag As Boolean
                        DoneFlag = False
                        StringData = ""
                        VariantData = Inet1.GetChunk(2048, icString)
                     While Not DoneFlag
                        StringData = Data + VariantData
                        VariantData = Inet1.GetChunk(2048, icString)
                        If Len(VariantData) = 0 Then DoneFlag = True
                        Wend
   —>                  Text1.Text = StringData
                End If
        End Sub


Downloading Text FTP Data

Now that we've reviewed FTP methods in Visual Basic, let's put them to work. Our goal in our example program, FTPer, is to download the directory of ftp.microsoft.com, indicate which text files are available, and let the user download the selected one:

This example is not very fancy, so the Internet Transfer control OpenURL() method will work best here. Using this method, you can fetch files with the HTTP and FTP protocols. That's all it does—if you want to use commands such as DIR or PUT, you must use the Execute method.

Now that we've added an Internet Transfer control to our FTPer project, we can put it to work. When the program starts, we'll have it automatically connect to ftp.microsoft.com and display a directory listing of the text files there (the files with the extension .txt). We will use the Form_Load() event handler, which is called when the main form is first displayed; currently it looks like this (where the program positions the form on the screen and sizes it):

Private Sub Form_Load()
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
End Sub

Next, we get the directory of the Microsoft server ftp.microsoft.com. We use the OpenURL() method, loading the directory information into a string named DirectoryText:

Private Sub Form_Load()
—> Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
—> DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
        .
        .
        .

Because we have not specified a file to download, we will get a listing of ftp.microsoft.com's directory. In general, the OpenURL() method has two arguments: the URL you want to get and a data type setting, which can hold the values icString (= 0, the default) to retrieve the data as a string, or icByteArray (= 1) to retrieve the data as a byte array. Although we skipped the second parameter, we could also have written that line this way:

DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com", icString)

When the program executes this line, it connects to the Internet (assuming that the user has a connection set up for the Internet) if the computer is not already connected and logs in to ftp.microsoft.com. Then it gets the directory of ftp.microsoft.com and places it into the string DirectoryString. With the addition of the Inet1 control to our program and the use of the OpenURL() method, we've connected to the Internet using the FTP protocol.

We can get the length of the directory data by checking the length of the string DirectoryString:

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
—> DirectoryTextLength = Len(DirectoryText)
        .
        .
        .
End Sub

Here's the actual data we would receive for the directory of ftp.microsoft.com; note that it is set up in HTML to make it easy for Web browsers:

<BODY>
  <H2>FTP root at ftp.microsoft.com</H2>
    <HR>
    <H4><PRE>
    This is FTP.MICROSOFT.COM.  Please see the
    dirmap.txt file for more information.
    </PRE></H4>
    <HR>
    <PRE>
    09/20/96 07:34PM      Directory <A HREF="/bussys/"><B>bussys</B></A>
    09/23/96 08:46PM      Directory <A HREF="/deskapps/"><B>deskapps</B></A>
    08/16/96 05:33PM      Directory <A HREF="/developr/"><B>developr</B></A>
    09/11/96 01:01AM          8,012 <A HREF="/dirmap.htm">dirmap.htm</A>
    09/11/96 12:57AM          4,368 <A HREF="/dirmap.txt">dirmap.txt</A>
    08/25/94 12:00AM            712 <A HREF="/disclaimer.txt">disclaimer.txt</A>
    11/19/96 01:23AM      Directory <A HREF="/KBHelp/"><B>KBHelp</B></A>
    12/03/96 11:10AM      8,168,053 <A HREF="/ls-lR.txt">ls-lR.txt</A>
    12/03/96 11:10AM      1,065,823 <A HREF="/ls-lR.Z">ls-lR.Z</A>
    12/03/96 11:10AM        879,116 <A HREF="/LS-LR.ZIP">LS-LR.ZIP</A>
    10/20/95 12:00AM      Directory <A HREF="/MSCorp/"><B>MSCorp</B></A>
    10/27/96 12:24AM      Directory <A HREF="/msdownload/"><B>msdownload</B></A>
    10/11/95 12:00AM      Directory <A HREF="/peropsys/"><B>peropsys</B></A>
    11/30/95 12:00AM      Directory <A HREF="/Products/"><B>Products</B></A>
    10/28/96 11:47PM      Directory <A HREF="/Services/"><B>Services</B></A>
    05/30/96 01:39AM      Directory <A HREF="/Softlib/"><B>Softlib</B></A>
    04/08/96 02:21PM      Directory <A HREF="/solutions/"><B>solutions</B></A>
    </PRE>
    <HR>
</BODY>
</HTML>

We want to place the names of the text files into the listbox List1, so we will write our program to search through the listing for .txt files; we do that by searching for ".txt," which locates the end of the file name strings that we want. We will place each file name into its own string, so we call the location of the substring ".txt" StringEnd (the end of the file name string):

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
—> StringEnd = InStr(DirectoryText, ".txt""")
        .
        .
        .
End Sub

As long as the variable StringEnd is not 0, we have found a match. We should set up a loop, because there will be more than one text file to find:

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
    StringEnd = InStr(DirectoryText, ".txt""")
—> While (StringEnd <> 0)
      .
      .
      .
—> Wend
End Sub

Because the directory of ftp.microsoft.com is set up as a Web page (which is quite common), each file name is enclosed in quotation marks:

09/11/96 12:57AM          4,368 <A HREF="/dirmap.txt">dirmap.txt</A>

Now that we've found the ".txt" part of the file name, we can work back to the quotation mark to get the whole file name and the length of the file name (which we place in the variable StringLength):

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
    StringEnd = InStr(DirectoryText, ".txt""")
    While (StringEnd <> 0)
  —>   StringLength = 0
  —>   While (InStr(StringEnd - StringLength, Left(DirectoryText,
  —>   StringEnd),
  —>       """") = 0)
  —>       StringLength = StringLength + 1
  —>   Wend
  —>   StringLength = StringLength - 1
          .
          .
          .
    Wend
End Sub

At this point, we've found a file name in the FTP directory. We know how long it is, so we add it to our listbox List1 this way:

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
    StringEnd = InStr(DirectoryText, ".txt""")
    While (StringEnd <> 0)
        StringLength = 0
        While (InStr(StringEnd - StringLength, Left(DirectoryText,
        StringEnd),
            """") = 0)
            StringLength = StringLength + 1
        Wend
        StringLength = StringLength - 1
 —>    List1.AddItem Mid(DirectoryText, StringEnd - StringLength + 1,
            StringLength - 1) + ".txt"
               .
               .
               .
    Wend
End Sub

Next, we truncate the directory listing string (we won't need it for anything else, so we remove the parts we've already searched) to remove the already-found file name. Then we loop again to find the next occurrence of ".txt":

Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
    StringEnd = InStr(DirectoryText, ".txt""")
    While (StringEnd <> 0)
        StringLength = 0
        While (InStr(StringEnd - StringLength, Left(DirectoryText,
        StringEnd),
            """") = 0)
            StringLength = StringLength + 1
        Wend
        StringLength = StringLength - 1
        List1.AddItem Mid(DirectoryText, StringEnd - StringLength + 1,
            StringLength - 1) + ".txt"
  —>   DirectoryText = Right(DirectoryText, DirectoryTextLength -
  —>   StringEnd)
  —>   DirectoryTextLength = DirectoryTextLength - StringEnd
  —>   StringEnd = InStr(DirectoryText, ".txt""")
    Wend
End Sub

At this point, our listbox displays the names of the .txt files:

All that remains is to read the files from the FTP server when the user double-clicks its name in the listbox. We use List1_Click(), creating the full name of the file to fetch by adding "ftp://ftp.microsoft.com/" to the name of the file we are supposed to get in List1.Text:

Private Sub List1_Click()
    RichTextBox1.Text = Inet1.OpenURL("ftp://ftp.microsoft.com/" + List1.Text)
End Sub

That's it—the program is now functional. Run it to see the listing of the .txt files in the FTP server ftp.microsoft.com, as shown in Figure 3.1. Now click a file, such as disclaimer.txt, and it will be downloaded and appear in the rich text box. Our FTP program is a success.

Figure 3.1 Retrieving files from Microsoft via FTP.

That's how to download text. Although we can use the Execute() method, it's usually easier to use OpenURL() if we just want to download a file. We can download binary data, too. Let's look into that now.


Downloading Binary FTP Data

The earlier listing of the ftp.microsoft.com directory shows that one of the files is a binary file named LS-LR.zip. Let's download that file now. (Note that when you read this, the contents of the microsoft FTP directory may have changed.) Open the Menu Editor in the Visual Basic Tools menu and add a new item, Get File, to FTPer's File menu following the Close item, as shown in Figure 3.2. Give this item the name Getfile.

Figure 3.2 The FTPer Get File menu item.

Now double-click that item to open its click event:

Private Sub Getfile_Click()
End Sub

Our task is to download the file ftp://ftp.microsoft.com/LS-LR.zip. We can use OpenURL() to get this file, but we can't just assign the output of OpenURL() to a string, because LS-LR.zip is not a text file; instead, we set up a binary array:

Private Sub Getfile_Click()
—> Dim binarydata() As Byte
          .
          .
          .
End Sub

Now we are free to get the file we want using OpenURL(), passing it the icByteArray parameter to indicate that we are downloading a binary file:

Private Sub Getfile_Click()
    Dim binarydata() As Byte
—> binarydata() = Inet1.OpenURL("ftp://ftp.microsoft.com/LS-LR.zip",
        icByteArray)
        .
        .
        .
End Sub

Now we store this binary data to disk by opening LS-LR.zip and writing the binarydata() array to it:

Private Sub Getfile_Click()
    Dim binarydata() As Byte
    binarydata() = Inet1.OpenURL("ftp://ftp.microsoft.com/LS-LR.zip",
        icByteArray)
—> Open "E:\vb\LS-LR.zip" For Binary Access Write As #1
—> Put #1, , binarydata()
        .
        .
        .
End Sub

Finally, we close the file and pop a message box on the screen to indicate we are finished:

Private Sub Getfile_Click()
    Dim binarydata() As Byte
    binarydata() = Inet1.OpenURL("ftp://ftp.microsoft.com/LS-LR.zip",
        icByteArray)
    Open "E:\vb\LS-LR.zip" For Binary Access Write As #1
    Put #1, , binarydata()
—> Close #1
—> MsgBox "Finished."
End Sub

And that's it—now we're able to download FTP files, both text and binary, as well as work with the standard FTP commands such as MKDIR, GET, PUT, and CD. The code for our FTPer program appears in Listing 3.1.

Listing 3.1 (FTPer) FrmMain.frm

VERSION 5.00
Object = "{F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.1#0"; "COMDLG32.OCX"
Object = "{6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.1#0"; "COMCTL32.OCX"
Object = "{48E59290-9880-11CF-9754-00AA00C00908}#1.0#0"; "MSINET.OCX"
Object = "{3B7C8863-D78F-101B-B9B5-04021C009402}#1.1#0"; "RICHTX32.OCX"
Begin VB.Form frmMain
   Caption         =   "FTPer"
   ClientHeight    =   4890
   ClientLeft      =   165
   ClientTop       =   735
   ClientWidth     =   4680
   LinkTopic       =   "Form1"
   ScaleHeight     =   4890
   ScaleWidth      =   4680

StartUpPosition = 3 'Windows Default

Begin ComctlLib.Toolbar tbToolBar
      Align           =   1  'Align Top
      Height          =   420
      Left            =   0
      TabIndex        =   1
      Top             =   0
      Width           =   4680
      _ExtentX        =   8255
      _ExtentY        =   741
      ButtonWidth     =   635
      ButtonHeight    =   582
      Appearance      =   1
      ImageList       =   "imlIcons"
      BeginProperty Buttons {7791BA41-E020-11CF-8E74-00A0C90F26F8}
         NumButtons      =   17
         BeginProperty Button1 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "New"
            Object.ToolTipText     =   "New"
            Object.Tag             =   ""
            ImageIndex      =   1
         EndProperty
         BeginProperty Button2 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Open"
            Object.ToolTipText     =   "Open"
            Object.Tag             =   ""
            ImageIndex      =   2
         EndProperty
         BeginProperty Button3 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Save"
            Object.ToolTipText     =   "Save"
            Object.Tag             =   ""
            ImageIndex      =   3
         EndProperty
         BeginProperty Button4 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button5 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Print"
            Object.ToolTipText     =   "Print"
            Object.Tag             =   ""
            ImageIndex      =   4
         EndProperty
         BeginProperty Button6 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button7 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Cut"
            Object.ToolTipText     =   "Cut"
            Object.Tag             =   ""
            ImageIndex      =   5
         EndProperty
         BeginProperty Button8 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Copy"
            Object.ToolTipText     =   "Copy"
            Object.Tag             =   ""
            ImageIndex      =   6
         EndProperty
         BeginProperty Button9 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Paste"
            Object.ToolTipText     =   "Paste"
            Object.Tag             =   ""
            ImageIndex      =   7
         EndProperty
         BeginProperty Button10 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button11 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Bold"
            Object.ToolTipText     =   "Bold"
            Object.Tag             =   ""
            ImageIndex      =   8
         EndProperty
         BeginProperty Button12 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Italic"
            Object.ToolTipText     =   "Italic"
            Object.Tag             =   ""
            ImageIndex      =   9
         EndProperty
         BeginProperty Button13 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Underline"
            Object.ToolTipText     =   "Underline"
            Object.Tag             =   ""
            ImageIndex      =   10
         EndProperty
         BeginProperty Button14 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button15 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Left"
            Object.ToolTipText     =   "Left Justify"
            Object.Tag             =   ""
            ImageIndex      =   11

EndProperty

BeginProperty Button16 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Center"
            Object.ToolTipText     =   "Center"
            Object.Tag             =   ""
            ImageIndex      =   12
         EndProperty
         BeginProperty Button17 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Right"
            Object.ToolTipText     =   "Right Justify"
            Object.Tag             =   ""
            ImageIndex      =   13
         EndProperty
      EndProperty
   End
   Begin VB.ListBox List1
      Height          =   675
      Left            =   360
      TabIndex        =   3
      Top             =   1320
      Width           =   3975
   End
   Begin MSComDlg.CommonDialog dlgCommonDialog
      Left            =   2880
      Top             =   600
      _ExtentX        =   847
      _ExtentY        =   847
      FontSize        =   1.87198e-37
   End
   Begin ComctlLib.StatusBar sbStatusBar
      Align           =   2  'Align Bottom
      Height          =   270
      Left            =   0
      TabIndex        =   0
      Top             =   4620
      Width           =   4680
      _ExtentX        =   8255
      _ExtentY        =   476
      SimpleText      =   ""
      BeginProperty Panels {2C787A51-E01C-11CF-8E74-00A0C90F26F8}
         NumPanels       =   3
         BeginProperty Panel1 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            AutoSize        =   1
            Object.Width           =   2619
            MinWidth        =   2540
            Text            =   "Status"
            TextSave        =   "Status"
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel2 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   6
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "12/4/96"
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel3 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   5
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "3:06 PM"
            Object.Tag             =   ""
         EndProperty
      EndProperty
      BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
         Name            =   "MS Sans Serif"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
   End
   Begin InetCtlsObjects.Inet Inet1
      Left            =   1200
      Top             =   480
      _ExtentX        =   1005
      _ExtentY        =   1005
   End
   Begin RichTextLib.RichTextBox RichTextBox1
      Height          =   2175
      Left            =   360
      TabIndex        =   2
      Top             =   2400
      Width           =   3975
      _ExtentX        =   7011
      _ExtentY        =   3836
      ScrollBars      =   3
      TextRTF         =   $"frmMain.frx":0000
   End
   Begin VB.Label Label2
      Caption         =   "Text:"
      Height          =   495
      Left            =   360

TabIndex = 5

Top             =   2160
      Width           =   1215
   End
   Begin VB.Label label1
      Caption         =   "Text Files:"
      Height          =   495
      Left            =   360
      TabIndex        =   4
      Top             =   1080
      Width           =   1215
   End
   Begin ComctlLib.ImageList imlIcons
      Left            =   2040
      Top             =   480
      _ExtentX        =   1005
      _ExtentY        =   1005
      BackColor       =   -2147483643
      ImageWidth      =   16
      ImageHeight     =   16
      MaskColor       =   12632256
      BeginProperty Images {8556BCD1-E01E-11CF-8E74-00A0C90F26F8}
         NumListImages   =   13
         BeginProperty ListImage1 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":00F7
            Key             =   ""
         EndProperty
         BeginProperty ListImage2 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0449
            Key             =   ""
         EndProperty
         BeginProperty ListImage3 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":079B
            Key             =   ""
         EndProperty
         BeginProperty ListImage4 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0AED
            Key             =   ""
         EndProperty
         BeginProperty ListImage5 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0E3F
            Key             =   ""
         EndProperty
         BeginProperty ListImage6 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1191
            Key             =   ""
         EndProperty
         BeginProperty ListImage7 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":14E3
            Key             =   ""
         EndProperty
         BeginProperty ListImage8 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1835
            Key             =   ""
         EndProperty
         BeginProperty ListImage9 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1B87
            Key             =   ""
         EndProperty
         BeginProperty ListImage10 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1ED9
            Key             =   ""
         EndProperty
         BeginProperty ListImage11 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":222B
            Key             =   ""
         EndProperty
         BeginProperty ListImage12 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":257D
            Key             =   ""
         EndProperty
         BeginProperty ListImage13 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":28CF
            Key             =   ""
         EndProperty
      EndProperty
   End
   Begin VB.Menu mnuFile
      Caption         =   "&File"
      Begin VB.Menu mnuFileNew
         Caption         =   "&New"
         Shortcut        =   ^N
      End
      Begin VB.Menu mnuFileOpen
         Caption         =   "&Open"
         Shortcut        =   ^O
      End
      Begin VB.Menu mnuFileClose
         Caption         =   "&Close"
      End
      Begin VB.Menu Getfile
         Caption         =   "Get File"
      End
      Begin VB.Menu mnuFileBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileSave

Caption = "&Save"

Shortcut        =   ^S
      End
      Begin VB.Menu mnuFileSaveAs
         Caption         =   "Save &As..."
      End
      Begin VB.Menu mnuFileSaveAll
         Caption         =   "Save A&ll"
      End
      Begin VB.Menu mnuFileBar2
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileProperties
         Caption         =   "Propert&ies"
      End
      Begin VB.Menu mnuFileBar3
         Caption         =   "-"
      End
      Begin VB.Menu mnuFilePageSetup
         Caption         =   "Page Set&up..."
      End
      Begin VB.Menu mnuFilePrintPreview
         Caption         =   "Print Pre&view"
      End
      Begin VB.Menu mnuFilePrint
         Caption         =   "&Print..."
         Shortcut        =   ^P
      End
      Begin VB.Menu mnuFileBar4
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileSend
         Caption         =   "Sen&d..."
      End
      Begin VB.Menu mnuFileBar5
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   0
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   1
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   2
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   3
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileBar6
         Caption         =   "-"
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileExit
         Caption         =   "E&xit"
      End
   End
   Begin VB.Menu mnuEdit
      Caption         =   "&Edit"
      Begin VB.Menu mnuEditUndo
         Caption         =   "&Undo"
         Shortcut        =   ^Z
      End
      Begin VB.Menu mnuEditBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuEditCut
         Caption         =   "Cu&t"
         Shortcut        =   ^X
      End
      Begin VB.Menu mnuEditCopy
         Caption         =   "&Copy"
         Shortcut        =   ^C
      End
      Begin VB.Menu mnuEditPaste
         Caption         =   "&Paste"
         Shortcut        =   ^V
      End
      Begin VB.Menu mnuEditPasteSpecial
         Caption         =   "Paste &Special..."
      End
   End
   Begin VB.Menu mnuHelp
      Caption         =   "&Help"
      Begin VB.Menu mnuHelpContents
         Caption         =   "&Contents"
      End
      Begin VB.Menu mnuHelpSearch
         Caption         =   "&Search For Help On..."

End

Begin VB.Menu mnuHelpBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuHelpAbout
         Caption         =   "&About FTPer..."
      End
   End
End
Attribute VB_Name = "frmMain"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private Declare Function OSWinHelp% Lib "user32" Alias "WinHelpA" (ByVal hwnd&, ByVal HelpFile$, ByVal wCommand%, dwData As Any)
Private Sub Form_Load()
    Dim DirectoryText
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
    DirectoryText = Inet1.OpenURL("ftp://ftp.microsoft.com")
    DirectoryTextLength = Len(DirectoryText)
    StringEnd = InStr(DirectoryText, ".txt""")
    While (StringEnd <> 0)
        StringLength = 0
        While (InStr(StringEnd - StringLength, Left(DirectoryText,
        StringEnd), """") = 0)
            StringLength = StringLength + 1
        Wend
        StringLength = StringLength - 1
        List1.AddItem Mid(DirectoryText, StringEnd - StringLength + 1,
        StringLength - 1) + ".txt"
        DirectoryText = Right(DirectoryText, DirectoryTextLength -
        StringEnd)
        DirectoryTextLength = DirectoryTextLength - StringEnd
        StringEnd = InStr(DirectoryText, ".txt""")
    Wend
End Sub
Private Sub Form_Unload(Cancel As Integer)
    If Me.WindowState <> vbMinimized Then
        SaveSetting App.Title, "Settings", "MainLeft", Me.Left
        SaveSetting App.Title, "Settings", "MainTop", Me.Top
        SaveSetting App.Title, "Settings", "MainWidth", Me.Width
        SaveSetting App.Title, "Settings", "MainHeight", Me.Height
    End If
End Sub
Private Sub Getfile_Click()
    Dim binarydata() As Byte
    binarydata() = Inet1.OpenURL("ftp://ftp.microsoft.com/LS-LR.zip",
    icByteArray)
    Open "E:\vb\LS-LR.zip" For Binary Access Write As #1
    Put #1, , binarydata()
    Close #1
    MsgBox "Finished."
End Sub
Private Sub List1_Click()
    RichTextBox1.Text = Inet1.OpenURL("ftp://ftp.microsoft.com/" + L
    ist1.Text)
End Sub
Private Sub mnuHelpAbout_Click()
    'To Do
    MsgBox "About Box Code goes here!"
End Sub
Private Sub tbToolBar_ButtonClick(ByVal Button As ComctlLib.Button)
    Select Case Button.Key
        Case "New"

mnuFileNew_Click

Case "New"
            mnuFileNew_Click
        Case "Open"
            mnuFileOpen_Click
        Case "Save"
            mnuFileSave_Click
        Case "Print"
            mnuFilePrint_Click
        Case "Cut"
            mnuEditCut_Click
        Case "Copy"
            mnuEditCopy_Click
        Case "Paste"
            mnuEditPaste_Click
        Case "Bold"
            'To Do
            MsgBox "Bold Code goes here!"
        Case "Italic"
            'To Do
            MsgBox "Italic Code goes here!"
        Case "Underline"
            'To Do
            MsgBox "Underline Code goes here!"
        Case "Left"
            'To Do
            MsgBox "Left Code goes here!"
        Case "Center"
            'To Do
            MsgBox "Center Code goes here!"
        Case "Right"
            'To Do
            MsgBox "Right Code goes here!"
    End Select
End Sub
Private Sub mnuHelpContents_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to t
'he user you can set the HelpFile for your application in the
'Project Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 3, 0)
        If Err Then
            MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuHelpSearch_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to
'the user you can set the HelpFile for your application in the
'Project Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 261, 0)
        If Err Then
            MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuEditCopy_Click()
    'To Do
    MsgBox "Copy Code goes here!"
End Sub
Private Sub mnuEditCut_Click()
    'To Do
    MsgBox "Cut Code goes here!"
End Sub
Private Sub mnuEditPaste_Click()
    'To Do
    MsgBox "Paste Code goes here!"
End Sub
Private Sub mnuEditPasteSpecial_Click()

'To Do

MsgBox "Paste Special Code goes here!"
End Sub
Private Sub mnuEditUndo_Click()
    'To Do
    MsgBox "Undo Code goes here!"
End Sub
Private Sub mnuFileOpen_Click()
    Dim sFile As String
    With dlgCommonDialog
        'To Do
        'set the flags and attributes of the
        'common dialog control
        .Filter = "All Files (*.*)|*.*"
        .ShowOpen
        If Len(.filename) = 0 Then
            Exit Sub
        End If
        sFile = .filename
    End With
    'To Do
    'process the opened file
End Sub
Private Sub mnuFileClose_Click()
    'To Do
    MsgBox "Close Code goes here!"
End Sub
Private Sub mnuFileSave_Click()
    'To Do
    MsgBox "Save Code goes here!"
End Sub
Private Sub mnuFileSaveAs_Click()
    'To Do
    'Set up the common dialog control
    'prior to calling ShowSave
    dlgCommonDialog.ShowSave
End Sub
Private Sub mnuFileSaveAll_Click()
    'To Do
    MsgBox "Save All Code goes here!"
End Sub
Private Sub mnuFileProperties_Click()
    'To Do
    MsgBox "Properties Code goes here!"
End Sub
Private Sub mnuFilePageSetup_Click()
    dlgCommonDialog.ShowPrinter
End Sub
Private Sub mnuFilePrintPreview_Click()
    'To Do
    MsgBox "Print Preview Code goes here!"
End Sub
Private Sub mnuFilePrint_Click()

'To Do

MsgBox "Print Code goes here!"
End Sub
Private Sub mnuFileSend_Click()
    'To Do
    MsgBox "Send Code goes here!"
End Sub
Private Sub mnuFileMRU_Click(Index As Integer)
    'To Do
    MsgBox "MRU Code goes here!"
End Sub
Private Sub mnuFileExit_Click()
    'unload the form
    Unload Me
End Sub
Private Sub mnuFileNew_Click()
    'To Do
    MsgBox "New File Code goes here!"
End Sub

We've explored the FTP protocol, and we've seen how powerful it is. The Internet Transfer control also supports the HTTP protocol, and we'll look into that next.


Using HTTP from Visual Basic

As with the FTP protocol, we can use either the Execute() method or the OpenURL() method with the HTTP protocol. Using the Execute() method, we can use the HTTP GET, HEAD (get header), POST, and PUT commands. These commands appear in Table 3.3. You can also use the secure Web protocol, HTTPS, with the Internet Transfer control; just use that prefix instead of http in the URL of the document you want to establish a secure link to: "https://www.microsoft.com." In this way, you can open a secure Web session (if the user's Web browser supports secure sessions).

Table 3.3 HTTP Commands of the Internet Transfer Control's Execute Method

Command | Does This
GET | Gets the file named in URL (e.g., Execute "http://www.server.com/index.htm", "GET").
HEAD | Gets headers of file given in URL property (e.g., Execute , "HEAD").
POST | Provides additional data to support request to host (e.g., Execute , "POST", strFormData).
PUT | Replaces data at URL (e.g., Execute , "PUT", "new.htm").


Downloading Text HTTP Data

Let's use the HTTP protocol in a program. We'll download the Microsoft Visual Basic Web page itself using the HTTP protocol. This page is named default.htm, and it resides at http://www.microsoft.com/vbasic. Let's name this new program HTTPer. Create that project in Visual Basic, making it an SDI program with the Application Wizard. Then add a rich text box (RichTextbox1) and an Internet Transfer control (Inet1) as we did in the previous example, FTPer:

Now we add a new item to the File menu—Get File—using the Visual Basic Menu Editor. When the user clicks this item, we'll download the HTML of the Visual Basic Web page. As with FTPer, place this item after the Close item in the File menu and give it the name GetFile. Now open that item's click event handler:

Private Sub GetFile_Click()
End Sub

To get the Visual Basic Web page, we use OpenURL() as we have before in this chapter. Because the URL we are opening is prefaced http:, OpenURL() will use the HTTP protocol. We open the Web page and place its text into our rich text box this way:

Private Sub GetFile_Click()
—> RichTextBox1.Text =
        Inet1.OpenURL("http://www.microsoft.com/vbasic/default.htm")
        .
        .
        .
End Sub

That's all there is to it. Now we can download HTML files using the HTTP protocol. When we run our program, the result appears in Figure 3.3.

Downloading the microsoft.com Visual Basic Web page.

We can also download binary data using OpenURL(), and we'll take a look at that now.


Downloading Binary HTTP Data

Let's say that we want to download a file named image.jpg at http://www.server.com/museums. We can do that with the Internet Transfer control, as you might expect. We fill a binary array with the data from the binary file and pass the icByteArray parameter to OpenURL() to indicate that we are downloading binary data:

Private Sub GetFile_Click()
—> Dim binarydata() As Byte
    RichTextBox1.Text =
        Inet1.OpenURL("http://www.microsoft.com/vbasic/default.htm")
—> binarydata() =
        Inet1.OpenURL("http://www.server.com/museums/image.jpg", icByteArray)
        .
        .
        .
End Sub

Now that the file has been loaded into the binary array, we store the file on disk:

Private Sub GetFile_Click()
    Dim binarydata() As Byte
    RichTextBox1.Text =
        Inet1.OpenURL("http://www.microsoft.com/vbasic/default.htm")
    binarydata() =
        Inet1.OpenURL("http://www.server.com/museums/image.jpg", icByteArray)
—> Open "E:\vb\image.jpg" For Binary Access Write As #1
—> Put #1, , binarydata()
—> Close #1
     .
     .
     .
End Sub

Finally, let's put a message box on the screen indicating that we have finished our downloading operations. (This code would be placed in the Internet Transfer control's StateChanged event handler if you were using the asynchronous Execute method.)

Private Sub GetFile_Click()
    Dim binarydata() As Byte
    RichTextBox1.Text =
        Inet1.OpenURL("http://www.microsoft.com/vbasic/default.htm")
    binarydata() =
        Inet1.OpenURL("http://www.server.com/museums/image.jpg", icByteArray)
    Open "E:\vb\image.jpg" For Binary Access Write As #1
    Put #1, , binarydata()
    Close #1
—> MsgBox "Finished."
End Sub

Now we can download binary files as well as text files. Using the Internet Transfer control, the process is easy: we just use OpenURL() or the Execute() method, and Visual Basic handles the rest. The code for the HTTPer project appears in Listing 3.2.

Listing 3.2 (HTTPer) FrmMain.frm

VERSION 5.00
Object = "{F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.1#0"; "COMDLG32.OCX"
Object = "{6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.1#0"; "COMCTL32.OCX"
Object = "{48E59290-9880-11CF-9754-00AA00C00908}#1.0#0"; "MSINET.OCX"
Object = "{3B7C8863-D78F-101B-B9B5-04021C009402}#1.1#0"; "RICHTX32.OCX"
Begin VB.Form frmMain
   Caption         =   "HTTPer"
   ClientHeight    =   4050
   ClientLeft      =   165
   ClientTop       =   735
   ClientWidth     =   6315
   LinkTopic       =   "Form1"
   ScaleHeight     =   4050
   ScaleWidth      =   6315
   StartUpPosition =   3  'Windows Default
   Begin ComctlLib.Toolbar tbToolBar
      Align           =   1  'Align Top
      Height          =   420
      Left            =   0
      TabIndex        =   1
      Top             =   0
      Width           =   6315
      _ExtentX        =   11139
      _ExtentY        =   741
      ButtonWidth     =   635
      ButtonHeight    =   582
      Appearance      =   1
      ImageList       =   "imlIcons"
      BeginProperty Buttons {7791BA41-E020-11CF-8E74-00A0C90F26F8}
         NumButtons      =   17
         BeginProperty Button1 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "New"
            Object.ToolTipText     =   "New"
            Object.Tag             =   ""
            ImageIndex      =   1
         EndProperty
         BeginProperty Button2 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Open"
            Object.ToolTipText     =   "Open"
            Object.Tag             =   ""
            ImageIndex      =   2
         EndProperty
         BeginProperty Button3 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Save"
            Object.ToolTipText     =   "Save"
            Object.Tag             =   ""
            ImageIndex      =   3
         EndProperty
         BeginProperty Button4 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button5 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Print"
            Object.ToolTipText     =   "Print"
            Object.Tag             =   ""
            ImageIndex      =   4
         EndProperty
         BeginProperty Button6 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button7 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Cut"
            Object.ToolTipText     =   "Cut"
            Object.Tag             =   ""
            ImageIndex      =   5
         EndProperty
         BeginProperty Button8 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Copy"
            Object.ToolTipText     =   "Copy"
            Object.Tag             =   ""
            ImageIndex      =   6
         EndProperty
         BeginProperty Button9 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Paste"
            Object.ToolTipText     =   "Paste"
            Object.Tag             =   ""
            ImageIndex      =   7
         EndProperty
         BeginProperty Button10 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button11 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Bold"
            Object.ToolTipText     =   "Bold"
            Object.Tag             =   ""
            ImageIndex      =   8

EndProperty

BeginProperty Button12 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Italic"
            Object.ToolTipText     =   "Italic"
            Object.Tag             =   ""
            ImageIndex      =   9
         EndProperty
         BeginProperty Button13 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Underline"
            Object.ToolTipText     =   "Underline"
            Object.Tag             =   ""
            ImageIndex      =   10
         EndProperty
         BeginProperty Button14 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button15 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Left"
            Object.ToolTipText     =   "Left Justify"
            Object.Tag             =   ""
            ImageIndex      =   11
         EndProperty
         BeginProperty Button16 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Center"
            Object.ToolTipText     =   "Center"
            Object.Tag             =   ""
            ImageIndex      =   12
         EndProperty
         BeginProperty Button17 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Right"
            Object.ToolTipText     =   "Right Justify"
            Object.Tag             =   ""
            ImageIndex      =   13
         EndProperty
      EndProperty
   End
   Begin ComctlLib.StatusBar sbStatusBar
      Align           =   2  'Align Bottom
      Height          =   270
      Left            =   0
      TabIndex        =   0
      Top             =   3780
      Width           =   6315
      _ExtentX        =   11139
      _ExtentY        =   476
      SimpleText      =   ""
      BeginProperty Panels {2C787A51-E01C-11CF-8E74-00A0C90F26F8}
         NumPanels       =   3
         BeginProperty Panel1 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            AutoSize        =   1
            Object.Width           =   5503
            MinWidth        =   2540
            Text            =   "Status"
            TextSave        =   "Status"
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel2 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   6
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "12/4/96"
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel3 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   5
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "9:25 PM"
            Object.Tag             =   ""
         EndProperty
      EndProperty
      BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
         Name            =   "MS Sans Serif"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
   End
   Begin MSComDlg.CommonDialog dlgCommonDialog
      Left            =   1740
      Top             =   1350
      _ExtentX        =   847
      _ExtentY        =   847
      FontSize        =   1.87933e-37
   End
   Begin InetCtlsObjects.Inet Inet1
      Left            =   2040
      Top             =   1320
      _ExtentX        =   1005
      _ExtentY        =   1005
   End

Begin RichTextLib.RichTextBox RichTextBox1

Height          =   3135
      Left            =   120
      TabIndex        =   2
      Top             =   600
      Width           =   6015
      _ExtentX        =   10610
      _ExtentY        =   5530
      ScrollBars      =   3
      TextRTF         =   $"frmMain.frx":0000
   End
   Begin ComctlLib.ImageList imlIcons
      Left            =   1740
      Top             =   1350
      _ExtentX        =   1005
      _ExtentY        =   1005
      BackColor       =   -2147483643
      ImageWidth      =   16
      ImageHeight     =   16
      MaskColor       =   12632256
      BeginProperty Images {8556BCD1-E01E-11CF-8E74-00A0C90F26F8}
         NumListImages   =   13
         BeginProperty ListImage1 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":00D2
            Key             =   ""
         EndProperty
         BeginProperty ListImage2 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0424
            Key             =   ""
         EndProperty
         BeginProperty ListImage3 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0776
            Key             =   ""
         EndProperty
         BeginProperty ListImage4 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0AC8
            Key             =   ""
         EndProperty
         BeginProperty ListImage5 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0E1A
            Key             =   ""
         EndProperty
         BeginProperty ListImage6 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":116C
            Key             =   ""
         EndProperty
         BeginProperty ListImage7 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":14BE
            Key             =   ""
         EndProperty
         BeginProperty ListImage8 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1810
            Key             =   ""
         EndProperty
         BeginProperty ListImage9 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1B62
            Key             =   ""
         EndProperty
         BeginProperty ListImage10 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1EB4
            Key             =   ""
         EndProperty
         BeginProperty ListImage11 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":2206
            Key             =   ""
         EndProperty
         BeginProperty ListImage12 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":2558
            Key             =   ""
         EndProperty
         BeginProperty ListImage13 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":28AA
            Key             =   ""
         EndProperty
      EndProperty
   End
   Begin VB.Menu mnuFile
      Caption         =   "&File"
      Begin VB.Menu mnuFileNew
         Caption         =   "&New"
         Shortcut        =   ^N
      End
      Begin VB.Menu mnuFileOpen
         Caption         =   "&Open"
         Shortcut        =   ^O
      End
      Begin VB.Menu mnuFileClose
         Caption         =   "&Close"
      End
      Begin VB.Menu GetFile
         Caption         =   "Get File"
      End
      Begin VB.Menu mnuFileBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileSave
         Caption         =   "&Save"

Shortcut = ^S

End
      Begin VB.Menu mnuFileSaveAs
         Caption         =   "Save &As..."
      End
      Begin VB.Menu mnuFileSaveAll
         Caption         =   "Save A&ll"
      End
      Begin VB.Menu mnuFileBar2
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileProperties
         Caption         =   "Propert&ies"
      End
      Begin VB.Menu mnuFileBar3
         Caption         =   "-"
      End
      Begin VB.Menu mnuFilePageSetup
         Caption         =   "Page Set&up..."
      End
      Begin VB.Menu mnuFilePrintPreview
         Caption         =   "Print Pre&view"
      End
      Begin VB.Menu mnuFilePrint
         Caption         =   "&Print..."
         Shortcut        =   ^P
      End
      Begin VB.Menu mnuFileBar4
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileSend
         Caption         =   "Sen&d..."
      End
      Begin VB.Menu mnuFileBar5
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   0
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   1
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   2
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   3
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileBar6
         Caption         =   "-"
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileExit
         Caption         =   "E&xit"
      End
   End
   Begin VB.Menu mnuEdit
      Caption         =   "&Edit"
      Begin VB.Menu mnuEditUndo
         Caption         =   "&Undo"
         Shortcut        =   ^Z
      End
      Begin VB.Menu mnuEditBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuEditCut
         Caption         =   "Cu&t"
         Shortcut        =   ^X
      End
      Begin VB.Menu mnuEditCopy
         Caption         =   "&Copy"
         Shortcut        =   ^C
      End
      Begin VB.Menu mnuEditPaste
         Caption         =   "&Paste"
         Shortcut        =   ^V
      End
      Begin VB.Menu mnuEditPasteSpecial
         Caption         =   "Paste &Special..."
      End
   End
   Begin VB.Menu mnuHelp
      Caption         =   "&Help"
      Begin VB.Menu mnuHelpContents
         Caption         =   "&Contents"
      End
      Begin VB.Menu mnuHelpSearch
         Caption         =   "&Search For Help On..."
      End

Begin VB.Menu mnuHelpBar1

Caption         =   "-"
      End
      Begin VB.Menu mnuHelpAbout
         Caption         =   "&About HTTPer..."
      End
   End
End
Attribute VB_Name = "frmMain"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private Declare Function OSWinHelp% Lib "user32" Alias "WinHelpA" (ByVal hwnd&, ByVal HelpFile$, ByVal wCommand%, dwData As Any)
Private Sub Form_Load()
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)
    Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)
End Sub
Private Sub Form_Unload(Cancel As Integer)
    If Me.WindowState <> vbMinimized Then
        SaveSetting App.Title, "Settings", "MainLeft", Me.Left
        SaveSetting App.Title, "Settings", "MainTop", Me.Top
        SaveSetting App.Title, "Settings", "MainWidth", Me.Width
        SaveSetting App.Title, "Settings", "MainHeight", Me.Height
    End If
End Sub
Private Sub GetFile_Click()
    Dim binarydata() As Byte
    RichTextBox1.Text =
        Inet1.OpenURL("http://www.microsoft.com/vbasic/default.htm")
    binarydata() =
        Inet1.OpenURL("http://www.server.com/museums/image.jpg", icByteArray)
    Open "E:\vb\image.jpg" For Binary Access Write As #1
    Put #1, , binarydata()
    Close #1
    MsgBox "Finished."
End Sub
Private Sub mnuHelpAbout_Click()
    'To Do
    MsgBox "About Box Code goes here!"
End Sub
Private Sub tbToolBar_ButtonClick(ByVal Button As ComctlLib.Button)
    Select Case Button.Key
        Case "New"
            mnuFileNew_Click
        Case "New"
            mnuFileNew_Click
        Case "Open"
            mnuFileOpen_Click
        Case "Save"
            mnuFileSave_Click
        Case "Print"
            mnuFilePrint_Click
        Case "Cut"
            mnuEditCut_Click
        Case "Copy"
            mnuEditCopy_Click
        Case "Paste"
            mnuEditPaste_Click
        Case "Bold"
            'To Do
            MsgBox "Bold Code goes here!"
        Case "Italic"
            'To Do
            MsgBox "Italic Code goes here!"
        Case "Underline"
            'To Do
            MsgBox "Underline Code goes here!"
        Case "Left"
            'To Do
            MsgBox "Left Code goes here!"
        Case "Center"
            'To Do
            MsgBox "Center Code goes here!"
        Case "Right"

'To Do

MsgBox "Right Code goes here!"
    End Select
End Sub
Private Sub mnuHelpContents_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to the user
    'you can set the HelpFile for your application in the
    'Project Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 3, 0)
        If Err Then
            MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuHelpSearch_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to the user
    'you can set the HelpFile for your application in the
    'Project Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 261, 0)
        If Err Then
            MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuEditCopy_Click()
    'To Do
    MsgBox "Copy Code goes here!"
End Sub
Private Sub mnuEditCut_Click()
    'To Do
    MsgBox "Cut Code goes here!"
End Sub
Private Sub mnuEditPaste_Click()
    'To Do
    MsgBox "Paste Code goes here!"
End Sub
Private Sub mnuEditPasteSpecial_Click()
    'To Do
    MsgBox "Paste Special Code goes here!"
End Sub
Private Sub mnuEditUndo_Click()
    'To Do
    MsgBox "Undo Code goes here!"
End Sub
Private Sub mnuFileOpen_Click()
    Dim sFile As String
    With dlgCommonDialog
        'To Do
        'set the flags and attributes of the
        'common dialog control
        .Filter = "All Files (*.*)|*.*"

.ShowOpen

If Len(.filename) = 0 Then
            Exit Sub
        End If
        sFile = .filename
    End With
    'To Do
    'process the opened file
End Sub
Private Sub mnuFileClose_Click()
    'To Do
    MsgBox "Close Code goes here!"
End Sub
Private Sub mnuFileSave_Click()
    'To Do
    MsgBox "Save Code goes here!"
End Sub
Private Sub mnuFileSaveAs_Click()
    'To Do
    'Set up the common dialog control
    'prior to calling ShowSave
    dlgCommonDialog.ShowSave
End Sub
Private Sub mnuFileSaveAll_Click()
    'To Do
    MsgBox "Save All Code goes here!"
End Sub
Private Sub mnuFileProperties_Click()
    'To Do
    MsgBox "Properties Code goes here!"
End Sub
Private Sub mnuFilePageSetup_Click()
    dlgCommonDialog.ShowPrinter
End Sub
Private Sub mnuFilePrintPreview_Click()
    'To Do
    MsgBox "Print Preview Code goes here!"
End Sub
Private Sub mnuFilePrint_Click()
    'To Do
    MsgBox "Print Code goes here!"
End Sub
Private Sub mnuFileSend_Click()
    'To Do
    MsgBox "Send Code goes here!"
End Sub
Private Sub mnuFileMRU_Click(Index As Integer)
    'To Do
    MsgBox "MRU Code goes here!"
End Sub
Private Sub mnuFileExit_Click()
    'unload the form
    Unload Me

End Sub

Private Sub mnuFileNew_Click()
    'To Do
    MsgBox "New File Code goes here!"
End Sub

That's it for our coverage of the FTP and HTTP protocols for the moment. In this chapter we've worked with powerful methods of interacting with the Internet using these protocols. We've discussed the Execute() method, FTP commands such as CD, MKDIR, GET, PUT, and DIR, and the HTTP commands GET, HEAD, POST, and PUT, as well as the OpenURL() method, which handles a great many of the details. In the next chapter, we'll continue our exploration of Visual Basic on the Internet when we turn to email.


VB on the Internet: Email

In this chapter, we'll take a look at working with email in Visual Basic programs. Using the email support in Visual Basic, you can read email, send email, compose email messages, save, copy, and delete messages, display an address book, read and send attachments, forward email, and more. These functions are performed by the Messaging Application Program Interface (MAPI) controls that we'll add to our Visual Basic programs.

There are two MAPI controls: MAPISession and MAPIMessages. The MAPISession control establishes an email session, and the MAPIMessages control works with individual email messages in that session. The MAPI controls work with the underlying email system of your computer, and here we'll use the Microsoft Exchange email system, which comes with Windows. (It is reasonable to expect that your users will also have Microsoft Exchange.) We'll begin by making sure that Microsoft Exchange is set up for Internet Mail.


Setting Up Internet Mail

The underlying email system installed on the user's computer does the actual work, as directed by the MAPI controls. For this reason, you should make sure that you have Microsoft Exchange set up for Internet Mail before working with the examples in this chapter. In Windows 95, open the Control Panel and double-click the Mail and Fax icon there, opening the MS Exchange Properties Settings dialog box. Then select the Services tab. If Internet Mail is one of the services listed in the Services dialog box, you're already set up. If it is not there, click the Add button to open the Add Service to Profile dialog box and click the Internet Mail entry. This opens the Internet Mail dialog box, as shown in Figure 4.1. Here you fill out the information about your Internet service provider, including your account name and password. Click OK.

Figure 4.1 Setting up Internet mail.

For the purposes of this chapter, we will assume that Microsoft Exchange is set up only for Internet mail this way and not for Microsoft mail or any other option. (Otherwise, you must make sure that you are set up to receive Microsoft Mail and any other types you specify; here, we'll look only at Internet mail.)

As before, if you are using a modem, you must also have a connection set up to your ISP so that the connection can be made. To do that in Windows 95, double-click the My Computer icon on the desktop and then open the Dial-up Networking icon. To add a new connection, double-click the Make New Connection icon and follow the directions there.

The MAPI controls are centered around the computer's Inbox; the Inbox icon appears on the Windows desktop. The MAPISession control, for example, automatically downloads any new email to the Inbox when it establishes a connection with your ISP. After your email is downloaded to the Inbox, you double-click the Inbox icon on the desktop to read it. (You can also fetch email from the Inbox in your Visual Basic program using the MAPIMessages control, as we'll see.)

Now that we know our email system is set up, let's start working with email in our Visual Basic programs.


Our eMailer Application

Our first example will be called eMailer:

With this program, we can check email and download it as well as compose and send email. We can even perform another function with eMailer: many programs allow the user to "register" with the click of the mouse, and we'll do that, too. We will set up eMailer to send a prewritten registration message to our email address; to register, all the user must do is to click a Register Now item in the Help menu. This action will send the message to us automatically, including the user's name and email address so that we can register him or her.

Using the Visual Basic Application Wizard, create a new SDI application named eMailer. This is the example program that we will add email capabilities to, so add the two MAPI controls that we'll need—MAPISession and MAPIMessages—by selecting the Components item of the Visual Basic Project menu. Select the entry marked Microsoft MAPI Control 5.0 and click OK. Next, double-click each icon of these new controls in the toolbox, adding two new controls to our application: MAPISession1 and MAPIMessages1. Now we're ready to go; we will start by checking the user's email from eMailer.


Checking Email

Many applications are becoming Internet-aware even if they have little to do with the Internet. You can browse the Web from many word-processing programs, and we've already seen how to add a Web browser to our programs. In much the same way, we'll add email capabilities to our eMailer example. To start, let's let users check their email.

We'll use the MAPISession control. Add a new item, Check email, in eMailer's File menu. Use the Visual Basic Menu Editor and give the new item the name email:

Now click the new menu item to open its click event:

Private Sub email_Click()
End Sub


Using the MAPISession Control

We begin by setting the MAPISession control's DownLoadMail property to True so that we will download mail when we establish a MAPI session:

Private Sub email_Click()
—> MAPISession1.DownLoadMail = True
        .
        .
        .
End Sub

The MAPISession control has only two methods: SignOn and SignOff. These are the methods you use to connect to the computer's email system and from there to the Internet. To check the user's email, we need only establish an email session with the MAPISession control. If the user's computer is not currently connected to the Internet, the email system automatically makes the connection. All we have to do is to establish an email session with our MAPISession1 control:

Private Sub email_Click()
    MAPISession1.DownLoadMail = True
—> MAPISession1.SignOn
        .
        .
        .
End Sub

At this point, the connection is made to the Internet (if necessary) and the user's email (if any) is downloaded into the Inbox. If you want to fetch the messages from the Inbox and display them, you can use the MAPIMessages control, as we'll see later.

There is always the possibility of error when a user signs on to an ISP, so we check for errors using the Err keyword:

Private Sub email_Click()
    MAPISession1.DownLoadMail = True
    MAPISession1.SignOn
—> If Err <> 0 Then
—>     MsgBox "Logon Failure: " + Error$
—> End If
        .
        .
        .
End Sub

If there has been an error, we report that to the user and send the Visual Basic error message that we get from the Error$ function.

Now that the email has been downloaded into the Inbox, we quit the MAPI session using the MAPISession control's SignOff method:

Private Sub email_Click()
    MAPISession1.DownLoadMail = True
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
—> MAPISession1.SignOff
End Sub

It's as simple as that to check the user's email. It's also worth noting that although the MAPISession control has a UserName property and a Password property, the user usually sets them in the Internet Mail dialog box when setting up the email system, so it's not usually necessary to include them here. If the connection is refused, Microsoft Exchange will ask the user to enter the user name and password again in any case, and it stores that information.

Now let's use the MAPIMessages control to send email.


Sending Email

To send email, we'll add a new item to the File menu: Send email…. There is already a Send… item in the File menu for sending files:

We change that to Send email… in the Menu Editor:

Now let's add the ability to send email to our program. Click the Send email… item to open its click event:

Private Sub mnuFileSend_Click()
End Sub

We start by creating a MAPI session, as we did when we checked email:

Private Sub mnuFileSend_Click()
—> MAPISession1.SignOn
—> If Err <> 0 Then
—>     MsgBox "Logon Failure: " + Error$
—> End If
     .
     .
     .
End Sub

Now we want to compose a new email message and send it. To do that, we will use the MAPIMessage1 control; the MAPISession1 control is used to establish an email session and to automatically download new email, and the MAPIMessages1 control is used to work with individual messages, including examining the text in incoming messages and composing new messages. To connect the MAPIMessages control to our new MAPI session, we load the MAPI session ID into the MAPIMessages SessionID property. We get the session ID from the MAPISession control's SessionID property:

Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then

MsgBox "Logon Failure: " + Error$

End If
—> MAPIMessages1.SessionID = MAPISession1.SessionID
        .
        .
        .
End Sub

Now the MAPIMessages1 control is connected to our MAPI session.

The MAPIMessages control is an indexed control; its methods appear in Table 4.1, and its important properties appear in Table 4.2.

Table 4.1 MAPIMessages Control Email Operations

Do This | Method | Action Method Constant (Obsolete)
Get email from Inbox | Fetch | MESSAGE_FETCH
Send email with | Send | MESSAGE_SENDDLG
Compose box |  |
Send email | Send | MESSAGE_SEND
Save a message | Save | MESSAGE_SAVEMSG
Copy message for reply | Copy | MESSAGE_COPY
Compose email | Compose | MESSAGE_COMPOSE
Reply to a message | Reply | MESSAGE_REPLY
Reply to all messages | ReplyAll | MESSAGE_REPLYALL
Forward a message | Forward | MESSAGE_FORWARD
Delete a message | Delete | MESSAGE_DELETE
Show address book | Show | MESSAGE_SHOWADBOOK
Show message details | Show | MESSAGE_SHOWDETAILS
Resolve recipient name | ResolveName | MESSAGE_RESOLVENAME
Delete recipient | Delete | RECIPIENT_DELETE
Delete attachment | Delete | ATTACHMENT_DELETE

Table 4.2 MAPIMessages Control Email Properties

Property | Does This
Action Property | Obsolete. Performs actions now performed by methods.
AddressCaption | Sets caption of the address book.
AddressEditFieldCount | Sets which address book edit controls to display.
AddressLabel | Sets appearance of "To" edit control in address book.
AddressModifiable | Sets whether address book can be modified by user.
AttachmentCount | Gets total number of attachments for current message.
AttachmentIndex | Set currently indexed attachment.
AttachmentName | Sets the name of the currently indexed attachment.
AttachmentPathName | Sets full path name of the currently indexed attachment.
AttachmentPosition | Sets position of indexed attachment in the message body.
AttachmentType | Sets type of currently indexed attachment.
FetchSorted Property | Sets message order when creating message set.
MsgConversationID | Sets the conversation thread identification value.
MsgCount | Gets the total number of messages in message set.
MsgDateReceived | Gets date on which current indexed message was received.
MsgID | Gets string identifier of current message.
MsgIndex | Sets index number of current message.
MsgNoteText | Text of current message.
MsgOrigAddress | Gets email address of originator of current message.
MsgOrigDisplayName | Gets originator's name for current message.
MsgRead | True or False depending on whether message has been read.
MsgReceiptRequested | Indicates if return receipt is requested for message.
MsgSent | Indicates if the message has been sent to mail server.
MsgSubject | Message's subject.
MsgType | Sets type of current message.

To get the messages in the Inbox, we use the MAPIMessage method Fetch. This creates a message set in the MAPIMessages control, and you can find out how many messages are in this set using the control's MsgCount property. Then you set the MAPIMessages control's MsgIndex property to point to the various messages in the message set. When a message is selected, you can examine it (and display it to the user) by using the various properties of the MAPIMessages control, such as the MsgOrigDisplayName property, which gives the name of the sender, or originator, of the message. You can get the email's subject from the MsgSubject property, the text of the message from the MsgNoteText property, and the date it was received from the MsgDateReceived property. In this way, you can work with the email messages in the computer's Inbox.

The MAPIMessages control also has an Action property, and you can perform standard email operations by setting the Action property to predefined constants, as in Table 4.1. However, the Action property is now considered obsolete, and Microsoft recommends use of the MAPIMessages methods, such as Fetch, Compose, and so on.

When we compose a new email message to send, that message is clearly not part of a message set, because it doesn't yet exist. For that reason, we set the MAPIMessages1 control's MsgIndex to –1 (this is necessary when you want to compose a new message):

Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
    MAPIMessages1.SessionID = MAPISession1.SessionID
—> MAPIMessages1.MsgIndex = -1
        .
        .
        .
End Sub

To compose the new message, we use the MAPIMessages Compose method:

Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
    MAPIMessages1.SessionID = MAPISession1.SessionID
    MAPIMessages1.MsgIndex = -1
—> MAPIMessages1.Compose
        .
        .
        .
End Sub

To let the user compose and send an email message, we use the Send method. This method takes an optional parameter that we can set to True or False (the default). Setting it to True places the Compose dialog box on the screen; leaving it False hides that box. We'll set the parameter to True:

Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
    MAPIMessages1.SessionID = MAPISession1.SessionID
    MAPIMessages1.MsgIndex = -1
    MAPIMessages1.Compose
—> MAPIMessages1.Send True
        .
        .
        .
End Sub

The preceding code will place the Microsoft Exchange Compose dialog box on the screen:

The user then addresses and types the message and clicks the Send button, sending the message. All that's left is to sign off from the MAPI session using the MAPISession SignOff method:

Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
    MAPIMessages1.SessionID = MAPISession1.SessionID
    MAPIMessages1.MsgIndex = -1
    MAPIMessages1.Compose
    MAPIMessages1.Send True
—> MAPISession1.SignOff
End Sub

Run eMailer and select the Send email… item in the File menu. The Microsoft Exchange Compose dialog box appears, as shown in Figure 4.2, and you can use it to send email.

Figure 4.2 Composing and sending email.

At this point, we can send and receive email. Our final goal for eMailer is to let the user register automatically through email with the click of the mouse.


Using Email to Register a User

It's becoming common to let users register their software with the manufacturers online. One way of doing that is through email, where all the details are hidden from users; all they have to do is to click a menu item. Let's add that menu item and give it the caption Register Now and the name Register; place it in the eMailer Help menu like this:

Open the new menu item's click event handler:

Private Sub Register_Click()
End Sub

In this event handler, we'll compose our own email message and send it without the user's intervention. We begin by starting a new MAPI session as we have done before:

Private Sub Register_Click()
—> MAPISession1.SignOn
—> If Err <> 0 Then
—>     MsgBox "Logon Failure: " + Error$
—> End If
               .
               .
               .
End Sub

Next, we pass the MAPI session ID to the MAPIMessages control:

Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
  —>   MAPIMessages1.SessionID = MAPISession1.SessionID
               .
               .
               .
End Sub

Now we're ready to compose the new message; we start by setting MAPIMessages1's MsgIndex property to –1, as we did before, and calling the Compose method:

Private Sub Register_Click()
    MAPISession1.SignOn

If Err <> 0 Then

MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
—>     MAPIMessages1.MsgIndex = -1
—>     MAPIMessages1.Compose
                .
                .
                .
End Sub

At this point, we're ready to address our email message. We use the MAPIMessages RecipDisplayName and RecipAddress properties. The RecipAddress property holds the email address where we will send our message. The RecipDisplayName property holds the name of the recipient. We will use the same entry ("VBISoft@server.com") for both properties:

Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
—>     MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
—>     MAPIMessages1.RecipAddress = "VBISoft@server.com"
                .
                .
                .
End Sub

If the recipient's name appears in the Microsoft Exchange address book, you can have it converted to an email address using the AddressResolveUI property and the ResolveName method. (You can place entries in the address book by selecting the Address Book item of Microsoft Exchange's Tools menu.) We set these properties here (although they are not needed, because we have set the email address) to show how to use them. We set AddressResolveUI to True to have Microsoft Exchange resolve the name from the address book, and we call the ResolveName method:

Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
        MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
        MAPIMessages1.RecipAddress = "VBISoft@server.com"
 —>    MAPIMessages1.AddressResolveUI = True
 —>    MAPIMessages1.ResolveName
              .
              .
              .
End Sub

Now we set the subject line of our email to "Registering" by setting the MsgSubject property this way:

Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
        MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
        MAPIMessages1.RecipAddress = "VBISoft@server.com"
        MAPIMessages1.AddressResolveUI = True
        MAPIMessages1.ResolveName
  —>   MAPIMessages1.MsgSubject = "Registering"
              .
              .
              .
End Sub
When we get the email, the Subject line will read "Registering." Next, we set the text of the message using the MsgNoteText property. We set the text to "Normal Registration Message":
Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$

End If

MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
        MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
        MAPIMessages1.RecipAddress = "VBISoft@server.com"
        MAPIMessages1.AddressResolveUI = True
        MAPIMessages1.ResolveName
        MAPIMessages1.MsgSubject = "Registering"
  —>   MAPIMessages1.MsgNoteText = "Normal Registration Message"
              .
              .
              .
End Sub

The email message is ready to send. We use the Send method, also closing the MAPI session with SignOff:

Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
        MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
        MAPIMessages1.RecipAddress = "VBISoft@server.com"
        MAPIMessages1.AddressResolveUI = True
        MAPIMessages1.ResolveName
        MAPIMessages1.MsgSubject = "Registering"
        MAPIMessages1.MsgNoteText = "Normal Registration Message"
  —>   MAPIMessages1.Send
  —>   MAPISession1.SignOff
End Sub

And that's it—now we can send registration email with the click of the mouse. The code for our eMailer project appears in Listing 4.1.

Listing 4.1 (eMailer) frmMain.frm

VERSION 5.00
Object = "{F9043C88-F6F2-101A-A3C9-08002B2F49FB}#1.1#0"; "COMDLG32.OCX"
Object = "{6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.1#0"; "COMCTL32.OCX"
Object = "{48E59290-9880-11CF-9754-00AA00C00908}#1.0#0"; "MSINET.OCX"
Object = "{20C62CAE-15DA-101B-B9A8-444553540000}#1.1#0"; "MSMAPI32.OCX"
Begin VB.Form frmMain
   Caption         =   "eMailer"
   ClientHeight    =   3195
   ClientLeft      =   165
   ClientTop       =   735
   ClientWidth     =   4680
   LinkTopic       =   "Form1"
   ScaleHeight     =   3195
   ScaleWidth      =   4680
   StartUpPosition =   3  'Windows Default
   Begin ComctlLib.Toolbar tbToolBar
      Align           =   1  'Align Top
      Height          =   1080
      Left            =   0
      TabIndex        =   1
      Top             =   0
      Width           =   4680
      _ExtentX        =   8255
      _ExtentY        =   1905
      Appearance      =   1
      ImageList       =   "imlIcons"
      BeginProperty Buttons {7791BA41-E020-11CF-8E74-00A0C90F26F8}
         NumButtons      =   17
         BeginProperty Button1 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "New"
            Object.ToolTipText     =   "New"
            Object.Tag             =   ""
            ImageIndex      =   1
         EndProperty
         BeginProperty Button2 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Open"
            Object.ToolTipText     =   "Open"
            Object.Tag             =   ""
            ImageIndex      =   2
         EndProperty
         BeginProperty Button3 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Save"
            Object.ToolTipText     =   "Save"
            Object.Tag             =   ""
            ImageIndex      =   3

EndProperty

BeginProperty Button4 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   ""
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button5 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Print"
            Object.ToolTipText     =   "Print"
            Object.Tag             =   ""
            ImageIndex      =   4
         EndProperty
         BeginProperty Button6 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   ""
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button7 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Cut"
            Object.ToolTipText     =   "Cut"
            Object.Tag             =   ""
            ImageIndex      =   5
         EndProperty
         BeginProperty Button8 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Copy"
            Object.ToolTipText     =   "Copy"
            Object.Tag             =   ""
            ImageIndex      =   6
         EndProperty
         BeginProperty Button9 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Paste"
            Object.ToolTipText     =   "Paste"
            Object.Tag             =   ""
            ImageIndex      =   7
         EndProperty
         BeginProperty Button10 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   ""
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button11 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Bold"
            Object.ToolTipText     =   "Bold"
            Object.Tag             =   ""
            ImageIndex      =   8
         EndProperty
         BeginProperty Button12 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Italic"
            Object.ToolTipText     =   "Italic"
            Object.Tag             =   ""
            ImageIndex      =   9
         EndProperty
         BeginProperty Button13 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Underline"
            Object.ToolTipText     =   "Underline"
            Object.Tag             =   ""
            ImageIndex      =   10
         EndProperty
         BeginProperty Button14 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   ""
            Object.Tag             =   ""
            Style           =   3
         EndProperty
         BeginProperty Button15 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Left"
            Object.ToolTipText     =   "Left Justify"
            Object.Tag             =   ""
            ImageIndex      =   11
         EndProperty
         BeginProperty Button16 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Center"
            Object.ToolTipText     =   "Center"
            Object.Tag             =   ""
            ImageIndex      =   12
         EndProperty
         BeginProperty Button17 {7791BA43-E020-11CF-8E74-00A0C90F26F8}
            Key             =   "Right"
            Object.ToolTipText     =   "Right Justify"
            Object.Tag             =   ""
            ImageIndex      =   13
         EndProperty
      EndProperty
      MouseIcon       =   "frmMain.frx":0000
   End
   Begin ComctlLib.StatusBar sbStatusBar
      Align           =   2  'Align Bottom
      Height          =   270
      Left            =   0
      TabIndex        =   0
      Top             =   2925
      Width           =   4680
      _ExtentX        =   8255
      _ExtentY        =   476
      SimpleText      =   ""
      BeginProperty Panels {2C787A51-E01C-11CF-8E74-00A0C90F26F8}
         NumPanels       =   3
         BeginProperty Panel1 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}

AutoSize = 1

Object.Width           =   2619
            MinWidth        =   2540
            Text            =   "Status"
            TextSave        =   "Status"
            Key             =   ""
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel2 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   6
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "12/4/96"
            Key             =   ""
            Object.Tag             =   ""
         EndProperty
         BeginProperty Panel3 {2C787A53-E01C-11CF-8E74-00A0C90F26F8}
            Style           =   5
            AutoSize        =   2
            Object.Width           =   2540
            MinWidth        =   2540
            TextSave        =   "2:37 PM"
            Key             =   ""
            Object.Tag             =   ""
         EndProperty
      EndProperty
      BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
         Name            =   "MS Sans Serif"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      MouseIcon       =   "frmMain.frx":001C
   End
   Begin MSComDlg.CommonDialog dlgCommonDialog
      Left            =   1740
      Top             =   1350
      _ExtentX        =   847
      _ExtentY        =   847
      FontSize        =   1.89257e-37
   End
   Begin InetCtlsObjects.Inet Inet1
      Left            =   2040
      Top             =   1320
      _ExtentX        =   1005
      _ExtentY        =   1005
   End
   Begin MSMAPI.MAPISession MAPISession1
      Left            =   2040
      Top             =   1320
      _ExtentX        =   1005
      _ExtentY        =   1005
      DownloadMail    =   -1  'True
      LogonUI         =   -1  'True
      NewSession      =   0   'False
   End
   Begin MSMAPI.MAPIMessages MAPIMessages1
      Left            =   2040
      Top             =   1320
      _ExtentX        =   1005
      _ExtentY        =   1005
      AddressEditFieldCount=   1
      AddressModifiable=   0   'False
      AddressResolveUI=   0   'False
      FetchSorted     =   0   'False
      FetchUnreadOnly =   0   'False
   End
   Begin ComctlLib.ImageList imlIcons
      Left            =   1740
      Top             =   1350
      _ExtentX        =   1005
      _ExtentY        =   1005
      BackColor       =   -2147483643
      ImageWidth      =   16
      ImageHeight     =   16
      MaskColor       =   12632256
      BeginProperty Images {8556BCD1-E01E-11CF-8E74-00A0C90F26F8}
         NumListImages   =   13
         BeginProperty ListImage1 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0038
            Key             =   ""
         EndProperty
         BeginProperty ListImage2 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":038A
            Key             =   ""
         EndProperty
         BeginProperty ListImage3 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":06DC
            Key             =   ""
         EndProperty
         BeginProperty ListImage4 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0A2E
            Key             =   ""

EndProperty

BeginProperty ListImage5 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":0D80
            Key             =   ""
         EndProperty
         BeginProperty ListImage6 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":10D2
            Key             =   ""
         EndProperty
         BeginProperty ListImage7 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1424
            Key             =   ""
         EndProperty
         BeginProperty ListImage8 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1776
            Key             =   ""
         EndProperty
         BeginProperty ListImage9 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1AC8
            Key             =   ""
         EndProperty
         BeginProperty ListImage10 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":1E1A
            Key             =   ""
         EndProperty
         BeginProperty ListImage11 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":216C
            Key             =   ""
         EndProperty
         BeginProperty ListImage12 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":24BE
            Key             =   ""
         EndProperty
         BeginProperty ListImage13 {8556BCD3-E01E-11CF-8E74-00A0C90F26F8}
            Picture         =   "frmMain.frx":2810
            Key             =   ""
         EndProperty
      EndProperty
   End
   Begin VB.Menu mnuFile
      Caption         =   "&File"
      Begin VB.Menu mnuFileNew
         Caption         =   "&New"
         Shortcut        =   ^N
      End
      Begin VB.Menu mnuFileOpen
         Caption         =   "&Open"
         Shortcut        =   ^O
      End
      Begin VB.Menu mnuFileClose
         Caption         =   "&Close"
      End
      Begin VB.Menu mnuFileBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileSave
         Caption         =   "&Save"
         Shortcut        =   ^S
      End
      Begin VB.Menu mnuFileSaveAs
         Caption         =   "Save &As..."
      End
      Begin VB.Menu mnuFileSaveAll
         Caption         =   "Save A&ll"
      End
      Begin VB.Menu mnuFileBar2
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileProperties
         Caption         =   "Propert&ies"
      End
      Begin VB.Menu mnuFileBar3
         Caption         =   "-"
      End
      Begin VB.Menu mnuFilePageSetup
         Caption         =   "Page Set&up..."
      End
      Begin VB.Menu mnuFilePrintPreview
         Caption         =   "Print Pre&view"
      End
      Begin VB.Menu mnuFilePrint
         Caption         =   "&Print..."
         Shortcut        =   ^P
      End
      Begin VB.Menu mnuFileBar4
         Caption         =   "-"
      End
      Begin VB.Menu email
         Caption         =   "Check email"
      End
      Begin VB.Menu mnuFileSend
         Caption         =   "Sen&d email..."
      End
      Begin VB.Menu mnuFileBar5
         Caption         =   "-"
      End
      Begin VB.Menu mnuFileMRU

Caption = ""

Index           =   0
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   1
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   2
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileMRU
         Caption         =   ""
         Index           =   3
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileBar6
         Caption         =   "-"
         Visible         =   0   'False
      End
      Begin VB.Menu mnuFileExit
         Caption         =   "E&xit"
      End
   End
   Begin VB.Menu mnuEdit
      Caption         =   "&Edit"
      Begin VB.Menu mnuEditUndo
         Caption         =   "&Undo"
         Shortcut        =   ^Z
      End
      Begin VB.Menu mnuEditBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuEditCut
         Caption         =   "Cu&t"
         Shortcut        =   ^X
      End
      Begin VB.Menu mnuEditCopy
         Caption         =   "&Copy"
         Shortcut        =   ^C
      End
      Begin VB.Menu mnuEditPaste
         Caption         =   "&Paste"
         Shortcut        =   ^V
      End
      Begin VB.Menu mnuEditPasteSpecial
         Caption         =   "Paste &Special..."
      End
   End
   Begin VB.Menu mnuHelp
      Caption         =   "&Help"
      Begin VB.Menu mnuHelpContents
         Caption         =   "&Contents"
      End
      Begin VB.Menu mnuHelpSearch
         Caption         =   "&Search For Help On..."
      End
      Begin VB.Menu Register
         Caption         =   "Register Now"
      End
      Begin VB.Menu mnuHelpBar1
         Caption         =   "-"
      End
      Begin VB.Menu mnuHelpAbout
         Caption         =   "&About eMailer..."
      End
   End
End
Attribute VB_Name = "frmMain"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private Declare Function OSWinHelp% Lib "user32" Alias "WinHelpA" (ByVal hwnd&, ByVal HelpFile$, ByVal wCommand%, dwData As Any)
Private Sub email_Click()
    MAPISession1.DownLoadMail = True
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPISession1.SignOff
End Sub
Private Sub Form_Load()
    Me.Left = GetSetting(App.Title, "Settings", "MainLeft", 1000)
    Me.Top = GetSetting(App.Title, "Settings", "MainTop", 1000)
    Me.Width = GetSetting(App.Title, "Settings", "MainWidth", 6500)

Me.Height = GetSetting(App.Title, "Settings", "MainHeight", 6500)

End Sub
Private Sub Form_Unload(Cancel As Integer)
    If Me.WindowState <> vbMinimized Then
        SaveSetting App.Title, "Settings", "MainLeft", Me.Left
        SaveSetting App.Title, "Settings", "MainTop", Me.Top
        SaveSetting App.Title, "Settings", "MainWidth", Me.Width
        SaveSetting App.Title, "Settings", "MainHeight", Me.Height
    End If
End Sub
Private Sub mnuHelpAbout_Click()
    'To Do
    MsgBox "About Box Code goes here!"
End Sub
Private Sub Register_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
        MAPIMessages1.SessionID = MAPISession1.SessionID
        MAPIMessages1.MsgIndex = -1
        MAPIMessages1.Compose
        MAPIMessages1.RecipDisplayName = "VBISoft@server.com"
        MAPIMessages1.RecipAddress = "VBISoft@server.com"
        MAPIMessages1.AddressResolveUI = True
        MAPIMessages1.ResolveName
        MAPIMessages1.MsgSubject = "Registering"
        MAPIMessages1.MsgNoteText = "Normal Registration Message"
        MAPIMessages1.Send
        MAPISession1.SignOff
End Sub
Private Sub tbToolBar_ButtonClick(ByVal Button As ComctlLib.Button)
    Select Case Button.Key
        Case "New"
            mnuFileNew_Click
        Case "New"
            mnuFileNew_Click
        Case "Open"
            mnuFileOpen_Click
        Case "Save"
            mnuFileSave_Click
        Case "Print"
            mnuFilePrint_Click
        Case "Cut"
            mnuEditCut_Click
        Case "Copy"
            mnuEditCopy_Click
        Case "Paste"
            mnuEditPaste_Click
        Case "Bold"
            'To Do
            MsgBox "Bold Code goes here!"
        Case "Italic"
            'To Do
            MsgBox "Italic Code goes here!"
        Case "Underline"
            'To Do
            MsgBox "Underline Code goes here!"
        Case "Left"
            'To Do
            MsgBox "Left Code goes here!"
        Case "Center"
            'To Do
            MsgBox "Center Code goes here!"
        Case "Right"
            'To Do
            MsgBox "Right Code goes here!"
    End Select
End Sub
Private Sub mnuHelpContents_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to the
'user you can set the HelpFile for your application in the Project
'Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 3, 0)

If Err Then

MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuHelpSearch_Click()
    Dim nRet As Integer
    'if there is no helpfile for this project display a message to
    'the user you can set the HelpFile for your application in the
    'Project Properties dialog
    If Len(App.HelpFile) = 0 Then
        MsgBox "Unable to display Help Contents. There is no Help associated with this project.", vbInformation, Me.Caption
    Else
        On Error Resume Next
        nRet = OSWinHelp(Me.hwnd, App.HelpFile, 261, 0)
        If Err Then
            MsgBox Err.Description
        End If
    End If
End Sub
Private Sub mnuEditCopy_Click()
    'To Do
    MsgBox "Copy Code goes here!"
End Sub
Private Sub mnuEditCut_Click()
    'To Do
    MsgBox "Cut Code goes here!"
End Sub
Private Sub mnuEditPaste_Click()
    'To Do
    MsgBox "Paste Code goes here!"
End Sub
Private Sub mnuEditPasteSpecial_Click()
    'To Do
    MsgBox "Paste Special Code goes here!"
End Sub
Private Sub mnuEditUndo_Click()
    'To Do
    MsgBox "Undo Code goes here!"
End Sub
Private Sub mnuFileOpen_Click()
    Dim sFile As String
    With dlgCommonDialog
        'To Do
        'set the flags and attributes of the
        'common dialog control
        .Filter = "All Files (*.*)|*.*"
        .ShowOpen
        If Len(.filename) = 0 Then
            Exit Sub
        End If
        sFile = .filename
    End With
    'To Do
    'process the opened file
End Sub
Private Sub mnuFileClose_Click()
    'To Do
    MsgBox "Close Code goes here!"
End Sub
Private Sub mnuFileSave_Click()

'To Do

MsgBox "Save Code goes here!"
End Sub
Private Sub mnuFileSaveAs_Click()
    'To Do
    'Setup the common dialog control
    'prior to calling ShowSave
    dlgCommonDialog.ShowSave
End Sub
Private Sub mnuFileSaveAll_Click()
    'To Do
    MsgBox "Save All Code goes here!"
End Sub
Private Sub mnuFileProperties_Click()
    'To Do
    MsgBox "Properties Code goes here!"
End Sub
Private Sub mnuFilePageSetup_Click()
    dlgCommonDialog.ShowPrinter
End Sub
Private Sub mnuFilePrintPreview_Click()
    'To Do
    MsgBox "Print Preview Code goes here!"
End Sub
Private Sub mnuFilePrint_Click()
    'To Do
    MsgBox "Print Code goes here!"
End Sub
Private Sub mnuFileSend_Click()
    MAPISession1.SignOn
    If Err <> 0 Then
        MsgBox "Logon Failure: " + Error$
    End If
    MAPIMessages1.SessionID = MAPISession1.SessionID
    MAPIMessages1.MsgIndex = -1
    MAPIMessages1.Compose
    MAPIMessages1.Send True
    MAPISession1.SignOff
End Sub
Private Sub mnuFileMRU_Click(Index As Integer)
    'To Do
    MsgBox "MRU Code goes here!"
End Sub
Private Sub mnuFileExit_Click()
    'unload the form
    Unload Me
End Sub
Private Sub mnuFileNew_Click()
    'To Do
    MsgBox "New File Code goes here!"
End Sub

That's it for our eMailer project. As you can see, the email system that we have access to in Visual Basic is powerful, allowing us to compose, send, receive, and read email. In the next chapter, we'll turn to another powerful part of Visual Basic Internet programming: ActiveX controls.


Professional Active Server Pages

Published by
Wrox Press Ltd.
30 Lincoln Road
Olton, Birmingham, B27 6PA

Copyright © 1997 by Wrox Press

All rights reserved. No part of the contents of this book may be reproduced, stored in a retrieval system or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording or otherwise, without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

The author and publisher have made every effort in the preparation of this book to ensure the accuracy of the information. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Wrox Press nor its dealers or distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book.

ISBN 1-861000-72-3

Trademark Acknowledgements

Wrox has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Wrox cannot guarantee the accuracy of this information.

Credits

Authors
Alex Homer
Andrew Enfield
Christian Gross
Stephen Jakab
Bruce Hartwell
Darren Gill
Brian Francis
Richard Harrison

Technical Editors
Chris Ullman
Tim Briggs

Technical Reviewers
Andrew Enfield
Richard Harrison
Brian Francis
Alex Homer
Bob Beauchemin
Luigi Ghirardi
Mark Ross
Zor Gorelov
Ron Miller

Development Editors
David Maclean
Gina Mance

Cover/Design/Layout
Andrew Guillaume
Graham Butler

Copy Edit/Index
Wrox Team


Advanced Database Techniques

In the last chapter, we spent a lot of time looking at the basic structure of the Active Database Component. This was intentional because, while it isn't as complex as many other Microsoft database engines, it's important that you are comfortable with the way that it's used before we go on to look at more complicated techniques. As you've seen, ADO follows the example of earlier database systems in being built around a defined object structure. This means that working with it requires a structured approach, but it brings many benefits.

For example, we've see that there are several different ways of creating a Recordset, and directly updating the database. The ADO is very flexible, and a great deal more complicated that any of the other Server Components we looked at in earlier chapters. However, while the techniques we used in the previous chapter are fine for databases such as Microsoft Access, in the real world we often have to connect our Web site to one of the more commercially-oriented systems. This might be Microsoft's SQL Server, Oracle, Sybase, DB2 or any of the other enterprise-based systems.

We also need to investigate some of the more advanced ways that the Active Database Component can be used. Rather than the simple examples you saw in the previous chapter, you'll now see how some real-world problems can be solved. For example, we need to consider how our pages might affect the security of our data, and take steps to protect our database systems. We'll be putting all the techniques you saw in the previous chapter to good use, and combining them to produce examples that more closely resembles the kinds of situation that you are going to meet.

So, in this chapter, we'll be looking at:

  • How the Active Data Object links to different databases

  • Techniques for manipulating enterprise-oriented databases

  • Real-world problem-solving techniques with the Active Data Object

  • How we can maintain security when linking a database to the Web


Manipulating Enterprise Data

So far we have assumed that the data source used to supply data to our Web page was an Access database. However, Access is not designed to handle really large volumes of data, or high transaction rates from multiple users. As we begin to look at developing an Internet or Intranet site, we need to consider the potential number of simultaneous visitors we might have to cater for. Hundreds of users might visit a typical Internet site in any given day, potentially dozens at the same time. In this environment, Microsoft Access would quickly overheat. So let's take a step beyond Access, and look at how the ADO can be used with other sources of data, including enterprise database systems such as Microsoft SQL Server.


Expanded Data Access

The ADO is a collection of objects that expose a standard set of properties and methods that our applications can use when accessing data. Like many of its predecessors, such as DAO and RDO, ADP relies on an underlying layer of software to actually interact with a given data source. As we saw in the last chapter, OLE DB is this underlying layer.

OLE DB technology is being positioned as the cornerstone to Microsoft's component database architecture. It is a set of OLE interfaces that provide applications with a standard means of accessing data stored in various information sources. These standard interfaces support specific elements of the Database Management System's (DBMS) functionality that are appropriate to the data source, enabling it to share its data.

The benefits of component DBMS's can be seen in the success of the Open Database Connectivity (ODBC) database access interface. ODBC is provided as a means of accessing data from a diverse set of sources, using a standard series of functions and commands, the idea being that the programmer is shielded from having to code to each specific data source’s requirements, thus vastly increasing productivity.

OLE DB takes ODBC a step further, towards a truly standard means of accessing data from diverse sources. Whereas ODBC is designed around accessing relational data sources using Structured Query Language (SQL), OLE DB is focused on providing access to any data, anywhere. For example, an ODBC provider has just been released that provides access to NT 4, Novell version 3, and NDS directory services, all through OLE DB. And there are more to come.

In addition to simplifying the programmer’s job, the OLD DB interface layer provides the developer with a means of accessing data which may not be stored in a traditional DBMS format. As we all know, there is a large amount of mission-critical data stored in systems that are not classified as a DBMS. One of the most significant limitations of the ODBC approach to data access is that it is difficult for non-relational database vendors to support. Building a data provider layer means exposing the data via SQL. For a non-SQL data provider, such as an Excel file or a Mail system, this requires the equivalent of a SQL engine within the ODBC driver.

OLE DB simplifies the development of access methods for simple tabular data providers by only requiring them to implement the functionality native to their data source. At a minimum, an access provider must implement the interfaces necessary to expose data in a tabular form. This requirement allows for the development of query processor components, such as SQL query processors, that can work with tabular information from any provider that exposes its data through OLE DB. In essence then, OLE DB provides an interface layer that is consistent despite its having an underlying data structure that may be very diverse.

OLE DB and the ADO

As we have seen, OLE DB is a collection of components that work together to provide data access capabilities to an application. These components are loosely grouped into two classes, consumers and providers. Consumers are the components that submit requests for data. Providers service these requests by accessing the data sources, and retrieving the information requested. In these terms, ADO is an OLE DB consumer. In other words, it makes requests to a provider. The name of the provider that will service a request is supplied either as a part of a connection string, or as the Provider property of the Connection object.

In this example, we supply the name of the Microsoft ODBC provider for OLE DB and the ADO, MSDASQL, as a parameter to the Provider method of the Connection object.

Set DBConn = Server.CreateObject("ADODB.Connection")
DBConn.Provider ="MSDASQL"
DBConn.Open "DSN=SQLForum;UID=sa;PWD=;"
Set Session("DBConn") = DBConn

Be careful not to include a Provider Name as both a part of the connection string, and as the Provider property of the Connection object. If you do, the result is unpredictable.

Providers, like any other OLE object, must be defined in the registry before the OLE DB layer can use them. They are identified by the OLE DB Provider sub-key, under the class ID of the provider. Within the HKEY_CLASSES_ROOT key, providers must have the following sub-keys and values for the programmatic identifier (ProgID):

ProviderProgID = FriendlyDisplayName
ProviderProgID\CLSID = ProviderCLSID

The entries for the MSDASQL ODBC provider are show here—the CLSID value is a unique key that identifies the provider installed:

MSDASQL = Microsoft OLE DB Provider for ODBC Drivers
MSDASQL\CLSID = {c8b522cb-5cf3-11ce-ade5-00aa0044773d}

Under the HKEY_CLASSES_ROOT\CLSID sub-key, providers must have the following sub-keys and values:

ProviderCLSID = FriendlyDisplayName
ProviderCLSID\ProgID = ProviderProgID
ProviderCLSID\VersionIndependentProgID = VersionIndependentProgID
ProviderCLSID\InprocServer32 = ProviderDLLFilename
ProviderCLSID\InprocServer32\ThreadingModel = Apartment | Free | Both
ProviderCLSID\OLE DB Provider = Description

Again, the entries for the MSDASQL ODBC provider are:

{c8b522cb-5cf3-11ce-ade5-00aa0044773d} = MSDASQL
{c8b522cb-5cf3-11ce-ade5-00aa0044773d}\VersionIndependentProgID  = MSDASQL
{c8b522cb-5cf3-11ce-ade5-00aa0044773d}\InprocServer32 = MSDASQL.DLL
{c8b522cb-5cf3-11ce-ade5-00aa0044773d}\InprocServer32\ThreadingModel =
                                                                   ? Both
{c8b522cb-5cf3-11ce-ade5-00aa0044773d}\OLE DB Provider = Microsoft OLE DB
                                              ? Provider for ODBC Drivers

In addition, the ODBC provider entry references a second key that points to another OLE object, which is used for error processing. Other providers are identified in the registry in this same way. Thankfully, the installation of ADO provides all the registry entries required, but you'll find the information here useful if you need to delve into the registry yourself.

Connecting to a Data Source

Let’s take a moment to look at how we connect to various data sources. The ADO provides a number of ways of actually establishing a connection to a data source. Each has its advantages and disadvantages but, underneath the covers, each is limited by the ability of the provider to service the connection. As we walk through various examples, keep in mind that some of the features discussed may only be appropriate for the provider being used in that example.

The Connection Object

In the previous chapter, we reviewed how the Connection object is used to create a connection between an ASP page and a data source. Now let's look in more detail at the Connection object and at some of the more advanced options we can take use when connecting to and working with various data sources, including SQL Server.

The Connection object is the parent object in a hierarchy of several other objects including the Errors object, Command object and Recordset object. The Connection object can be used to create a new connection to a data source, by providing it with a connection string or connection information. This connection can then be referenced by each of the other objects in the hierarchy to interact with the described data source.

Set DBConn = Server.CreateObject("ADODB.Connection")
DBConn.Provider="MSDASQL"
DBConn.Open "DSN=SQLForum; UID=sa; PWD=; APP=Forum; WSID=MAINFRAME;
                                                  ? Database=Forum"
Set Session("DBConn") = DBConn

However, unlike the DAO and RDO libraries, the ADO doesn't require you to work your way down a hierarchy of objects in order to instantiate the one you actually require. The Command and Recordset objects can be called independently of any Connection object, allowing these objects to create a new Connection directly and interact with the database through it:

Set oRS = Server.CreateObject("ADODB.RecordSet")
oRS.Open "Select * from Message", "DSN=SQLForum;UID=sa;PW="
Response.Write "<B> Records Found: </B><P>"
Do While Not oRS.EOF
  For intCount = 0 to oRS.Fields.Count -1
    Response.Write oRS.Fields(intCount).Value & " - "
  Next
  Response.Write "<BR>"
  oRS.MoveNext
Loop

In this example, we create a simple listing of each record in the Message table. You will notice that no Connection object is explicitly created, we simply create a Recordset with the Open method of the Recordset, using an SQL query and a connection string. Behind the scenes, ADO creates a Connection object and associates it with the Recordset we just created. When the Recordset goes out of scope or is set to Nothing, the Connection object is released.

Let's take another example. Here we need to call an SQL Server Stored Procedure only once per session. Rather then opening a Connection object, and passing a reference to it to a Command object, we can take advantage of the Command object’s ability to create a Connection object for us automatically in the background:

Set oCmd = Server.CreateObject("ADODB.Command")
oCmd.ActiveConnection = "dsn=SQLForum;database=Forum;uid=sa;pwd=;"
oCmd.CommandText = "{call myproc}"
oCmd.Execute

In the above example, we supply a connection string to the Open method of the object. The connection string is passed directly to the underlying ODBC driver, which in turn uses it to attach to the specified data source. Here a connection object is created for temporary use and destroyed when the oCmd object variable goes out of scope, or is set to Nothing.


Connection Tips

When defining connections, here are a few simple guidelines that are useful to follow:

  • Generally you will want to use a connection variable instead of a connection string as the ActiveConnection for commands. By using a connection variable, you're calling an existing connection. If you use a string, as we saw above, you are creating a new connection.

  • Make sure you explicitly close a connection when it's no longer needed. Although a connection is closed when it goes out of scope, it's better to make sure it is closed when you think it is. You can proactively call Close and set the variable to Nothing when you know that you will no longer need a connection. This frees up resources before it actually goes out of scope.

When working with a SQL Server data source, consider these points:

  • With forward-scrolling, read-only cursors (also known as firehose cursors) against SQL Server, you will not be able to start a new transaction on that connection. This is because the connection is dealing with the cursor and needs to complete what it's doing (i.e. get to the end of the cursor and close it) before continuing.

  • In addition, it is a good idea to separate your forward-scrolling cursor code from your command execution code. You could get into a situation where the same SQL pages are used concurrently, for example a query such as SELECT * FROM TableX and an update to TableX. Without setting the CommandTimeout property, your could hang your system due to locking contention.


Connection Pooling

In each of the examples we've discussed, we've created a new Connection object for each session, and then done something with it. However, consider a site where the number of simultaneous users is in the hundreds. In this environment, creating a new Connection object for each and every user can be very resource intensive. To help alleviate this problem, ADO can take advantage of a new feature provided with ODBC 3.0 and ASP known as connection pooling. This is a resource manager for connections, maintaining the open state on frequently used connections, and thereby avoiding the need to continuously create new connections.


Enabling Connection Pooling

By default ASP does not take advantage of this ability. To turn this feature on you will have to update the Registry and manually adjust the appropriate key. This can seem a daunting task. However, it is really very straightforward and the performance benefits can be well worth the effort.

Note for IIS users: Unless you install Microsoft Windows NT Service Pack 2 (SP2) or later, using Microsoft Access with connection pooling enabled may cause system failures when shutting down IIS.

To edit the Registry, run the REGEDIT utility. You will need to modify the following registry entry:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\W3SVC\ASP\Parameters

Find the StartConnectionPool entry and change the value from 0 to 1. For example:

Once you have made this change, the ADO and ASP will take advantage of their ability to hold open frequently-used connections between sessions. However, we now need to consider whether, and to what extent, the data source we are attaching to can support connection pooling. If your data server is Microsoft SQL Server, you'll want to take into account which underlying network library is being used to support a connection. Let’s take a look at the changes that we should made to the SQL Server environment to support connection pooling with ADO.

SQL Server Performance and Stability with Connection Pooling

In order to take advantage of connection pooling when using SQL Server, the TCP/IP or Multi-Protocol network library must be used. In most cases, as we are developing an Internet or Intranet based solution, TCP/IP will be the default underlying network library in use at our site. However, to verify the library in use, use the SQL Server Client Configuration utility. Remember that even if the Web Server and SQL Server are running on the same physical system, the Web Server (and thus our ASP pages) is still a client of SQL Server.

The SQL Server Client Configuration utility is an easy-to-use GUI tool that displays the DB Library and NET Library defined for use on the machine, along with any DB LIB-defined names tied to a specific library. (DB LIB is the native database library supplied with SQL Server and Sybase). As ASP interacts with ODBC and its underlying SQL Server driver, no changes are required to the DB LIB settings. However, both DB LIB and ODBC communicate with SQL Server using an underlying Net Library driver.

To configure a client system (in our case the Web server) to use the TCP/IP Net Library when communicating with SQL Server, we click on the Net Library tab, and under Default Network select TCP/IP Sockets.


Creating a DSN 'On the Fly'

So far, each of our connections has assumed that we have previously defined a Data Source Name (DSN). The DSN describes a data source, and the standard settings to be used when attaching to it. However, this can be limiting in some circumstances—especially when attaching to data sources that are dynamic in nature.

For example, let’s take a situation where the user connects to several servers, depending on the information they need. We could setup an ODBC Data Source Name definition for each server and selectively attach to it. A better way is to create the connection 'on the fly', without using a DSN:

ServerName = Request.QueryString("ServerName")   'from the submitted page
DBConn.Open "Driver={SQL Server};Server=" & ServerName & _
    ";UID=sa;PWD=;WSID=MAINFRAME;Language=us_english;Database=Forum;DSN=;"
...

In this example we create a definition on the fly, using the server name from a form submitted by the user to determine which SQL Server to attach to.


Expanding the Connection Object

The Connection object is responsible for more than just the initialization of communication between the server and the application. It is also used to manage transactions, submit SQL statements directly to a provider, and set provider-specific attributes.

Let's look at an example of using several of the advanced features of the Connection object to update our database. Typically, when working with the ADO, you might assume that to update a record in a table we would open a Recordset, use AddNew to add a new record, update the fields in the Recordset and then Update the Recordset. This is how we did it in the previous chapter:

...
Set rsAddMessage = Server.CreateObject("ADODB.Recordset")
rsAddMessage.Open "Message", Conn, adOpenKeyset, adLockBatchOptimistic
rsAddMessage.AddNew
rsAddMessage.Fields("FromMsg") = strFrom
rsAddMessage.Fields("Email") = strEmail
rsAddMessage.Fields("Subject") = strSubject
rsAddMessage.Fields("Body") = strBody
rsAddMessage.Fields("WhenMsg") = CStr(Now())
rsAddMessage.Fields("MsgLevel") = intNewMsgLevel
rsAddMessage.Fields("PrevRef") = lngPrevRef
rsAddMessage.Fields("ThreadPos") = intNewThreadPos
rsAddMessage.UpdateBatch
rsAddMessage.Close
Set rsAddMessage = Nothing
...

However, consider the number of steps involved, as well as the potential server traffic issues that could arise, if the UpdateBatch method were not supported. In our example, we applied all of the required changes as a single transaction, using the UpdateBatch parameter, and thus reduced the amount of interaction that had to occur between the server and the application. However, your ADO access provider must support this facility in order for it to be of value.

Now, let's take a look at a piece of code that does the same thing, using a different tactic:

...
SQLQuery = "INSERT INTO Forum.dbo.Message " _
         & "( FromMsg, Email, Subject, Body, WhenMsg, MsgLevel, " _
         & " PrevRef, TopRef ) " _
         & " VALUES ( '" & strFrom & cDlm & strEmail & cDlm & strSubject _
         & cDlm & strBody & cDlm & CStr(Now()) & "', " & intNewMsgLevel
         & ", " & lngPrevRef & ", " & intNewThreadPos & " )"
oConn.Execute SQLQuery, lRecs
...

Here we build an INSERT statement to apply the data stored in a number of variables to the records. The SQL Statement is syntactically compatible with Transaction SQL (TSQL), the native SQL language of SQL Server.

The SQL statement is then submitted to the provider using the Execute method of the Connection object. Notice that an lRecs variable is supplied as a parameter to the Execute method call. This variable will contain a count of the number of records affected by this statement after it is submitted. It can be used to test for the successful completion of the statement.

One of the advantages of this approach is that it simply requires fewer steps. More importantly, it provides us with the ability to supply batches of SQL to the Server. In the above example, we use a single INSERT statement. Consider, however, the need for multiple inserts or updates—or perhaps additional data validation that might occur during, or as a part of, the submission.


Batch and Transaction Management

Perhaps the most compelling reason to use a batch SQL approach is the need for a finer level of transaction control. When using an Access database with a single user, transaction management is a minor concern at best. However, when multiple users are accessing a data source, with the potential of simultaneous updates, transaction management can have a huge impact on the performance and reliability of SQL Server.

In the previous example, the transaction is committed to the server—i.e. the changes are actually applied to the data—as soon as it is executed. In many cases this is perfectly acceptable. However, if we need a finer level of control over when a transaction is committed, either for performance reasons or due to dependencies between records, we can choose to define the beginning and the end of each transaction.

We looked briefly at transactions in the previous chapter. To define the beginning and end of transaction, we use the BeginTrans and CommitTrans methods of the Connection object:

Conn.BeginTrans
Conn.Execute SQLQuery, lRecs
Conn.CommitTrans

Here we explicitly define the beginning of a transaction. By doing so, the underlying ODBC provider will no longer automatically commit a transaction. To apply changes to a database, the Connection object's CommitTrans method is called. If an error occurs, the RollbackTrans method is available. RollbackTrans will 'undo' any changes made as a result of the SQL that has been submitted since the last CommitTrans was called.

Things to Consider When Using Transactions

To get the maximum performance when using transaction statements to process database records, we need to structure them well. The following is a list of guidelines that may be helpful as you experiment with transactions:

  • Keep the transaction processing blocks as short as possible. Remember that as long as a transaction is open on a series of records (especially in the case of an update), other people cannot access or change them. For example:

  • Conn.BeginTrans
  Statement1...
  Statement2...
  Statement3...
  Statement4...  '*** If you have a number of statements,
                 '*** you could slow processing and increase
                 '*** the chance of errors occurring.
Conn.CommitTrans  (or RollbackTrans)
  • When working with transaction statements, don’t create a new connection or perform further database processing until the transaction is complete. Jumping out of an open transaction can lead to contention and lockout conditions:

  • Conn.BeginTrans
  Statement1...
  Statement2...

  '*** Don’t create a new transaction within an open tranaction

  Set RS = Server.CreateObject("ADODB.RecordSet")
  RS.Open "select * from Message", DBConn, 3, 1, 2
  If NOT RS.EOF Then
    Statement3...
    Statement4...
  End if
Conn.CommitTrans  (or RollbackTrans)
  • When working with a Recordset object, don't refresh the object in the middle of a series of transactions. Doing so will create the "Attempt to Commit or Rollback without BeginTrans" error:

  • Conn.BeginTrans
  Statement1...
  If UserReset Then
    Rs.Refresh  '*** Can cause an error
  End If
  Statement2...
  Statement3...
Conn.CommitTrans  (or Rollback)
  • When using transactions to control server updates, don't execute a Close in the middle of a transaction. This can lead to problems when attempting to commit the transaction:

  • Conn.BeginTrans
  Statement1
  If Error
    db.Close  '*** Commit will fail after this
  End If
  Statement2...
  Statement3...
Conn.CommitTrans  (or Rollback)

As you can see, we can now specify when and under what conditions data is applied to our data server. In addition, and perhaps more importantly, we can undo these changes at any point in the process.


Connection Attributes

Depending on the Connection object's Attributes property, calling either the CommitTrans or RollbackTrans methods may automatically start a new transaction. If the Attributes property is set to adXactCommitRetaining, ADO automatically starts a new transaction after a CommitTrans call. If Attributes is set to adXactAbortRetaining, ADO automatically starts a new transaction after a RollbackTrans call.

Take care to note what state the driver is in. The ODBC provider for ADO does not support multiple simultaneous transactions within a single connection. As such, if we attempt a BeginTrans and the previous CommitTrans automatically created a new transaction, an error will result.

Note that once you start a transaction yourself, rather than having it started automatically, you will have to manually manage transactions until the connection is closed and reopened.


The Command Object

The Command object is used to obtain records and create Recordset objects, as well as to execute bulk operations or manipulate the structure of a database. Depending on the functionality the provider exposes; some collections, methods, and properties of a Command object may not be available. Refer to your provider's documentation to verify a command feature is available before attempting to use one.

In previous chapters we have taken a look at how we use the Command object. Now let's take a closer look at one of its most appealing capabilities, especially when working with SQL Server.


Using Stored Procedures

Stored procedures provide an alternative to executing batches of SQL statements in ADO. We looked briefly at how we can call a stored procedure earlier. Let's take a look at how to get values back out of a stored procedure. Here's a simple code sample showing how to call a stored procedure with a single output parameter.

Set cmd = Server.CreateObject("ADODB.Command")
oCmd.ActiveConnection = "dsn=SQLForum;database=pubs;uid=sa;pwd=;"
oCmd.CommandText = "{call recordcount(?)}"
'now specify parameter info
oCmd.Parameters.Append oCmd.CreateParameter("cnt", adInteger, adParamOutput)
oCmd.Execute
Response.Write "RecordCount = " & oCmd(0)

In this example, we have a stored procedure called recordcount, which accepts one integer parameter and returns the number of records in our table. Alternately we can use this approach with the same stored procedure:

Set oCon = Server.CreateObject("ADODB.Connection")
Set oCmd = Server.CreateObject("ADODB.Command")
oCon.Open "SQLForum", "sa", ""
Set oCmd.ActiveConnection = oCon
oCmd.CommandText = "{? = call recordcount}"
'now specify parameter info
oCmd(0).Direction = adParamReturnValue
oCmd.Execute
Response.Write "RecordCount = " & oCmd(0)


The Recordset Object

The Recordset object is the real heart of the ADO. It's the primary mechanism we use to interact with a database. In previous chapters we've introduced the Recordset, and seen what it has to offer. In this section, we'll consider some of the things we must take into account when creating Recordsets. Then we'll look at how we go about retrieving Binary Large Object (BLOB) data using a Recordset.

A Recordset is in essence a cursor. A cursor is a subset of a database, organized and sorted in accordance with an SQL query. Cursors are powerful tools within a relational database engine. They allows developers to retrieve a subset of data, position that data in tables and columns, and then navigate the result set—both forward and backward—updating and changing the data as required. The following are some things to consider when creating and working with Recordsets.


Tips for Working with Cursors

Some providers, such as SQL Server, implement a forward-scrolling, read-only (or 'firehose') cursor mode, meaning that they can efficiently retrieve data by keeping a connection open. When working with such providers, the connection could be blocked by another user's transaction. The following examples demonstrate scenarios that result in errors.

dbConn.Open "DSN=SQLForum;UID=sa;PWD=;"        'Example 1
dbConn.BeginTrans
RS.Open "SELECT * FROM Message", dbConn
Set dbCmd.ActiveConnection = dbConn
  • Example 1: The problem is that the command object's ActiveConnection is being set to a connection that is forward-scrolling and in 'firehose' mode. This is the same connection involved in the batch mode. The error from the provider will only appear in the Err object, and it will return as unspecified. For example, with the ODBC Provider, you will get "Unspecified error".

  • dbConn.Open "DSN=SQLForum;UID=sa;PWD=;"        'Example 2
RS.Open "SELECT * FROM Message", dbConn
dbConn.BeginTrans
  • Example 2: The problem here is that the connection is forward-scrolling and in firehose mode, so it cannot be put into transaction mode. The error returned in the Errors collection from the provider will indicate that it is operating in firehose mode, and can't work in transaction mode. For example, with the ODBC Provider against Microsoft SQL Server, you will get the error "Cannot start transaction while in firehose mode".

  • dbConn.Open "DSN=SQLForum;UID=sa;PWD=;"        'Example 3
RS.Open "SELECT * FROM Message", dbConn
Set dbCmd.ActiveConnection = dbConn
dbConn.BeginTrans
  • Example 3: The problem here is that the connection is in forward-scrolling firehose mode, so it cannot also be involved in a batch mode. The error returned in the Errors collection from the provider will indicate that the transaction could not be started. For example, with the ODBC Provider against Microsoft SQL Server, you will get the error "Cannot start transaction because more than one hdbc is in use".


Working with the CacheSize Property

When dealing with Cursors, especially those created by ODBC, it is important to control how much data is cached by ODBC in client memory. ASP assists in this area considerably, as the actual ADO processes are executed on the Web server. As such, cached data is stored in the server's memory, as opposed to the actual end-user or client machine. However, this can also have a downside as we scale up our server. If our default Recordset cache is large, and our server incurs a large volume of traffic, we can fast run into resource issues.

The CacheSize property is used to control how many records our underlying ODBC driver keeps in its memory buffer, and how many to retrieve at one time into local memory. For example if the CacheSize is set to 10, the ODBC driver retrieves the first ten records into a local cache as soon as a Recordset object is opened. As we navigate through the Recordset object, the ODBC driver then retrieves data from the data source and puts it into the cache as required, for example as soon as we move past the last record in the cache, it retrieves the next ten records from the data source.

Typically, for a read-only forward-scrolling Recordset, we only want to cache one record at a time, and CacheSize is set to 1. Because we don't need to move backward, and we're not changing the data, caching records on the client isn't efficient. Bringing them into memory as they are read is all that is needed.

However, when data is being updated, or we create a Recordset that supports forward and backward navigation, a larger CacheSize may be appropriate. In the case of updates, when the cache size is 1, the recordset will need to go back to the server for each changed record. With a recordset that allows backward as well as forward navigation, it has to go back to the server to retrieve the same data many times, especially as we move upwards (MovePrevious) in the recordset.

The value of the CacheSize property can be adjusted during the life of the Recordset object, but changing this value only affects the number of records in the cache after subsequent retrievals from the data source. To force the cache size to be adjusted immediately, we can Resync the Recordset. However, we cannot set the cache size to 0—if we do an error will result.


Binary Data

The Recordset is an extremely flexible object. In addition to working with data of various types, it supports the retrieval and manipulation of raw binary data stored in a database. This can be especially valuable if our database contains items such as graphical or sound data.

It's important to note that although BLOB (Binary Large Object) data can be retrieved from a database, it is not wise to store all of the graphical images on the Web site in the database. SQL Server, or Access for that matter, was not designed to process and work with this type of data. Typically it's far more efficient to store BLOB information as normally, as .gif or .wav files, along with our Web pages.

BLOB data manipulation comes in handy when we have graphics that are supplied from other sources, and are typically dynamic in nature. For example, a workflow imaging system may track document images across various users, and we may need to display some of these images within a dynamic Web page. To do this, the GetChunk method of the Field object is used. Let's take a look at an example:

ID = Request.QueryString("ID")
BlockSize = 4096
Response.ContentType = "image/JPEG"
strQuery = "SELECT * FROM Blob WHERE Blob_ID = " & ID
Set oRS = oConn.Execute(strQuery)      'oConn is a Session level object
oRS.MoveFirst
Set Field = oRS("Blob")
FileLength = Field.ActualSize
NumBlocks = FileLength \ BlockSize
LeftOver = FileLength Mod BlockSize
Response.BinaryWrite Field.GetChunk(LeftOver)
For intLoop = 1 To NumBlocks
  Response.BinaryWrite Field.GetChunk(BlockSize)
Next
oRS.Close

Make sure the BLOBs are the last things in your SELECT statement. Currently they don't work if they are not the last field(s).

In this example, a jpeg image has been stored in an Image field within our SQL Server database. We're retrieving it from the database, and displaying it on our Web page. SQL Server supports several binary data types. Two of these, Binary and VarBinary are limited to a maximum of 255 characters. In addition, SQL Server supports an Image type that stores data as 2KB increments of binary information. This type meets our need for this example.

Our first step is to obtain a key that references the binary object in which we are interested. Here, we will use an ID that is selected by the user on a previous page. In a real world scenario, the previous page would include a number of “thumb nail” pictures, that when selected would call this page and pass the appropriate ID for the image required.

A BlockSize value is used to determine how much data will be read from the data source at one time. Take care when setting this value. It is best to use a value that is a multiple of the field increment size to avoid an excessively large or small leftover chunk. It may seem logical to read the whole image as a single chunk, but keep in mind that we need to move this data from our server to our Web site in a single transaction, a process that can be very resource intensive with large files.

The next step is to define the type of data that will be displayed on the page. This is done using the ContentType property of the Response object. Then, with our initial values set, we can define a query to retrieve the data. Using the Execute method of the Connection object, we submit the query and return a Recordset containing the query results. To verify that the data set is populated, we call the MoveFirst method of the Recordset object.

Now we are ready to actually read the data from our Recordset. To do this we define an object variable representing the BLOB field. In this example we use:

Set Field = RS("BLOB")

The object variable Field now has a reference to the actual binary data. Our next step is to calculate how much data we need to retrieve in terms of the block size defined earlier. We do this by retrieving the actual size of the object (using the ActualSize property as you might guess), and then dividing that value by our block size variable to determine the number of chunks our data can retrieved in. As our block size may not be evenly devisable by the size of the image, a LeftOver value is calculated to determine any partial chunk of data that needs to be retrieved:

FileLength = Field.ActualSize
NumBlocks = FileLength \ BlockSize
LeftOver = FileLength Mod BlockSize

Using the BinaryWrite method of the Response object, we can output a chunk of the data read from the data source with the GetChunk method. GetChunk is design to read an unstructured binary data stream from an object of a given size. Here, we initially retrieve the extra bytes (if any), and then read blocks of data of the size defined, and write them to the Web page:

Response.BinaryWrite Field.GetChunk(LeftOver)
For i = 1 To NumBlocks
  Response.BinaryWrite Field.GetChunk(BlockSize)
Next


Using Recordset Filters

At the risk of sounding repetitive, a Recordset object works with a result set of data. We can think of a result set as a table that is a subset of the original table from which the Recordset retrieved its data. Consider the need to retrieve further subsets of this result data. We have two options—either create a new Recordset with the additional filtering criteria, or apply the filtering criteria to the existing record set.

Creating a new Recordset introduces a great deal of overhead and processing time, as well as the need to cache data that may be duplicated. To avoid this the Recordset object supplies a filtering method: Filter.

A Filter can be a string that provides filtering information to the Recordset, or an array of bookmarks on specific records in the record set. Keep in mind however, that the ADO must do the filtering work itself, and incur the processing overhead it entails. With a large recordset, this overhead can exceed the effort of simply creating a new recordset.

Let's take a look at how a string of filtering criteria might be applied. A criteria string is made up of values in the form FieldName Operator Value (for example, "LastName = 'Smith'"). We can create compound clauses by joining individual clauses with AND (for example, "LastName = 'Smith' AND FirstName = 'John'"). The following lists some guidelines for creating filter strings:

  • FieldName must be a valid field name from the Recordset. If the field name contains spaces, we must enclose the name in square brackets.

  • Operator must be one of the following: <, >, <=, >=, <>, =, LIKE.

  • Value is the value with which we will compare the field's values (for example, 'Smith', #8/24/95#, 12.345 or $50.00). Use single quotes with strings and hash signs (#) with dates. For numbers, we can use decimal points, dollar signs, and scientific notation. If Operator is LIKE, Value can include wildcards. Only the asterisk (*) and percent sign (%) wild cards are allowed, and they must be the last character in the string. Value may not be Null.

The filter property also allows us to supply a number of constants that effect the way in which the filter is applied to the data, and the results it produces. The constants that can be applied are:

Constant | Value | Description
adFilterNone | 0 | Removes the current filter and restores all records to view.
adFilterPendingRecords | 1 | Allows you to view only records that have changed but have not yet been sent to the server. Only applicable for the batch update mode.
adFilterAffectedRecords | 2 | Allows you to view only records affected by the last Delete, Resync, UpdateBatch, or CancelBatch call.
adFilterFetchedRecords | 3 | Allows you to view records in the current cache, that is, the results of the last fetch from the database.


Beyond Microsoft Access

In the previous sections, we've looked at the workings of OLE DB and the ADO in detail. It's time to put some of it into practice. We'll take an example database, and move it from Access to the SQL Server platform. This is a surprisingly straightforward process. With some thought as to the differences between the two platforms, and some good up-front design work, we can create and develop an ASP application using Access, then upsize it to SQL Server for use in an enterprise-oriented, high volume environment.


Upsizing to SQL Server

Since release 2.0 of MS Access, an add-on tool has been available from Microsoft that can greatly simplify moving an Access database into the SQL Server environment. This tool is called the Upsizing Wizard. It is available from a variety of sources, including Microsoft Office 97 Professional and the Microsoft Developer Network. In this example, we'll use the version supplied with Office 97.

Upsizing Wizard consists of two tools. The first is the wizard itself, which takes a Microsoft Access database and creates an equivalent database on SQL Server—with the same table structure, data and most (but not all) of the attributes of the original Microsoft Access database.

The second tool supplied is the SQL Server Browser. This allows the developer to view, create, and edit SQL Server objects, including tables, views, defaults, rules, stored procedures and triggers. SQL Server Browser can be used to manage both a SQL Server database created by the Upsizing Wizard, and any existing SQL Server objects.

Upsizing Design Issues

Before we can upsize an Access database to SQL Server, there are several important design issues we need to consider. Ideally, we would design our database from the very beginning with Web deployment in mind. As we'll see, the design of a database optimized for access from the Internet, or on an intranet, is very different to that of a typical single-user database. If you have an existing database that was not built with Web access in mind, you're likely to have to redesign certain aspects of it to take advantage of the upsizing process. We'll look at these aspects first.

Design Tips from the Client Side

Throughout this book, we're looking at how to build Active Server Pages. Let's take a moment to recap on some of the things we should take into account when designing a Web page that accesses data using ADO. These can have an enormous impact on the performance of our site.

  • Use Recordset objects of the static cursor type if the result set contains relatively few columns, doesn’t contain OLE Object or large Memo fields and when you don’t need to update the server tables. If you are simply presenting data, and multi-directional strolling is not required, use the adOpenForwardOnly cursor type. This makes a single pass through the request table to present the results, incurring a minimum overhead.

  • Minimize the number of items in server-populated combo boxes, list boxes and other selection controls. Use static cursor type Recordset objects to populate these controls wherever possible. Don't let the selection lists get too big—keep in mind that you have to pump all this data across to the user's Web browser. Anyway, from a design perspective, a selection list with too many items quickly becomes unwieldy.

  • Adhere to server-based naming restrictions from the beginning. Upsizing Wizard can correct many common errors in this regard, but not all of them. It's best to adhere to SQL Server restrictions from the beginning, in order to assure a painless migration. SQL Server field names must be 30 characters or less. The first character must be a letter or the symbol @. The remaining characters may be numbers, letters, or the symbols, $, # and _. No spaces are allowed.

  • Make sure you have an ODBC data source defined before you start the upsizing process. Upsizing Wizard requires you to log into an SQL Server database. If you are creating a new database, you should make sure you have an ODBC data source for the Master database for the SQL Server to which you want to upsize. If you are upsizing to an existing database, make sure you have an ODBC data source name for it set up first. You can create an ODBC data source name by running the ODBC Administrator—see Appendix G for details.

Design Tips from the Server Side

On SQL Server, we also need to take into account some basic design issues. Addressing these before we start the process can save us the difficulties associated with having to start again.

  • To make upsizing go as smoothly as possible, you should make sure that you have sufficient access permissions on the SQL Server you want to upsize to. The permissions you need will vary according to what you want to accomplish. At minimum, you must have CREATE TABLE permission. If you want to build a new database from scratch, you must have CREATE DATABASE permissions. Finally, if you want to create new devices, you must be a member of the Admin group.

  • Calculate how much disk space upsizing will require, by multiplying the size of your Microsoft Access database by two, and make sure you have enough free. This will ensure that Upsizing Wizard has enough space to upsize your database and leave it some room to grow as well. If you expect a lot of data to be added to the database, you should allow more space.

  • If your server has more than one physical hard disk, you may want to place your database on one disk, and the log for the database on a different disk. In the event of a disk failure, the likelihood of recovering will be much greater.


Upsizing the Sample 'Forum' Database

In the case studies in Part 3 of this book, we discuss a Forum application that allows you to create a discussion group on your Web site. The original was developed in Microsoft Access, with no regard for the problems that would arise from upsizing it to SQL Server. In this case, it's an ideal example of how we go about the process.

Upsizing Wizard allows us to create a new SQL Server database or if we have previously upsized our Access database (or just want to add Microsoft Access tables to an existing SQL Server database) we can upsize to an existing database. Typically, we will have created a database beforehand, as opposed to having the wizard create it. By creating the database up front, we are assured that it is created with the attributes and permissions required.

The next step is to install and start Upsizing wizard from the Access Tools | Add_Ins menu.

Here we will be using a database that has already been created. Clicking Next brings up a Data Source dialog requesting the name of the data source to upsize to. This list reflects the ODBC data sources defined on our system—in our case we select a Data Source Name (DSN) which we previously defined using the ODBC Administrator for the SQL Server we are upsizing to. If we hadn't already created the DSN, we can define it at this point.

Using the DSN we selected, the wizard will attempt to connect to the SQL Server specified. Once connected, it presents a list of the tables stored in the Access database. We simply select the tables we want to be upsized.

Now things get a bit more interesting. Although Access and SQL Server have many common characteristics, there are several significant differences in the way data is structured, and the indexes maintained. The wizard goes a long way in trying to simplify the conversion from Access to SQL Server, but we need to give it a bit of guidance. First, we need to decide what attributes that we want to upsize. The wizard can convert indexes, validation rules, Access defaults and relationships to SQL Server.

Indexes

Indexes in SQL Server and Microsoft Access are very similar. If we choose to have the Access indexes upsized, the wizard will convert primary keys to SQL Server clustered, unique indexes named aaaaa_PrimaryKey. In an Access database, a primary key does not have a user-assigned name. However, SQL Server requires a name for all objects, including the unique index created to represent the Access primary key.

All other indexes retain their names, except where they contain illegal characters—which are replaced with an underscore. Unique and non-unique Access indexes become unique and non-unique SQL Server indexes. SQL Server doesn’t support ascending or descending indexes, and so this attribute is ignored.

Default Attributes and Validation Rules

Default attributes are directly supported by the SQL Server catalog, and as such are ported directly between the two platforms. However, validation rules are by necessity treated differently. Validation rules and referential integrity, in Access, are part of the data dictionary and are enforced at the engine level. SQL Server validation rules and referential integrity are not part of the data dictionary, and are enforced through code bound to a table (stored procedures and triggers). This can lead to difficulties if a validation rule cannot be implemented as a stored procedure. The wizard is very good at making this conversion, but it is always a good idea to check the procedures generated for accuracy.

A data dictionary is a repository of information concerning the structures in the database.

Relationships

When we create a relationship between two Access tables, a new index on the foreign key in the relationship is created automatically. These system-generated indexes do not appear in the Access index editor. Access names these relationship-indexes Reference and, if the index name is not unique within a database, adds a suffix. Because an index named Reference could also be created by a user, Upsizing Wizard exports all indexes, and does not distinguish between system-generated and user-created ones. System-generated relationship indexes improve performance when tables are joined. However, if we end up with two identical indexes, one user-created and the other system-generated, we can drop one of them.

Relationships can also be enforced in SQL Server 6.5 through Declarative Referential Integrity (DRI) as opposed to triggers. Upsizing Wizard for Access 97 allows us to enforce the relationships defined in our Access tables using DRI in SQL Server. DRI can be easier to administer, better performing, and more flexible then referential integrity enforced through triggers. If you are upsizing to SQL Server 6.5 you, may want to consider this option.

For more information on SQL Server 6.5 and DRI see Professional SQL Server 6.5 Admin (ISBN 1-874416-49-4), published by Wrox Press.

Timestamp Fields

By default, Upsizing Wizard creates additional new columns, with the timestamp datatype, in SQL Server tables generated from Access tables that contain floating-point (single or double), memo or OLE fields. A timestamp field contains a unique value, generated by SQL Server, which is updated whenever that record is updated. Access uses the value in timestamp fields to see if a record has been changed before updating it.

Allowing Upsizing Wizard to create timestamp columns is most often used when Access Tables will be attached to the corresponding SQL Server. In this instance, Access will use these timestamp columns to determine if data in a table has changed, without having to scan the whole table. Upsizing Wizard can also attach the upsized tables to an Access database. This can be of value in a system where Access databases will provide local storage. In our case, this would not be practical.

When we reach the final screen of the Upsizing Wizard, it offers to create an Upsizing Report. This documents the objects that the wizard has created in SQL Server. It includes information about any databases that were created, as well as a complete explanation of how each Access object that was upsized maps to an SQL Server object. After upsizing is complete, we can view this report on screen, or print it for future reference.

Differences between Access and SQL Server

As we mentioned earlier, Access and SQL Server are not 100% compatible. These differences, as well as design decisions made by Upsizing Wizard, mean that much of the Microsoft Access data dictionary cannot be mapped directly to SQL Server constructs. The following table summarizes how objects are mapped from Microsoft Access objects to SQL Server:

Microsoft Access object | SQL Server object
Database | Database
Table | Table
Indexes | Indexes
Field | Field
Default | Default
Table validation rule | Update and Insert triggers
Field validation rule | Update and Insert triggers
Field Required property | Update and Insert triggers
Relations | Update, Insert and Delete triggers

Database and Table Objects

An Access mdb file maps directly to an SQL Server database. A Microsoft Access table, excluding much of its data dictionary, maps to an SQL Server table. Access maintains this information as a part of the mdb database. Upsizing Wizard replaces illegal characters with the _ symbol. Any names that are SQL Server keywords, for example FROM and GROUP, have the _ symbol appended to them, resulting in the names FROM_ and GROUP_.

However, this does not always avoid problems. For example, the Access Forum database has two column names that SQL Server cannot work with. Here's our Access table in design view:

Notice that one column has been named When and another From. These are both keywords in SQL Server. As such, the wizard attempts to create a table using:

CREATE TABLE TemporaryUpsizedTable
(
  ID int IDENTITY(1,1),
  Subject varchar (50),
  From_ varchar (50)  ,
  Email varchar (40)  ,
  Body text           ,
  When varchar (30)   ,
  MsgLevel smallint   ,
  PrevRef int         ,
  TopRef int          ,
  CONSTRAINT aaaaaOld2_Message_PK
    PRIMARY KEY NONCLUSTERED
    (When ,TopRef ,ID)
)

SQL Server rejects this syntax as invalid, and fails to create the table. To correct the problem, we will need to rename these tables to something that SQL Server can work with. For expediency sake, we can simply rename the columns FromMsg and WhenMsg. Of course, we then need to make sure that any dependent queries reflect the changes that we make.

Also, we need to keep in mind that the data types supported by SQL Server differ from those supported by Access. Upsizing Wizard will convert our Access data types to the nearest matching SQL Server types. The conversions are made as follows:

Access Data Type | SQL Server Data Type
Yes/No | Bit
Number (Byte) | Smallint
Number (Integer) | Smallint
Number (Long Integer) | Int
Number (Single) | Real
Number (Double) | Float
Currency | Money
Date/Time | Datetime
Counter | Int
Text(n) | varchar(n)
Memo | Text
OLE Object | Image

The Upsizing Report indicates whether the Upsizing Wizard was successful in converting all the field names and creating the new tables.


Using the SQL Server Browser

The second of the Upsizing Wizard's two tools is the SQL Server Browser. It's essentially a database container integrated into the Access development environment. It allows us to view, edit and modify SQL Server objects, including Tables, Views, Rules, Defaults and Stored Procedures. This allows the developer to take advantage of their understanding of Access when developing against a back-end database.

Just like Upsizing Wizard, the SQL Browser requires an ODBC definition to create the connection to SQL Server. It provides an "Access-like" view of SQL Server:

A developer familiar with Access should have no difficulty interacting with the SQL Server database using this utility. By using Upsizing Wizard and the SQL Server Browser, moving from an Access data source to a SQL Server data source can be relatively painless.


Database MAP Example

With our database now in SQL Server, let's take a look at some techniques we can use to access it. In this example we'll use many of the techniques we've already discussed to pull catalog data from our new data source. So far, we have seen how to get data from a data source using various objects. In this example we'll take a look at retrieving information concerning the data source itself.

The MAP Example is designed to present us with a listing of all the tables it finds in a SQL Server database we specify. We then have the option of selecting a table, and seeing its structure. The source code for this example is available for download with the other samples, from our Web site at: http://rapid.wrox.com/books/0723/.


The Login Page

To start things off, we are presented with a screen where we enter the information that will connect us to our SQL Server. This is SQLServerMAP.htm, and is created with a single form containing normal HTML TEXT controls and a SUBMIT button:

The Server name, along with a Database Name, Login ID and Password, are used to locate and attach to a SQL Server database. A straightforward HTML page produces this screen. When we click the Login button an ASP page List.asp is executed, which will actually attempt the connection.


The Table List Page

To start things off, we define a number of variables that will be used throughout this page:

Dim oServer
Dim sServerName
Dim sUid
Dim sDatabase
Dim sPassword

Of interest, as you will soon see, is the oServer variable. It will be used to hold a reference to the SQL Server OLE Object—in this first part of our sample, we have to take a step beyond ADO. Why? Well, we want catalog information concerning what structures are actually maintained by the SQL Server, and typically this information would be gathered from the ODBC provider.

However, at the time of this writing, the ODBC/ADO provider supplied for SQL Server does not include catalog level functionality. As such, we need to find an alternative method. The alternative is to create a reference to the SQL Server OLE Object directly. SQL Server 6.5 client utilities provide a number of OLE Objects that can be used to access SQL Server services. These OLE objects take advantage of the underlying SQL Server network libraries in order to communicate with the server.

First off, we use the same CreateObject method to create an instance of the SQL Server Object, sqlole.sqlserver, within ADO:

Set OServer = Server.CreateObject("sqlole.sqlserver")

With this reference created, we now have an access mechanism we can use to talk to a SQL Server. Before we can establish a connection, we will need the information supplied by the user on the main page. To do this we use:

sServerName = Trim(Request.Form("ServerName"))
sDatabase   = Trim(Request.Form("DatabaseName"))
sUid        = Trim(Request.Form("Login"))
sPassword   = Trim(Request.Form("Password"))

Then, with this information, we call the OLE Object's Connect method to establish a link to the server specified by the user:

OServer.Connect sServerName,sUid,sPassword

An important point to note here is that, because we are accessing the SQL Server via its OLE Object libraries, we are bypassing ODBC completely. Any previous ODBC definitions have no impact on this connection.

With a connection established, we are ready to retrieve the catalog data needed. In this case, we want a list of all the tables that exist for the selected database. This can be done using the catalog functions that are provided via the SQL OLE interface. First we display a confirmation of the login details, then retrieve the information we want by querying the Database object's Tables object. The name of each table found is used to populate a drop down list box:

<SELECT NAME="TableNames" SIZE="1">
<% Set oSQLdb = oServer.Databases(sDatabase)
   For Each oSQLTable In oSQLdb.Tables
      Response.Write "<option value=""" & oSQLTable.Name & """ > " _
                     & oSQLTable.Name & "</option>"
   Next %>
</SELECT>

And here's the result:

Now we can select a table on which to gather additional information, and click the Submit button to bring up our final ASP page Map.asp. This will display all of the fields in the selected table, along with their type, size, precision etc..

To do this, ADO does provide the functionality we need. To begin, we create a read-only, forward-scrolling Recordset:

Set oRS = Server.CreateObject("ADODB.recordset")
oRS.Open "SELECT * FROM " & sDatabase & ".dbo." _
         & Request.Form("TableNames"), "Driver={SQL Server};Server=" _
         & sServerName & ";uid=" & sUid & ";pwd=" & sPassword _
         & ";Database=" & sDatabase & ";DSN=;"

Note that in this instance, the use of a forward-scrolling Recordset is very important. If you remember earlier, we indicated that the default CacheSize setting for this type of cursor is one. As such, even though we are doing a SELECT * SQL command; only a single record will actually be retrieved. You may also notice that this recordset is created without using a DSN. This is because we do not know what the connection will be defined as until the user of the application provides us with a server name. We define the connection 'on the fly'.

With a cursor in place, we are ready to retrieve information about the fields in our table:

iRow = RS.Fields.Count
For iCount = 0 to (iRow - 1)
  Set Fld = RS.Fields(iCount)
  response.write "<TR>"
  response.write "<TD> " & Fld.Name & "</TD>"
  response.write "<TD> " & Fld.Type & "</TD>"
  response.write "<TD> " & Fld.ActualSize & "</TD>"
  If Int(Fld.Precision) >= 255 Then
    response.write "<TD> 0 </TD>"
  Else
    response.write "<TD> " & Int(Fld.Precision) & "</TD>"
  End If
  If Int(Fld.NumericScale) >= 255 Then
    response.write "<TD> 0 </TD>"
  Else
    response.write "<TD> " & Int(Fld.NumericScale) & "</TD>"
  End If
  response.write "</TR>"
Next

Here we use the number of Field objects in the data source's Fields collection to control how many cells are created in the output table. We then populate each cell with data from each Field object. The end result is a presentation of each Field, along with its attributes:


Some Components, Tips and Methods

Once you come to use ADO regularly, you'll find that there are lots of ways to achieve the same thing, and all kind of things that you can't easily achieve with other dynamic Web page technologies. Here are a few useful tips, which may help to expand your outlook, and make different kinds of task that bit easier. It's a mixed bag of components, add-ins, and methods.


The HTML Table Formatter Component

In our previous example, and in examples you saw in the last chapter, we've come up against the need to display data from a table in our Web pages. This is a regular requirement and is, of course, the main reason that HTML sports the <TABLE> tag and the wide range of formatting options it offers.

However, we still have to manually create the <TR></TR> and <TD></TD> tags to get the layout right. There are tools that help to do this, but a sample component that is now available from Microsoft can do it all automatically. You can get the component from: http://www.microsoft.com/iis/usingiis/developing/samples

It's written in Java, so you'll also need to install the latest Java Virtual Machine (VM) from Microsoft, as well as the component. Because it's a sample, rather than part of ASP, you need to install it separately, but full instructions are included in the distribution file.

So what can it do? Well, it has four properties, and one method. The best way to learn about it is to see how we use it:

' Create a connection, and open it using the System DSN 'MyDatabase'
Set oConn = Server.CreateObject("ADODB.Connection")
oConn.Open "MyDatabase"

' Create a recordset containing the records from the table 'MyTable'
Set oRs = oConn.Execute("MyTable")

' Create an instance of the HTML Table Formatter Component
Set oTblfmt = Server.CreateObject("IISSample.HTMLTable")

' Now set its properties for the borders, caption, style and headings
oTblFmt.Borders = False
oTblFmt.Caption = "Displaying my data"
oTblFmt.CaptionStyle = "ALIGN=CENTER VALIGN=BOTTON"
oTblFmt.HeadingRow = True

' And insert the formatted table into the page
oTblFmt.AutoFormat(oRs)

If you are into creating components, you'll find the download worth it just to see how it's done—all the source code is included. And it certainly saves a lot of coding, and keeps the actual size of the page down as well!


Converting from IDC to ASP--IDC2ASP

Converting from IDC to ASP—IDC2ASP

If you've already got a Web site or corporate Intranet bulging with data that is supplied via the Internet Data Connector (IDC) there is help available. IDC is an interface supplied with IIS that allows quite complex data retrieval from ODBC data sources, for use in a Web page. We discussed it briefly in Chapter 1, and we have no intention of covering it in depth. ASP is not only more powerful and efficient, but is also a lot easier to work with as well.

However, that doesn’t help you to convert existing IDC scripts and templates into ASP format. What does help, is the IDC2ASP tool that is available free of charge from Microsoft. It can be downloaded from http://www.microsoft.com/iis/usingiis/developing/samples, and comes in two versions:

IDC2ASP.EXE is a command-line utility that will convert IDC files into ASP form automatically. There are a series of command-line switches available that control all kinds of aspects of the conversion, and it leaves the original files intact so that you can test the new ones before committing yourself fully to the new technology.

Alternatively, there is an active server component version of IDC2ASP, which can be instantiated within an ASP page, just like any other component. All that's needed then is to set its properties and call the Convert method.

IDC2ASP carries the IntraActive name, and is not supported by Microsoft. However its potential usefulness makes it worth a try if you have a lot of IDC files to convert.


Using Arrays and the GetRows Method

We'll often find ourselves wanting to repetitively scan through a recordset searching for different sets of data. This can easily be achieved using dynamic cursor recordsets or multiple SQL queries, but both are less that ideal in the efficiency stakes. There is a far more efficient method at our disposal. ADO provides us with a method that enables us to easily convert a Recordset object into an array—which can then be scanned efficiently using ordinary code. This is the syntax:

variantarray = recordset.GetRows(NumberOfRows)

The GetRows method takes a Recordset object and converts it into the equivalent multi-dimensioned array. Just as we'd expect, a recordset consisting of three fields and ten records is converted to a 3 x 10 array—think of the first dimension being horizontal and the second vertical, just like a normal table is displayed in Access or SQL Server. The NumberOfRows argument is optional, and specifies the number of records to retrieve. This argument is frequently used on ordered recordsets, where the record ordering has been set by a defined table index, or an ORDER BY clause in the SQL query. You'll see the GetRows method used in the Forum case study chapter later in the book.


Using Multiple Recordsets

One other nice method available for the Recordset object is NextRecordset. So far, we've only talked about a CommandText property as containing one SQL query. We can provide multiple SQL queries as long as they are separated by semicolons, in the form SELECT * FROM Table1; SELECT * FROM Table2; etc.. Normally, when we open a recordset from a query like this, it's only the first set of results that are available. To retrieve the next set of records we just call the NextRecordset method. We can also supply the name of a variable that will be set to the number of records affected by the query, though this is optional:

Set recordset = recordset.NextRecordset(RecordsAffected)

When we use the NextRecordset method, we're provided with a new Recordset variable that is one of the following:

  • If a row-returning command returns no records, the Recordset object referenced by the variable will be empty. We can test for this case by inspecting the BOF and EOF properties, which will both be True.

  • If a non row-returning command completes successfully, the Recordset referenced by the variable will be closed. We can test for this case by ensuring that the Recordset object variable is not equal to Nothing. In this case, inspecting the EOF property will generate an error.

  • When there are no more statements, the Recordset variable returned will be set to Nothing.

Note that the NextRecordset method will continue to return Recordset objects as long as there are additional SQL statements in our CommandText parameter.


ADO and OLE-DB in the Real World

In response to the increased capacity of personal computer software tools, and their extended accessibility to external and remote data, businesses have a growing need to provide solutions that span desktop, midrange, mainframe, and Internet technologies. The expanding diversity in structure and complexity of business data creates the need for a universal data access middleware that enables a new class of solutions to be easily built and managed.

ADO can bridge the gap. As we've discussed in these two chapters, ADO is a higher level interface to OLE-DB. It provides the programmer with a very usable interface both for client server, Internet and intranet applications—allowing standardized access to almost any data provider. This doesn't just mean Active Server Pages programming. ADO can provide a translation layer for use with other tools and applications as well.

We haven't covered the real core of OLE-DB in these two chapters because, as an ASP programmer, you really don’t need to know what's happening 'over the fence'. However, we have included an Appendix in the reference section that describes OLE-DB in more depth, and discusses a middleware tool from ISG called Navigator. This can accomplish the real-world requirements of connecting your Active Server Pages code to a range of database systems and other data providers, including legacy systems. Check out Appendix J for more information.


ADO and SQL Server Security

Security is a growing concern in the Internet/intranet development community. It is a constant trade off between access to services and data, and protection of those services and data. This is really the heart of the security dilemma—if your site is too secure it could lose its value. On the other hand, without appropriate security measures, you might find someone corrupting your database for you, or selling your company's secrets to your competitors.

Security issues can be addressed at three different levels—the features offered by the Web server, those offered by the operating system, and those offered by the data source being accessed. In the Microsoft world, the operating system and the Web server are tightly coupled, as are the security features they offer.

Internet Information Server (IIS) supports advanced security facilities, such as a Secure Sockets Layer (SSL) which provides a security scheme for bulk-encrypting data between the server and its clients, when private communication is required. In addition, IIS completely integrates with the object-level and user-level security services provided by Windows NT. This can be used to protect a specific area of your site, such as DSN definition files, to which you don't want people to have access. In Chapter 9, we'll be looking at Web site security in a lot more detail.

However, as powerful as these facilities are, they will generally need to be supplemented with additional security measures that can be used to protect the data accessed by your site. In this section we will take a brief look at SQL Server security, and see what features and capabilities are available in this product. Then we will apply these features to accessing a SQL Server from an ASP page. Finally, we will take a look at a few simple steps you can take to protect your site and its data.


Security and Data Access

The level of security actually required is dependent on the level of access needed by our clients. If our database is servicing a small group of individuals, with information that requires minimal protection, then a very open security framework could be put into place. Essentially, we just need to allow our users to access the data they need, while protecting them from accidentally destroying or damaging it. However, if our database is servicing a public forum, much more stringent controls must be built. Not only do we need to protect the data from accidental damage, we need to verify that those accessing it have the right to do so, as well as protect it against those that do not—or who might intentionally damage it.

SQL Server supports several powerful security features that can be applied to each of these scenarios. SQL Server security options determine a server's login security mode, what auditing is done, and what objects, resources and data a user has access to. In the next section, we will take a high-level look at some of the security features offered by SQL Server.

Login Security

SQL Server 6.5 offers three login security modes. The mode we choose will significantly effect how the server handles security. Let's take a look at each of the options available, and how these options affect our environment:

  • Standard Security Mode: This is the default security mode. In standard mode, SQL Server manages its own login validation process for all connections (except client applications that explicitly request integrated security over trusted connections).

  • Windows NT Integrated Security Mode: This mode uses Windows NT authentication mechanisms for all connections. Only trusted connections are allowed into SQL Server. SQL Server always ignores the login name and SQL Server password submitted in the login request from an Open Database Connectivity (ODBC) client application. Network users, who were assigned user-level privileges to SQL Server, log in using their network username or the default login ID (if their network username is not found in syslogins).

  • Mixed Security Mode: This mode allows both trusted and non-trusted connections, and is a combination of integrated and standard modes. For trusted connections, SQL Server examines the requested login name as specified by the client ODBC application. If this login name matches the user's network username, or if the login name is null or spaces, SQL Server first tries the Windows NT integrated login rules as described above. If this fails, SQL Server uses the standard rules. If the requested login name is any other value, the user must supply the correct SQL Server password, and SQL Server handles the login using the standard rules described above. All login requests from non-trusted connections are handled using the standard rules.

Integrated and mixed security modes are best utilized in an intranet environment. In this environment, user IDs and privileges are defined in the Windows NT domain, and subject to domain security. The user ID can be retrieved from the client system directly, and authenticated against the domain master list. Here, the user is never required to provide authentication information.

In the big wide world of the Internet, a domain-based authentication model would not be realistic. Here, security must be tightly controlled and access provided only as is necessary.

User Groups

Creating a Login is just the first level of security associated with SQL Server. A Login ID does not permit the user to access any of the objects in a database. Access to a database and the objects within it are granted to individual users or groups of users.

SQL Server security is based on a detailed hierarchy of groups, which include users. Both groups and users are defined as having specific access to, and control over, services and data. In SQL Server, permissions for services and data can be controlled at a very granular level. For example, access to an individual object can be controlled, and then the actions that are possible on that object can also be regulated.

A group is simply a means of organizing the users of a database. Permissions are assigned to the group, as opposed to individual users. Users in the group have access to any resources available to the group as a whole. This simplifies the administration of users and objects in an SQL Server environment.

There is a built-in group, public, in every database. Each user automatically belongs to public and can be added to only one other group. A user cannot be remove from the public group. If a group is deleted (or 'dropped'), all users in that group are automatically removed from the group. However, dropping a group does not drop its users. Users who were members of the dropped group are still valid users in the database and members of the public group.

Database and Object Owners

SQL Server is organized around databases. Each one contains objects, such as tables, stored procedures, rules etc.. Each object has an owner, who has full authority over that object. SQL Server recognizes two types of owner—the database owner (DBO) and database object owner.

The database owner is the creator of a database, and has full privileges over it. However, beyond simply having the ability to manipulate the object itself, the DBO has the option of granting access to the database to other users or groups. In summary, the DBO can:

  • Allow users access to the database.

  • Grant users permission to create objects and execute commands within the database.

  • Set up groups

  • Assign users to groups and add guest accounts, which give users limited access to a database.

Just like any other user, the database owner logs into SQL Server by using an assigned login ID and password. In their own database, the user is recognized as DBO; in databases which they haven't created, the user is just known by their database username.

As we said earlier, a database contains objects. The user who creates a database object is the database object owner for that object. In order for a user to create an object within a database, the database owner must first grant that user permission to create that particular type of object. Just as the database owner can grant permissions for their database to other users, the object owner can grant permissions for their object.

Database object owners have no special login IDs or passwords. The creator of an object is automatically granted all permissions to it. An object owner must explicitly grant permissions to other users before they can access the object. Even the database owner cannot use an object unless the object owner has granted the appropriate permission.

As you can see, database and database object privileges are assigned at a very detailed level. Let's take a look at what privileges (referred to as permissions) can be granted to users and groups.

Security Permissions

SQL Server has two categories of permissions: object and statement. Some statement permissions (for the SELECT, UPDATE, INSERT, DELETE and EXECUTE statements) are handled as object permissions because these statements always apply to database objects that are in the current database.

Object permissions regulate the use of certain statements on certain database objects. They are granted and revoked by the owner of the object. Object permissions apply to the following statements and objects:

Statement | Object
SELECT | Table, view, columns
UPDATE | Table, view, columns
INSERT | Table, view
DELETE | Table, view
REFERENCE | Table
EXECUTE | Stored procedure

Statement permissions are not object-specific. They can be granted only by the system administrator (often referred to as the sa) or the database owner. Statement permissions allow the user to create new objects within a database. The following are examples of these statements:

  • CREATE DATABASE (can be granted only by sa, and only to users in the master database)

  • CREATE DEFAULT

  • CREATE PROCEDURE

  • CREATE RULE

  • CREATE TABLE

  • CREATE VIEW

Each database has its own independent permissions system. In other words, being granted permission to perform a given task in one database has no effect in other databases.

Now that we have had a chance to take a brief look at SQL Server Security, let's look at a few tips that will help us design our next database.


Security Tips

When designing a database in the SQL Server environment, which will be accessed by public sources, here are a few guidelines to keep in mind:

  • Take advantage of the Guest default login. The Guest login is a special login defined by default in SQL Server. Guest has no rights to any object in the server aside from the ability to login. The Guest login can be granted read access to the specific views, tables, or objects needed for your Web site. The Web site can then use this account when attaching to SQL Server objects, minimizing your site's exposure.

  • Use stored procedures where possible for updates and insertions. Coding static SQL in your ASP page is perhaps the most straightforward way to provide dynamic data content. However, keep in mind that not only can your Web site see this code, but so might a really clever hacker. To minimize this exposure it is often better to call a stored procedure, which in turn performs the required updates or inserts. A stored procedure can be granted permissions to access and modify objects and data that the ID calling the stored procedure cannot. This provides a shield between your site and the actual data being changed.

  • When tables are scanned for data, views can serve as security mechanisms. Through a view, users can query only the data provided by the view. The rest of the database is neither visible nor accessible. Permission to access the subset of data in a view must be granted or revoked, regardless of the set of permissions in force on the view’s underlying tables. Data in an underlying table that is not included in the view is hidden from users who are authorized to access the view but not the underlying table.

Don't expose more then you have to. In most cases, you will want to retain the majority of your data behind the 'firewall', and supply a mechanism to access this data. Always keep in mind that the any security can be compromised and, as such, preventative measures must be in place that assume that this will occur. One recommendation is to store only high-volume transactional data on the SQL Server that is directly accessed from a Web site. Supportive information can be maintained on a separate secured system, and retrieved as needed using Remote Stored Procedures, or other similar technologies.


Summary

In this chapter we have had the opportunity to review a wide variety of techniques and technologies that we can bring to bear when developing Active Server Web sites. ASP is a very powerful tool. Its ability to access data via ADO only scratches the surface of the many features that are made available by this exiting new product. The issues that we have addressed here provide you with a good introduction into the many features and capabilities of ASP and the ADO.

The main points of this chapter are:

  • Connecting to a data source: In this section we examined the hows and whys of connecting to a data source, and then went beyond the basics to take a look at the flexibility built into the Connection object.

  • With an understanding of the Connection object under our belts, we dug into the workings of ADO, looking at expanding the Recordset object and accessing binary data.

  • With the fundamentals behind us, we took a look at moving our Access database into the SQL Server environment using the upsizing tools available for Access.

  • With a lot of information behind us, we took a look at a 'real' example of how to make some of this stuff to work for us. The MAP example goes outside ADO, and walks through pulling more then just data from the database.

  • In the end, we had to consider the nature of things on the wild and woolly Internet. A brief introduction to SQL Server security provided some insight as to how we can protect our data.

With this chapter, we've reached the end of Part 1, and the introduction to the basics of Active Server Pages, Active Server Components and, in particular, the ActiveX Data Object. We've come a long way in these five chapters, but by now we have all the information we need to put ASP to work on our Web sites.

ASP isn't, however, just a Web site tool for creating dynamic pages. As you've seen in some of the examples, it allows us to start building real client/server applications in an environment (i.e. HTTP and TCP/IP) which was never really designed with this task in mind. This is the subject for Part 2 of this book.


Part 2 - Building Active Server Applications

In the first part of this book, we've been learning about Active Server Pages almost as though it was a technology on it's own. While we discovered that it could achieve some very clever effects with our Web pages, and do a lot of server-based information processing, we only ever sent what were effectively static pages back to the client's browser. In other words, all we were really doing was churning out text and HTML code—to the browser, it seemed like it was talking to a traditional Web site full of normal, static pages.

Of course, this is a good thing in many ways. It means that we minimize any compatibility concerns at the client's end, because we can produce pages to a lowest common denominator standard. Almost any browser can understand them. For a site that is basically in the business of supplying static information, this is an ideal scenario.

But, today, we generally need to do a lot more than that. Our visitors expect 'cool', dynamic, exciting pages—especially if they're just surfing rather than seeking particular nuggets of information. This means we need to use other techniques, not just Active Server Pages. Once the page has been sent to the client, ASP on its own can do nothing to influence the content.

The other topic that we must consider is how we can combine all our pages into a working application. Again, on a site that just needs to supply static information, this may not be a priority, though it can make a visitor's experience much nicer. In many cases, and especially on the corporate Intranet, this is an absolute necessity.

In this part of the book, we move on from focusing solely on Active Server Pages to consider how we can combine it with other techniques to create these interactive applications. We're into the world of client/server now, so stand up and feel proud—no longer are you just a Webmaster, you're a client/server developer!


Getting Into Client/Server

Up to now, our discussion of Active Server Pages has centered on the application of the specific technologies within the Active Server framework. In Chapters 1 and 2 we discussed what Active Server Pages is, and learned about its object model and the ways we can program our pages in this context. In Chapter 3, we discovered Server Components, which can make our life easier by providing plug-in objects that will perform specific tasks. Then, in Chapters 4 and 5, we discussed how we can integrate databases with our pages using the Active Database Component.

This chapter begins a broader exploration of Active Server Pages. Now that you are familiar with the individual techniques, we'll move on to look at how they can be combined with existing technologies available on the client's browser—and implemented from a client/server point of view.

Developing a client/sever application is never a simple task. While many of the benefits of the Web and the Active Server platform are universal access, richer content, and easier creation, ASP does not trivialize the process. What it does is simplify the distribution of server code and client code. In this part of the book, we'll learn how to start building client server applications that can be used in mission critical situations.

But first, we need to understand the background to client/server as a whole. If you are coming to ASP from a Web site developer's point of view, the term client/server may well carry threatening overtones. So we'll use this chapter to explain what client/server actually is, where it comes from, and how you can easily get into it using Active Server Pages.

Specifically, we'll be looking at:

Where client/server came from, and where it's going.

How client/server can be applied to Active Server Pages.

How we go about designing a client/server application.

How we integrate server-side components and database connections.

So, to start the ball rolling, just what are client/server applications?


What Is Client/Server?

In previous chapters, we've used the term client regularly enough, and it's pretty reasonable to assume you wouldn't have got this far if you didn't know what a server was. However, as any good cook will tell you, you can't always define the results of something in terms of its constituents. Just because my mother can take some flour, water, and a few other ingredients, and turn out a wonderful sponge cake, it doesn't mean that my desperate attempts at stirring them together and incinerating them in an oven will produce anything remotely edible.

So, it's all about the subtler ways that we combine the ingredients of our applications, that make them greater than the sum of their parts—the cream buns in the World Wide Web delicatessen. Our ingredients may be just lumps of hi-tech electronic equipment, and some files full of text, HTML, and code—but the combination can produce remarkably smooth and dynamic client/server applications. By the end of this book, you'll be in a position to receive your own Web site 'chef of the year' award.


The Background to Client/Server

So where did client/server come from? To answer this, and give you some background that will help you to understand the design principles we'll be discussing later in this chapter, we need to go back to the beginnings of computer networks.

Even the earliest computers were network based, in that the user sat at a terminal in the corner of the room, and the computer filled the other four floors of the building. In effect, the control of the machine was from a terminal—a remote keyboard, card reader, printer, or screen.

Dumb Terminal Networks

If this sounds a little far-fetched, consider the modern PC. The DOS interpreter still understands the kind of commands that were used then, for example the keyword CON refers to the console—what the operating system understands as the keyboard and the screen. In a DOS Command Window, COPY C:\MYFILE.TXT CON produces a listing of the file's contents, and COPY CON NEWFILE.TXT creates a new file containing the character you type. (If you decide to try this, don't forget that you have to press Ctrl-Z to close the file and get control of the system again).

The advent of graphical interfaces has blurred this distinction somewhat, but in effect you're still sitting at a machine where the keyboard and screen form a 'dumb terminal', which talks to the rest of the system behind the scenes. This is a similar scenario to the first kinds of distributed computing. All you needed to do was allow the central processing unit to support several sets of screens and keyboards—or terminals—and scatter them around the building.

In a dumb terminal network, the server stores all the data and carries out all the processing.

This was the traditional central processing system model. 'Dumb terminals' carried no processing power of their own, other than that required to collect keystrokes and send them back to the main processing unit, and display information coming from it.

This is still a common model for modern-day computing, where a mainframe or mini computer drives the network. It stores all the data, and carries out all the processing. All the terminal sees is the results of the commands it sends—and this concept has even been extended to provide graphical user interfaces on the client terminal.

The Advantages of Dumb Terminal Networks

The traditional dumb terminal network is the administrator's dream come true. All of the configuration and (most important of all) the power of the system, is contained inside that air-conditioned room. As long as the physical network connections are intact, and the simple terminals aren't belching smoke, it all works. Central control means that the entire network can be managed, monitored, and maintained from one place. It also means that network traffic is minimized. All that has to travel the wires are the instructions coming from the terminals, and the results being sent back.

And if you think that the dumb terminal network is dead, then just take a look at Java. Right now there is a huge development effort going into the Java Station, the Network Computer, and even 'TV set-top box' Web terminals. All these, by and large, are terminals with limited processing power, zero configuration requirements, and no local storage.

The Internet as a Dumb Terminal Network

So how does the Internet, and especially the World Wide Web, fit in? It's easy to see that the concept of a dumb terminal network almost exactly matches way in which we use the 'Net. Although the machine on our desk has huge reserves of processing power, and (in theory anyway) plenty of local storage space, all we are doing with a browser is acting as a dumb terminal.

We send a request off to the Web server, and it sends back the processed information as a static page that the browser just has to display. Up until the advent of client-side technologies like Java, ActiveX and scripting languages, the browser was literally a dumb terminal.

The physical structure of the Internet also matches this model very well. Bandwidth is at a premium, so the minimization of network traffic is a major bonus. And the remote geographical nature of the terminals makes visits by the network technician impossible.

Putting PCs on the Network

Of course, with the arrival of the personal computer, users wanted more than just a dumb terminal on their desk. Seeing what was possible with their own 'real' computer meant that static information coming from a server, over which they had little or no control, was obviously severely limiting when the technology beckoned with ever-increasing capabilities. And soon, PCs were strung together to form local area networks. Users could share files and resources, like printers, between the machines.

On a peer to peer network, each machine holds it's own data and carries out its own processing on that data.

This is great, except for three things. Firstly, there is no real central management, so everyone ends up using different applications, styles, and formats—documents stray from the standard corporate design, and everyone can do their own thing. Secondly, because each machine has its own local storage, files are duplicated across the network. Each user keeps their own copy of the corporate data, and so backing up—and even just getting an overall picture of the information available—becomes impossible.

And finally, the PC is a rather more complex beast than a dumb terminal. Configuration and maintenance now involve the technician rushing around the building, installing and upgrading each machine separately. Even strict management of the users, and new technologies which allow remote configuration of machines and replication of data across them, generally fail to achieve real solutions to these problems.

Peer to Peer vs Central File Storage

Linking individual PCs together, as we've just been discussing, is generally referred to a peer-to-peer networking, because everyone has equal rights on the network. And, as we've seen, one of the major problems is that data becomes duplicated across the various machines. One simple way to cure this problem is to place a single copy of each file on a nominated machine, and let every user access that copy.

Now, the junior accountant keeps the customer database on his hard disk, and remembers to back it up daily. However, the constant accesses from all the other users are going to limit the responsiveness of his machine. It could well slow to a crawl when the sales desk is busy. The solution is to dedicate one machine on the network as a central file server, provide it with oodles of disk space, and put all the files there. It becomes a lot easier to do proper backing up, and duplication of the data is prevented.

Using a central file server means that all the data has to pass across the network each time it is queried or updated.

While this network model solves the file duplication problem, and to some extent aids network management, it does little to solve the concerns of configuring and maintaining the rest of the machines on the network. It also, unfortunately, adds another problem. Every file has to travel across the network from the server to the end user, then back again to be saved. If the junior accountant needs to update the customer database, the complete file it has to be fetched from the server, processed, and saved back there again. Network bandwidth requirements go through the roof.

Intelligence at Both Ends of the Network

In recent years, technologies have been developed which were aimed solely at solving the mixture of problems we've seen so far in the various networking models. An example of this is Microsoft Access, which can work either as a stand-alone application, or in a kind of client/server mode.

When we create a new database on our hard disk, Access works as a single-user local processing application. All the data storage and manipulation is done on our machine. However, we can use Access as a 'front end' to a set of database tables, by linking them to it. These tables can then be placed on another part of the network, say the central file server. Now, everyone can have an Access front-end (and not necessarily all the same one), while working with a single set of data.

But this alone isn't client/server computing, and it does little to limit bandwidth requirements. What completes the picture is that the central server can carry it's own copy of the database engine, minus the 'front end'. Now, instead of the client machines fetching a whole table of data across the network each time, they can issue an instruction to the central database engine, which extracts the results they need from the tables and sends just that back across the network.

Client/server systems minimize network traffic by distributing the processing between the server and the client machines.

In Microsoft Access terms, the database engine is executing stored procedures, something we came across back in Chapters 4 and 5 when we looked at the Active Data Object. This itself is just a way of connecting to, amongst other things, different database engines.

So client/server, at least in theory, gives us the best of all the worlds. We get minimized network traffic, central data storage, and easier systems management because the 'important' processing can be done at the server end if required. The only real down side, and the one that is currently the biggest cause for concern in the corporate world, is the continued difficulties of individual client machine maintenance, upgrades, and configuration.

The Internet as a Client/Server System

As you'll have already guessed from our earlier discussions of Active Server Pages, one thing it aims to achieve is to allow easy development of client/server applications on the Internet, and more particularly in the environment of the World Wide Web. In many ways, this is the only way to go, because the prospect of any real increases in the bandwidth available in the near future is unlikely. While a modern corporate network will run at 10Mbps, and even up to 100Mbps, many users are usually limited to 28.8Kbps or 56Kbps. Even an ISDN connection can only manage somewhere up to 128Kbps.

As for the client end of the network, almost without exception, the user has a modern PC with plenty of local storage space, and spare horsepower available, so the browser can afford to do more and more of the processing at its end. Even the downside of system maintenance, upgrades and configuration is less of a problem. At least, on the Internet as a whole, it's the user's problem now, and not the network administrator's. In a corporate Intranet environment this isn’t the case, but standardization on one browser does tend to make the job a great deal easier.

So, having seen some of the background to client/server development, let's move on to look at what it offers in the environment of the Web. Client/server on the Web is exciting, because it solves many problems that are making traditional client/server projects expensive, overdue, and hard to maintain. More specifically, Web-based client/server solves the problems of:

Distribution: Distribution is automatic, because new copies of the pages (the components of our application) are downloaded to the client machine whenever the local client cache says they need updating. No more walking round the building installing software, or expecting your Web site visitors to download and install it themselves.

Flexibility: The automatic distribution and installation means that applications can be updated much more easily. This is a huge benefit if the environment requires quick turn around on changes to the specifications or just bug fixes. Even 'Interface du jour' could be implemented, where different interfaces and content are deployed daily—of great interest to marketing departments when selling products.

Central Control: With the deployment of PC’s and local installations, the system administrators lost control, and help desk costs for maintaining non-standard desktops increased. Being able to control the applications at the server end is highly cost effective.


The Theory of Client Server

So having justified the case for client/server, we need to investigate further what this enticing new technology actually is. This is a question that has many different interpretations. The reason is that client/server has become a buzzword that every new product must contain. In its simplest form, we can use this definition:

Client/server computing is the splitting of a computing task between client and server processes.

Getting More Specific

This literal definition is very vague, however, there are more concrete definitions. For example, the following diagram is more specific. It states that there are four layers to a client/server system.

These four layers are from the classical model of client/server design, and can show up in various different forms. But to be able to develop a client/server system you must be able to understand the four layers.

The Data Capture Layer

The first layer is called Data Capture. This means that the data is captured and converted from a human representation to a computer representation. The previous sentence could be more easily termed as punching in the data. However, using that type of definition implies that the user has something that they can actually punch—such as a keyboard. The more precise definition is better, because it sums up all possibilities. For example, in the future, input devices could use voice, gesture, or neural transmitters. Coming back to reality, current input devices may be a mouse, keyboard or virtual reality interface device. The only limit is human imagination.

The reverse of input is output, and the statement also applies in this context. Examples of output devices are monitors, printers, or tape drives. Programming operations that are acceptable in this layer would be filling or reading the contents of a list box or a combo box, and then packing the data into a pre-defined structure.

However, it's important to note that this layer is only responsible for the translation of the data from one form to another, human to computer or vice-versa. The actual contents of the data are not verified for correctness or accuracy here.

The Business Rule Layer

This layer is responsible for applying the Business Rules to the data captured in the first layer. It is responsible for converting the data to a business context, and adding information about the business rules. The user does not interact with the software in this layer at all, however this layer is critical because it validates the data to make sure that it is in the correct form—and is applied to the data that is both coming from or going to the server. The business rules must only be rules, they must not process the data.

Consider, for example, the implementation of a mortgage loan application. The business rule layer would filter the input first for completeness, then apply any other validation. A valid rule would be granting loans only to people who are at least 18 years old. However, a rule that denied a mortgage application just because the applicant did not earn enough for the property at a specific price would not be a valid business rule. In reality, denial of a mortgage is more complicated than a simple rule could cover. And while rules like this could be expressed in computer terms, they tend to contain too many and's, if's, and but's.

A business rule should nest within other business rules, otherwise this layer becomes too fat, and requires too much processing. Neither should it depend on any data coming from higher layers—it needs to be self-supporting in all cases. In programming terms, the rules should be able to be stored in a small local file or, even better, coded within the program.

The Application Interface Layer

The third layer is called the Application Interface layer. This is responsible for converting the data from a business context to a technology context. The technology context is whatever the final layer, the Application Server layer, requires.

Going back to the mortgage example, the application interface might convert the request and associated data into an SQL statement, and then pass that to the final layer. By convention you would not to put any business logic into this layer, to allow for future expandability.

The Application Server Layer

The final layer is called the Application Server layer. This layer has the task of processing the data, which is now in a technology context, and the process is not dependent on the actions of the user interface. Neither does the processing need to be logical in human terms. This layer is all about the storing of data and calculation of results.

For example the equation y = x / b is a mathematical reality, and can be computed for all values where b is not zero. However, when b is zero, the results are undefined in mathematical terms and an overflow error occurs. Yet in our own human terms, the result still has sense of reality—it's just a very big number. The point is that in this layer the data is manipulated as something only mathematics, science or computers can fully understand. Our conceptions of the result will often be wrong.

As another example, take an SQL statement that we generated in the previous layer. While we would understand the plain English definition of the query, and probably have a good idea of what the SQL statement actually meant, we would have no conception of what actually went on inside the database while the query was being processed. It is considered to be a 'black box'.

These four layers form a definition of client server computing in a nutshell, and are of interest to anyone who needs to develop corporate solutions. But they prompt the questions 'Why do we really care about these layers at all?' and 'Why don’t we just create programs and let the layers sort themselves out?'

Partitioning a Client/Server System

In fact, the whole purpose of the layer definitions is to help us understand how we divide up, or partition, our client/server applications. We ultimately have to define the split between the client and the server, in other words decide which layers will be on either side of the network connection. This is the subject of a great deal of industry debate. Typically what has happened in the past is that the layers one, two and three were on the client, and the fourth layer was on the server. This is the infamous fat client, plagued with problems of poor performance, complex maintenance, and high costs.

Client/Server Middleware

A second problem with corporate client server architecture, running on the company network, has always been how the client and server communicate with each other. The technical term for this is middleware—the software that makes it simple to abstract the communication. Every vendor has a solution, and they are not always compatible with each other. This causes many network headaches, and solving it would make client/server development that much easier.

However, we won't be delving into the details of which other architectures are acceptable and workable in this book, but instead we'll focus on the single architecture that interests us—Web client/server. The Web solves the communication layer problem because the HTTP protocol it uses provides a common base for all applications. What we do need to do is decide how we are going to partition our applications in this environment.

Partitioning a Web Based Application

To end our look at the theory of client/server computing, We'll go through how a typical application would be partitioned under the Web client/server architecture. Layer one is the human to computer interface, and it would typically be a HTML based browser. There may be some client side controls or scripts to add to richness to the user interface, but this is purely optional.

Layer two is the business rule layer, and is generally handled on the client by the scripts and controls in the HTML page. However no parts of the rules are hard-coded into the browser, they only exist in the pages themselves. There is an argument that this can pose a security risk because the rules then need to pass across the network. However the use of Secure Sockets Layers (or Secure Channel Services) can help to reduce the risks. We'll be discussing all these topics in Chapter 9. So in some cases, either to enhance security or because a rule requires features that aren't available on the client, all or part of the business rules layer may reside on the server.

Layer three, the conversion from a business context to a technological context, occurs on the server. It could be that an HTTP request triggers a routine that creates a structure of data, or that ASP converts it to a new representation ready for the final layer.

Layer four, the application server layer, is again located on the server, and is the 'back end' that actually does the processing and produces the results. This may be a database or other business object, and the result might be retrieval of information for return to the client, or just storage of data sent from it.

So in our Web-based model, layers one and two are on the client machine, and the server contains layers three and four. We have a more balanced and better performing system, with one exception—the middleware.

Middleware on the Web

At the time of writing there was no standard middleware based on the HTTP protocol. There is new software that is showing some promise of being accepted, however for it to become reality, a standard method of tunneling protocol needs to become freely available.

Tunneling is the process of securely embedding one protocol within another, so that information can be sent directly from one client/server layer to another across the 'Net, within the HTTP packets.

This ends our theoretical, and perhaps rather dry look at client/server. However, it should have indicated that there is more to the subject than just script and components. We need to think seriously about how we design our applications to conform to the accepted standards, and to get the best performance—in terms of processing, usability, and security. The remainder of this chapter looks at different aspects of designing an application that will combine Active Server Pages and some of the other technologies that are relevant. At the same time, we'll be setting the scenes for the remainder of this part of the book and the case studies that follow.


Applications, Sessions and State

Traditional network based applications have always had one major advantage—the automatic maintenance of state by the network itself. The previous lack of this feature within HTTP has tended to limit the development of true Web-based applications. What do we mean by state? In a traditional local or wide area network, the software (such as Novell, Banyan, or Microsoft's own protocols) combined with features of the operating system in such a way that once a user logged into the network, they could be automatically recognized by all the applications on it.


Bringing State to HTTP

On the Web, with previous applications based on technologies such as CGI, the problem has always been that, when a client submits a request, the server just produces a response and returns it. When another request is received from that user, the server has no idea if there was a previous request. This is because the HTTP protocol is stateless. Building client/server programs with a stateless protocol can become very complicated.

Why Do We Need State?

To understand why, imagine building a traditional application in, say, Visual Basic. You design a form with a dozen or so command buttons, and write a piece of code that runs when each button is clicked. VB itself knows which button was clicked, and so runs the correct code routine for that button. But now imagine that VB was designed like HTTP. Instead of running individual routines for each button, you would have only a single block of code. VB would run this code when any of the buttons is clicked, without telling you which one it was. You can see how this would, to say the least, make building applications difficult.

To get around this problem Netscape developed the concept of a cookie. A cookie is purely a small informational text string that is stored on the user's hard disk—we looked at them in some depth back in Chapter 2.

Many people worry that cookies can be dangerous. While they can be used in all kinds of ways by server-based application, they pose no risk to you as a user. Cookies, as they are documented, will not format your disk, send back information about your system, or put you on a mailing list. Unfortunately they don't make the coffee either.

Creating State with Cookies

Typically, when the client makes a request for a document from a specific virtual directory on a server (which can include the wwwroot directory), the server returns a cookie—or token of information. When the user returns to the same virtual directory, or one of its subdirectories the browser sends that cookie back, as part of the HTTP request.

If we were building CGI-based applications, we would use the cookie information to check who the user was and collect other information regarding their interaction with our application. Working with cookies directly can be fun and interesting for about 30 seconds, and until you realize that you need to go home by five o'clock. As we saw in Chapter 2, ASP does make the task a lot easier through the Cookies collections of the Request and Response objects.

However, the big advantage that the Active Server Pages framework provides is an automatic mechanism for maintaining state in our applications, without having to directly manipulate cookies ourselves—although ASP requires the browser to support and accept cookies in order to function properly. If the user refuses to accept cookies through settings in their browser's security options, or if their browser doesn't support cookies, this automated mechanism will fail.


Using Applications and Sessions

We looked briefly at what the Application and Session object are in Part 1 of this book, and had a brief glimpse of the way they could be used. When we come to build Web-based client/server applications, these two objects assume a far greater importance that we’ve previously credited them with. They allow us to connect together all the parts of our application.

For example, we can tell where on our site our visitor has already been, where they need to go next, and what they've decided to buy as they go along. In other words we can store and maintain the values of variables for each user, as well as globally for all users. In this section, we'll explore some of the possibilities in detail.

Understanding Global.asa

To make Application and Session work in ASP, we use a single file named global.asa for each 'application'. This file resides in the directory on the server that is the root of that application—i.e. where the files that make up the application are located. Any subdirectories of the main application directory are also part of the application, and the global.asa file applies to their contents as well. This means that you need to be aware of the possibility of overlap between applications, and should generally create separate directories for each one. For example:

Physical directory | Virtual directory | Application
C:\InetPub\WWWRoot\ | / |
C:\InetPub\WWWRoot\Demo\ | /DemoApp | Demo
C:\InetPub\WWWRoot\Demo\Images\ |  | Demo
C:\InetPub\WWWRoot\Apps\Main\ | /MainApp | MainApp
C:\InetPub\WWWRoot\Apps\Main\Test\ | /TestApp | TestApp and MainApp

So, when we talk about an application in ASP, we are actually talking about all the files included in the same directory as global.asa, and any of its subdirectories.

Application and Session Events

We talked about the events that Application and Session implement back in Chapter 2. However, we'll summarize these briefly in the context of state, and then quickly move on to look at how we can use them in our applications.

The Application_onStart and Session_onStart Events

Both of these events are to be used to initialize state, by setting up variable that are global either for the application or a specific user. When the first user accesses a file in our application, the Application_onStart event is triggered. This is used to initialize any application-wide global variables. When the user begins a session for the first time, the Session_onStart event is triggered. This is used to initialize user-specific information.

The power of the Session object comes from the fact that it can store variables that are global to just that specific user, and so each user can have their own individual value for that variable. Session objects are not always created automatically for every user when they enter our application. However, storing or accessing a variable in the Session object will create it, and fire the Session_onStart event. We can force new sessions to always be created as soon as a visitor enters our application by writing code in global.asa to respond to this event.

When responding to the Application_onStart event, we must not under any circumstances use code specific to any one particular user. In this event we would typically create global objects, such as a server side component that needs to be shared and available to every visitor.

Session and Application events only happen when a client retrieves an ASP page—they are not triggered when an HTML page in the application is requested. Therefore, if you have additional server side applications such as ISAPI or CGI scripts, make sure that they do not depend on specific events having occurred within an ASP page. Otherwise the ISAPI or CGI script may crash and cause the Web server to hang.

The Session_onEnd and Application_onEnd Events

The Session_OnEnd event occurs either when a current Session is abandoned by using the Session.Abandon method, or when it times out. By default this is 20 minutes after the last request for a page from the application, though this can be changed either by setting the Session.Timeout property or by editing the registry. See Chapter 2 for more details.

Something we need to consider is if we have objects that themselves contain timeouts. If, for example, we create a database connection in a Session, and the connection timeout is less than the Session timeout, it's possible for corruption of the object to occur. If the database connection times out after ten minutes, and the Session times out after twenty minutes, the database connection will not be valid, even though the object in the Session still is.

The Application_onEnd event can be used to clean up all of the global objects and variables. There is a problem at the present time, however, in that this event may not actually be triggered until the Web server is stopped. Revisions of ASP seem likely to specify that the Application_onEnd event be triggered once the last Session_onEnd event occurred, i.e. when the last session ends and there are no current application users.


Application and Session Variables

The Application and Session objects can be used to store values that are global either to a particular user (the Session) or to all users (the Application). Within the onStart events, we can initialize these variables. We can also store new variables, or change existing values, in the code inside any other ASP page.

Initializing variables is very important, especially with a language like VBScript that uses Variants. Imagine the following code in a page:

Response.Write("The current value is: " & Session("MyValue"))

This places the contents of the Session variable MyValue in the page. The only problem with this code is if the variable has not been initialized. What we get is:

The current value is:

Any Variant (the only data type available in VBScript) that has not been assigned a value is said to be Empty. Because we are dumping the variable as its default type, we get nothing. The best way to solve this type of problem is either assign a default value to it, or examine the variable using the IsEmpty() function. Here's how we could use IsEmpty():

varTheValue = Session("MyValue")
If IsEmpty(varTheValue) Then varTheValue = "* Undefined *"
Response.Write("The current value is: " & varTheValue)

Alternatively, we can set any default value we like in the Session_onStart event, so that we have a value ready for access in that session:

Sub Session_OnStart
  Session("MyValue") = 42
End Sub

Counting Sessions

An immediately obvious use of this technique is to count how many sessions have occurred during the current application. All we do is use a variable stored in the Application object, which is then available to all sessions:

Sub Application_OnStart
  Application("NumVisitors") = 0
End Sub

Now, in Session_onStart, we can increment the value for each new session:

Sub Session_OnStart
  Application.Lock
  Application("NumVisitors") = Application("NumVisitors") + 1
  Application.Unlock
End Sub

Them we can drop it into the 'welcome' page with a few lines of code:

<% Application.Lock %>
<H3> Your are visitor number <% = Application("NumVisitors") %> </H3>
<% Application.Unlock %>

Storing Array Variables

As we briefly mentioned in Chapter 2, we have a problem when storing arrays in a Session or Application object, which maintain all values as Variants. The following example shows how we can get round this limitation:

'Create and initializing the array
Dim MyArray()
ReDim MyArray(5)
MyArray(0) = "hello"
MyArray(1) = "some other string"
'Store the array in the Session object
Session("StoredArray") = MyArray
'Now retrieve the array from the Session object
LocalArray = Session("StoredArray")
LocalArray(1) = "there"
...
'and then store the updated one back again
Session("StoredArray") = LocalArray
...

We create the array MyArray()and ReDim it to hold five elements. Then we assign strings to the elements 0 and 1,and simply assign the array directly to a variable StoredArray in the Session object. We've effectively converted our array of strings into a variant array, and stored it in a single Variant in the Session.

Later in the same script, or in another request in the same session, we can retrieve the array using LocalArray = Session("StoredArray"), then access it as normal using the indexes. You might be tempted to believe that instead of assigning the array to a single Session variable, we could access it using Session("StoredArray")(1) = "there", but this is not the correct method. It will result in loss of data.

The Application and Session objects' variables are implemented as collections, however we should not actually access them in this way—it is not documented or recommended. Neither is it possible to store references to any of the Active Server Pages built-in objects—for example the statement Set Session("MyRequest") = Request is not legal.

Reference Counting with the Application Object

You'll recall we suggested earlier that the Application_onEnd event might not be fired until the Web server is stopped. So what happens to the global objects that are stored within the Application object? They are kept alive and available all of the time. If you have a resource that you don't want to be kept in memory for days on end, when the application is not in use, the following code solves the problem, and demonstrates some of the ways of using the Application and Session objects:

Sub Session_OnStart
  Application.Lock
  If IsEmpty(Application("Object")) Then
    Set Application("Object") = Server.CreateObject("global.connection")
    Application("ObjectCount") = 0
  End If
  Application("ObjectCount") = Application("ObjectCount") + 1
  Application.Unlock
End Sub
Sub Session_OnEnd
  Application.Lock
  Application("ObjectCount") = Application("ObjectCount") - 1
  If Application("ObjectCount") = 0 Then
    Set Application("Object") = Nothing
  End If
  Application.Unlock
End Sub

The technique we use in this code is called reference counting. Notice that the Application events are not used, but the Application object is. All the events are those that occur for each Session. The technique, however, is simple enough. At the start of each Session, we only create an instance of the object we want (in this case a fictitious "global.connection") if it does not already exist. At the end of each Session, we destroy the object if no-one else is using it. If you like, it's the old 'last one out turn off the lights' trick.

How It Works

This code works by keeping a count of the number of users that are using the application at any one time, storing this count in a global variable called ObjectCount. If this is zero, or does not exist, the Session_onStart code creates the object and stores the reference to it in the Application object as a variable called Object, and it's then available to all users. At the same time, it sets the value of ObjectCount to zero, in case it didn't actually exist—in other words if this was the first ever session to use the application.

The next step is to increment the value of ObjectCount, so that it reflects the number of current sessions. If the object already exists when a Session starts, and the Object variable does reference an object when this session begins (i.e. it's not Empty), the code will just increment ObjectCount without creating a new instance of the object.

The Session_onEnd code, which runs for each Session as it ends, just has to decrement the value of ObjectCount, and it can then tell how many other sessions are still active. If this is zero it can destroy the object by setting its reference variable to Nothing.

Notice that we've again used the Application object's Lock and Unlock methods before changing the values of any of its variables. Failing to do this can cause all kinds of problems by allowing more than one session to access the values at the same time. This concept is called concurrency, and we'll look at the implications that arise from it next.


Application Concurrency Issues

As we've already seen, the purpose of the Application object is to be able to store information that is globally used among all application users. These could be object reference variables or other values that all application users require access to. However, the problem is that, because they are shared, we can come up against concurrency problems. Consider the following example:

Application("NumberOfSales") = Application("NumberOfSales") + 1

This line of code in an ASP file could be used to count the number of users who had ordered goods from our site. It maintains the count in the Application variable myData. However, if two users access this variable at the same time corruption is likely occur, because the single Application object has global scope and is visible to all. User A reads the value of NumberOfSales at the same time as user B. Both instances of the ASP page get the same value, they both increment it, and they both store it back in the Application object. The result is that it only gets incremented once. It might even corrupt it altogether if both writes occurred at the same moment.

Locking the Application Object

To solve the problem, we just change the code to read:

Application.Lock
Application("NumberOfSales") = Application("NumberOfSales") + 1
Application.Unlock

You don’t need to use Lock and Unlock in the Application_onStart event, because this event is can only be called once—by the single session that starts the application. The Application_onStart event is called before the first Session_onStart event, which is called before the ASP page is actually processed.

Read-Only Session and Application Variables

You may think that with a simple assignment like the one we've just seen, the chances of corruption are remote. That may be true for a single processor computer, but with SMP multi-processor machines becoming popular this assumption is not safe under all circumstances. One useful trick, where appropriate, is to make variables read-only for all sessions by checking the value first—as shown below:

Sub Session_OnStart
  Application.Lock
  If IsEmpty(Application("myData")) Then
     Application("myData") = strTheValue
  End if
  Application.Unlock
End Sub

This code only sets the value of the variable if it hasn't been set before, giving it—in effect—a default value. Once the variable has been assigned a value, and is not Empty, it can't be changed. However any other code can read the value as required, and with no fear of corruption:

Application.Lock
MyVar = Application("myData")
Application.Unlock

Using Global Values and Object References

While the previous example solves the concurrency problem, it brings new ones. In a very busy site, the code to read the value could be called thousands of times a day. Locking the application just to read a value could drastically hit server performance. The situation is even worse when we come to store references to objects, which could be required by all users.

If we only need to read the value of a variable as it stood at the start of a session, we could copy it from the Application object into that user's Session object. Then, each ASP page could reference the Session version. This has the down side, however, in that the value will not keep up with changes to the value in the Application object automatically—but it all comes down to what we actually use the values for.

Connection Pooling with ADO

When we come to use objects like the Active Data Object, we often need to access a single instance of it several times in a page, and across our application. For example, we use the ADODB.Connection object to create a database connection. Instead of doing it on every page, we might consider just opening a single database connection once (in Session_onStart, or even in the Application_onStart) and then using that connection throughout the entire session or application.

Database applications designed for a multi-user audience will generally run on something like SQL Server, or another high-end database. While reusing a connection stored in the Session or Application object might be an efficient way to work with low transaction volume databases like Microsoft Access, this method loses attractiveness as the number of users of our application increases.

You might recall from Chapter 4 that ODBC 3.0 includes a feature called connection pooling, which manages connections across multiple users. When using connection pooling it’s best to open and close the database connection on each page that uses it—this allows ODBC to manage the connections most efficiently. Without it, there could be ten users logged onto our application, but who weren't actually doing anything and this could cause ten idle connections that decrease performance and consume server and database resources.

So, with Access, you should consider a Session_onStart instantiation of connections—while for other databases it may well be wiser to use local connections in each page. The Roadmap help file that ships with ASP has more information on connection pooling.

Global Object Reference Problems

When we come to create global references to objects, however, we can't just shuffle the values around between the Application and Session objects, because we risk problems as the object's own internal state changes. Instead, we need to make the decision as to where we actually want to use the object. We saw ways of creating and destroying an object at global Application level earlier. We can, of course, create Session level objects in Session_onStart, and destroy them in Session_onEnd. In this case, we don’t have to worry about concurrency, and we know they will not hang around in memory on the server when the session ends.

Creating objects at Session level is generally the best solution. If we do need a global object for the whole application, like the Card Game case study you'll see in Part 3, we have to take some extra care when using it. Have a look at these two extracts of code:

Sub Application_OnStart
   Application("myObject") = Server.CreateObject("game500.player")
End Sub

This is in global.asa, and it creates the global instance of the object that will manage the whole game. In one of the ASP files that are used as part of the game, we call the SetData method of this object to change the game state at that point—when a card is played.

Application("myObject").SetData(Request.QueryString("txtUser"))

The problem here is that we risk corruption again. Two users could call this method at the same time, leaving the results undefined. This time, the concurrency problem is not at the script level, but at the object level. It needs to be handled by the object itself, and is concerned with the threading model it uses, and the way that the COM interface is implemented. These topics are beyond the scope of this chapter, but will be investigated in more depth in the case study in Chapter 14.


When State Doesn't Look After Itself

When State Doesn’t Look After Itself

The whole concept of Applications and Sessions requires the browser to accept and implement cookies. If it doesn't, then ASP is unable to maintain state information of any kind. We have to resort to the older methods of CGI programming.

How State Works in ASP

As we said earlier, the HTTP protocol is inherently stateless, and relies on cookies to be able to manage and recognize requests, and match them to the users. When ASP wants to establish a session, it sends the Set-Cookie HTTP header to the browser, to establish a unique user session ID and the path of the application it corresponds to—i.e. where the appropriate global.asa file resides:

Set-Cookie: ASPSESSIONID=LRUSDYXQMWRTNWEB; path=/TestApp

From our discussions of cookies in Chapter 2, you'll realize that this one doesn't expire until the browser is shut down. In theory, if the browser was to be left running for weeks on end, the cookie will remain current. But the Session object has a default Timeout of twenty minutes, and that point the Session is marked as having ended and any variables in it are destroyed.

The Un-expired Cookie Problem

The fact that a user's Session ID cookie may not have expired, while the matching Session on the server already has expired, raises an interesting question. What happens if that user comes back to our site again? The server will check the session ID in their cookie against the currently active sessions, and if it doesn't match will just create and initialize a new Session. The user then effectively 'goes back to the start'.

Of course, this assumes that the session ID will be unique for every user. If this isn't the case, the application might find itself with two users sharing a Session. We have to assume that Microsoft has thought of this, and that the session ID generator is sufficiently random to prevent it happening!

When the Cookie Jar is Empty

One problem that Microsoft cannot prevent is the situation where the session ID cookie our server sends is not supported by the browser, or not accepted by the user. Or more critically, it's possible for the server administrator to turn off the ability to maintain Sessions altogether by editing the registry. Now, the automatic state mechanism is not going to work, so we need an alternative plan.

We could use a system originally implemented in ASP, but then removed again, which added parameters to all the hyperlinks in the pages so as to pass on the session ID. We could emulate this, or just shunt the user off to a separate version of our site that worked without requiring state information. We could even throw them out altogether, though this may tend to discourage future visits...

Checking for Cookie Acceptance

But before we can do any of this, we need a way of determining whether or not the browser can support cookies, without causing havoc. Easy enough, we just use a default opening page that gets sent to every user, when they first enter the application. We could include a 'Welcome' graphic, like a real application's splash screen, or possibly some introduction to the site:

Here's the code that creates this page, with the important lines are highlighted:

<HTML>
<HEAD>
<META HTTP-EQUIV="REFRESH" CONTENT="5; URL=hellotest.asp">
<TITLE>Document Title</TITLE>
</HEAD>
<BODY>
<CENTER><IMG SRC="Welcome.gif"></CENTER>
<% On Error Resume Next
   Session("TestBrowser")="Hello" %>
</BODY>
</HTML>

The ASP code first turns off error checking, then stores a value in a Session level variable named TestBrowser. If the browser doesn’t support cookies, or if they're turned off, there will be no Session object and the code will fail—however there won't be an error message because of the On Error Resume Next. The real key to this page is the first highlighted line:

<META HTTP-EQUIV="REFRESH" CONTENT="5; URL=hellotest.asp">

Once the page has finished loading, the browser waits five seconds and then loads the page hellotest.asp—which contains this code, placed before the <HEAD> section:

<% On Error Resume Next
   If IsEmpty(Session("TestBrowser")) Then
     Response.Redirect "NoCookie.asp"
   Else
     Response.Redirect "AllowCookie.asp"
   End If %>
<HTML>
<HEAD>
  ...

All it does is check the value of the TestBrowser variable. If it's Empty we know that the browser, for one reason or another, doesn't have the ability to use a Session—which is likely to be because it can't (or won't) support our session ID cookies. If this is the case we just redirect the visitor to a page that is intended for non-session ASP users. Otherwise its business as usual, and we redirect them to the main menu of our application. Notice that it's necessary to wrap the If statement in another On Error Resume Next, in case the IsEmpty test fails. If it does, the next statement to be executed still sends them to the non-session area of the site.

Using Document Redirection

The code we've seen in the previous section is an excellent example of the way that we can redirect users to a different page at will. We can often take advantage of this method to route users through an application, depending on the current state for that user, or the application as a whole.

For example, we might allow them to chose goods they want to purchase, in a kind of virtual shopping trolley. Each time they click the Yes, I Want One button, we add the details to our Session object, using an array like we saw earlier:

Sub cmdYesIWantOne_onClick
  'Retrieve the array and current item count from the Session object ...
  LocalArray = Session("BoughtItems")
  intNumberOfItems = CInt(Session("ItemCount")) + 1
  LocalArray(intNumberOfItems) = strItemCodeNumber
  'then store the updated values back again.
  Session("BoughtItems") = LocalArray
  Session("ItemCount") = CStr(intNumberOfItems)
End Sub

When they've finished shopping, and click the All Done Now button, we only need to route them to the virtual checkout if they have actually bought anything. It's easy using document redirection:

If CInt(Session("ItemCount")) > 0 Then
  Response.Redirect "cashdesk.asp"
Else
  Response.Redirect "thankyou.asp"
End If

Redirecting From Within a Page

Doing a redirection like the last example is only possible from the header of a document, before any content has been sent to the browser. If we attempt it after that, we get a Buffer not empty error. Remember from Chapter 2 that if we want to provide an opportunity for redirection to occur part way through a page, we need to turn on buffering, and clear the buffer, first:

<%@ LANGUAGE="VBSCRIPT" %>
<% Response.Buffer = True %>
<HTML>
<HEAD><TITLE> Document Title </TITLE></HEAD>
<BODY>
<H1>Welcome to our site</H1><P>
<% If Session("TestCondition") = True Then
     Response.Clear
     Response.Redirect "anotherpage.asp"
   End If %>
   ...
   rest of page
   ...
</BODY>
</HTML>


Making Use of State Information

We've now established the ways that Active Server Pages can provide us with the state information we need to create working client/server applications. We have a unique Session object for each user, and we can store values and object references in it, which will remain valid while that user is working with the application. In this section, we'll look at ways that we can use this information, then move on to consider how it might influence the design of our site, and the pages it contains.


Logging Users into an Application

One of the primary reasons that we use the state information is to identify each user. They are already identified within ASP by a session ID, but this tends not to be very useful. What we will generally want to do is change the behavior of our application based on the users absolute identity, i.e. a UserID or nickname. This follows a similar practice to a normal, non Web-based client/server application.

It's possible to identify users through advanced communication methods, when using Secure Channels or Challenge/Response Authentication—subjects that are covered in Chapter 9. However, often we'll need to implement a standard log-on type of dialog, and then use the information the user provides within our application.

The user supplies a user name and a password, and clicks the Submit button. All we need to do is respond to the request by checking these details. It makes sense to use the POST method for the form so that the information is not send attached to the URL in the query string, but at least partly concealed within the HTTP header. Of course we should really use some type of secure transmission as well, to ensure maximum privacy.

Using the User's Information

However, having got the request at the server, we can examine the values the user entered:

txtUserName = Request.Form("txtUserName")
txtPassWord = Request.Form("txtPassWord")

The question now is what do we do with it? We can use it in our code in minimalist ways, like:

Welcome <% = txtUserName %> to our site

and even to redirect users, or control access to the pages:

If txtUserName = "Admin" And txtPassWord = "secret" Then
   Response.Redirect "AdminPage.asp"
ElseIf txtUserName = "Manager" And txtPassWord = "reports" Then
   Response.Redirect "SalesReports.asp"
Else
   Response.Redirect "UserMenu.asp"
End If

However, this doesn’t really provide a foundation for 'proper' application design. We need to be able to log users in, verify their passwords against a central list, and decide what further action to take. For example, we might want to deny access to users who don't have an existing user name (i.e. not accept new users), or we might want to automatically add them to our user list and give them certain default access permission. And if they have been here before, do we have any user preferences we need to set, like the background and foreground colors of their pages?

Verifying the User

Generally, the first step will be to verify who the user is, and decide if we already know them. Then, if we do know them, we can check to see if they've supplied the correct password. This process involves three basic steps, and we'll normally be taking the information from a database of some kind. In the following example we'll work through the steps, which are:

See if the user name exists in the database.

If it does, check that the password is correct.

If it doesn't, add the user to the database if appropriate.

Does the User Already Exist?

To see if the user already exists in our database, we just need to search for their user name in the appropriate table. Assuming we have a table UserDetails with UserName and UserPword fields, we can use an SQL query to extract the details. However, we may need to ensure that the length of the database fields and the validation are identical, depending on how the database actually stores text fields.

The database system may offer two types of text field—varChar and char. If the field is of type char and length 10, we would need to pad out the string to 10 characters first, to ensure that we get a match. Using varchar, or a normal Text field in Access, avoids this problem.

So, we can use the normal ADO object methods in our page to find out if the user exists. Here, we're using a connection string that we previously stored in the Session object, so that we can retrieve it as required:

'create SQL string using the value in the txtUserName control on the form
strSQL = "SELECT UserName, UserPword FROM UserDetails WHERE UserName = '" _
       & Request.Form("txtUserName") & "')"
'create the database connection and open a recordset with the results
Set oConn = Server.CreateObject("ADODB.Connection")
oConn.Open Session("Logon_ConnectionString"),
Set oUsers = Server.CreateObject("ADODB.Recordset")
oUserRs = oConn.Execute(strSQL)
'now check if there is a record
If oUserRs.BOF AND oUserRs.EOF Then           'we didn't get any records
   Response.Write("User name does not exist")
   ...

Once we've got the recordset, we just need to see if there was a record in it—there will only be one at most, because the user name would need to be unique, and defined this way in the database tables. We might try using the RecordCount property, but this requires a move to the last record first (see Chapter 4 for more about how this works). And if we do this with an empty recordset, it causes an error anyway. The easier way is to do what we do here, and check the values of BOF and EOF. Only if the recordset is empty will they both be True.

Does the Password Check Out?

If this first check discovers a record, then the user exists in the database, so we can now compare the password. The recordset we retrieved contains the password field as well, so it's just a matter of a straight comparison:

...
Else
   If Request.Form("txtPassword") <> oUserRs.Fields("UserPword") Then
      Response.Write("You did not enter the correct password")
      ...

The usual option here would be to allow them another attempt, by displaying the login form again. It's quite possible to do this in the same page, and in the next few chapters you'll see how we can achieve this.

Adding New Users to the Database

If the user name check fails, in other words the user doesn’t already exist, we need to decide if we want to add them to the database as a new user. Again, an SQL statement can do this. We know that the username they have supplied is unique compared to the existing ones, because we didn’t get a record returned. All we need is an appropriate SQL INSERT statement:

...
'add the user to the database
strSQL = "INSERT INTO UserDetails (UserName, UserPword) VALUES ('" _
       & Request.Form("txtUsername") & "','" _
       & Request.Form("txtPassword") & "')"
oConn.Execute(strSQL)
...

Ensuring Concurrency During Logon

There is just one problem. If we are going to add records to a globally accessible database, we need to make sure that we don’t upset any other logons that are being performed concurrently. For small sites this risk is marginal, but consider what would happen if two new users specified the same user name. The recordset we retrieved would be out of date by the time we came to add the new record, and the original could by now already contain the user name we are trying to add.

We could get round this in several ways, such as by making sure the database table design specified unique values—and then trapping the error that would arise. However, this approach is not ASP-centric, so instead we'll try one that makes use of the features available in ASP. What we need is a concurrency model that controls access to an item during the process, preventing two sessions accessing it at the same time.

Locking the Connection

Here's one possibility. Having decided to add the user, we do another search of the database for this user name in case it has been added meanwhile, by another visitor. However, we first lock the Application object while we read the connection string, and keep it locked until we've finished the whole process. Now, no other session can create a connection, and upset our code. Notice also that we only need to extract the user name and not the password as well. And the result we want is to not find a record this time

'create SQL string using the value in the txtUserName control on the form
strSQL = "SELECT UserName FROM UserDetails WHERE UserName = '" _
       & Request.Form("txtUserName") & "')"
'create the database connection and open a recordset with the results
Application.Lock
Set oConn = Server.CreateObject("ADODB.Connection")
oConn.Open Session("Logon_ConnectionString"),
Set oUsers = Server.CreateObject("ADODB.Recordset")
oUserRs = oConn.Execute(strSQL)
'now check if there is a record
If oUserRs.BOF AND oUserRs.EOF Then      'we didn't get any records
   strSQL = "INSERT INTO UserDetails (UserName, UserPword) VALUES ('" _
          & Request.Form("txtUsername") & "','" _
          & Request.Form("txtPassword") & "')"
   oConn.Execute(strSQL)
Else
   Response.Write("Error accessing database, please try again")
End If
Application.Unlock

We could, of course, have just locked the Application originally, and this would have saved the second search through. It all depends on whether you expect to get more existing users than new visitors, or vice-versa.


Making Use of the Logon Information

Having identified our users as they log on, what are we actually going to do with the information? This is open to the design of your applications, and it's not possible to lay down criteria, methods, or examples that are going to be specific to any individual's needs. However, there are a few obvious details that we should consider.

Setting User Preferences

Recall that when we logged an existing user into our application, we extracted the single record from the UserDetail table in our database that contained their user name and password. This record could just as easily contain a great deal more about the user, such as their department, workgroup, page color preferences, or even the name of their dog. All we need to do is change the SQL query to include all the fields in the recordset, and then capture the other details in Session level variables ready for use while the user is touring our site:

strSQL = "SELECT * FROM UserDetails WHERE UserName = '" _
       & Request.Form("txtUserName") & "')"
...
...
Session("BackColor") = oUserRs.Fields("BackColor")
Session("FontColor") = oUserRs.Fields("FontColor")
Session("DogsName") = oUserRs.Fields("DogsName")
...

Now, we can really make them feel at home:

<HTML>
<HEAD>
<TITLE> Welcome to the Wrox Pets Supermarket </TITLE>
</HEAD>
<BODY BGCOLOR=<% = Session("BackColor") %>>
  <FONT FACE=Arial COLOR=<% = Session("FontColor") %>>
  <CENTER><IMG SRC="welcome.gif"><CENTER><P>
  <H3> Great News... </H3> We now have a new variety of Scrummy<BR>
  <I><B>the dog food for champions</B></I><BR>
  available from the new products page.<P>
  Why not take some home for<B> <% = Session("DogsName")%> </B>- we
  <I>guarantee</I> it will be a hit!
</BODY>
</HTML>

Checking the Success of Object Creation

One thing we regularly do in the Application_onStart and Session_onStart events is create instances of objects that we need to use throughout the application. In our logon example, we are also creating objects to check the user's identity—database connections and recordsets. If any of these processes fail, the state of our application is invalid. In this case, with the login page for example, we must be sure to set the 'logged on' status to False.

The easiest way is to examine the object variable that is now supposed to be referencing the object instance. We can use either IsEmpty() or IsObject() for this, or compare it the standard value Nothing. It depends on whether we are referring to the object itself or a variable that holds a reference to it. In both cases, for it to work, we have turn off error checking with an On Error Resume Next statement first:

On Error Resume Next
Set oConn = Server.CreateObject("ADODB.Connection")
If IsObject(oConn) Then
  '... success, OK to continue
Else
  '... error, could not create object
End If
On Error Resume Next
Set Session("MyConnection") = Server.CreateObject("ADODB.Connection")
'now check if it failed
If Session("MyConnection") Is Nothing Then
  '... got an error
'or use this method
If IsEmpty(Session("MyConnection")) Then
  '... got an error


Designing for Client/Server

We spent some time early in this chapter looking at what client/server really is, and how we need to build applications to a structure that makes sense for the environment where they are used. In our case, whether on the Internet or an Intranet, we are looking to share processing as evenly as possible between the client and the server. This involves moving the business rules, wherever appropriate, to the client—and leaving the server to do the actual processing work.

The browser (our client) can no longer be just the dumb terminal that we’ve been assuming all the way through this book so far. It has to do its share of the work where appropriate, and to achieve this we need to extend our application's processing across the network. We need to be able to program the client. Much of the rest of this book details how we can do this, and how we integrate the processing at both ends of the wire into a seamless application.

To finish off this chapter, we'll look briefly at the methods available to do this, and consider how this affects the design of our applications. We'll be considering three main areas:

Designing to match the Business Rules.

Different ways to use a browser.

Client-side scripting and components.


Designing to the Business Rules

The key to developing successful client/server applications is to develop the objects within it using the business rules. Allowing a programmer to create their objects as they go along, without understanding the ultimate needs of the application and the business it must support, leads to long term maintenance and expansion problems. Any team developing client/server applications, or any other programs, needs to talk to the business team, so that they are aware of the problems they face, and the solutions they need.

As an example, imagine building a house. If it was built according to the builder's needs, i.e. speed, ease of construction, and minimum cost, the house would be the functional and not overly complex. However, the people who actually live in the building may find it confining, unsuitable for their domestic requirements, and when it comes time to redecorate they may find it too expensive to realize their wishes.

But didn't the team leader at your last project meeting say that the current main aims of the project were speed, ease of construction, and minimum cost?

Use Good Design Techniques

This is an absolute requirement. You must begin by designing your application and not just start building parts of it to see what happens. Grab a book on client/server systems design (Grady Booch's work is the 'bible'), read and analyze the examples, and then develop. Good design, which matches the needs of the business, will ensure that the application will be maintainable and expandable, and as a by-product the debugging requirements will fall dramatically.

The key to good design is developing the applications as components, be they individual pages, HTML files, or compiled standard or custom components. Active Server Pages allow even script code to be built into modules and used as required. Remember the Server-Side Include command #include? We can use this to insert any file into an ASP file, and it's an ideal way of storing functions and subroutines as text files that are used by several other pages:

<!-- #INCLUDE FILE="MyFunction.inc" -->

By defining and maintaining the same interface, (i.e. names and parameters of the routines it contains) we can update them as required, without affecting the pages that use them.

Use Small and Efficient Components

Active Server Pages isn't supported by a very efficient debugging environment, and they are still interpreted at run-time. So it makes sense to move much of the standard functionality into components where possible. Using small well-defined components can speed up the entire development process, and the execution speed of the application. It has other advantages as well, as you see discussed in Chapter 10. For example we can update the component, and all the pages that use it will automatically take advantage of the changes. It also gives us the opportunity to encapsulate secret business rules inside the component, rather than as script in a page.

The question is how do we define a small component? In reality what we want is the minimum code and the optimum functionality—based on a business rule or business object. Remember, we may have many instances running at a time if they are created at page or session level.

A Sample Page Counter Component

We saw how we can use the Application object and VBScript code to maintain a count of visitors to our site, by counting new sessions. There is, however, a problem with this. The contents of the Application object are lost when it is destroyed, i.e. when the application ends. This may be when the Web server is stopped, or in future when all active sessions have ended. So we need to consider some more solid way of saving the data.

The Microsoft IIS Web site contains a sample page counter component that neatly demonstrates the principles of component design. It's written in C++, and is reasonably compact in code terms. It also has added functionality in that it can maintain a count of accesses to almost as many different pages as we like. The details of the 'hits' to each page are stored in a text file on the server's disk. Before the server shuts down, and at undetermined intervals while it is running, the component updates this file from a memory-based cache of hit counts.

The component is instantiated on any page for which a count is required—it can't be created at session or application level like most other components because it uses the URL of the current page (where it is running) to identify the page:

Set MyCount = Server.CreateObject("IISSample.PageCounter")

This is all that's required to maintain the hit count. Each time the page is accessed by the server, the count is incremented. To get the count for the current page, we just use the Hits method:

Response.Write("You are visitor number " & MyCount.Hits)

To reset the counter for the current page, we just call its Reset method. So this is a simple but highly efficient component, packaged up and available to be dropped into a page easily. You can get a copy from:

http://www.microsoft.com/iis/usingiis/developing/samples


Different Ways to Use a Browser

In a client/server situation, the browser is going to become a lot more than just a way of viewing static pages. It actually becomes part of the application, and so we need to be aware of some of the other ways it can be used. For example, we can embed the browser inside another application, or use a special control called the Web Browser Control that comes as part of the Internet Explorer controls installation.

Alternatively, we can embed other applications within the Web browser, although this gives us only limited opportunity for adding extra features to our applications. We can even use a control called the Internet Transfer Control, that allows us to retrieve pages from the server without actually displaying them—but we can then get at the information in the HTTP stream, the HTML code, and the text contents. You'll see example of this control's use, and embedding a browser in a Visual Basic application, in the Case Study in Chapter 14.

Embedding a Browser within an Application

Typical web browsing involves either surfing from one site to another, or searching for specific information. In many situations, the user simply types in a URL, or clicks on previously defined bookmarks. While this is simple enough, it isn't the most user-friendly method. And if you have a set of predefined sites that the users go to regularly—such as the technical catalogue menu on your Intranet or a shares value monitor on the 'Net as a whole—how do you keep the lists up to date?

Microsoft's Internet Explorer is actually an object, and the program IExplore.exe that we run to start it is just a shell. We can create our own instances of the InternetExplorer object in code in Visual Basic, or most other languages. By embedding the browser within another application like this we have the ability to create a lightweight shell, and maintain dynamic content within that shell.

This has many advantages. For example it can give us a standard corporate interface with pop-up forms, and extra features. The regularly used sites can be catalogued and controlled by the application, even allowing the server administrator to shift all of the users from one site to another, without the clients even realizing that it is happening. Selecting certain sites could be entirely automatic, and the server can control the whole process. Of course, all these techniques are really Intranet-bound, where you can specify the client software directly.

Using the InternetExplorer.Application Object

Creating and controlling the browser in code is easy, and can be done from any application that supports Visual Basic for Applications, or most other programming languages. Here's an example that uses VBA:

Set TheBrowser = CreateObject("InternetExplorer,Application")
TheBrowser.Visible = True

Once we've got the browser instance, and made it visible, we just use its methods and properties like any other object. Appendix F contains a list of the common ones, but here are a few examples:

TheBrowser.Navigate URL:= "http://www.wrox.com
TheBrowser.Top = 100                   'set the browser in the top
TheBrowser.left = 100                  'left of the user's screen
TheBrowser.StatusBar = True            'display the status bar
TheBrowser.GoSearch                    'open the default Search page
TheBrowser.GoHome                      'open the default Home page
TheBrowser.Refresh                     'refresh the current page
TheBrowser.Quit                        'close the browser

Running an Application within the Browser

The browser will quite happily download and display different kinds of documents, as well as the usual defaults of .htm pages, .txt files, and .jpg or .gif images. However, Internet Explorer will also handle standard office documents, like Word .doc files and Excel .xls speadsheets. It hosts a copy of the office application within it's own window, and displays it just like the original application would.

Here, we've used Word 97 to create a document, and saved it in Word's standard .doc format. The template contains custom macros that manipulate the Office Assistant character, and produce a user guide:

This is really only useful in an Intranet environment, because of the size of the documents. And the user cannot normally edit the original, only the copy that is downloaded to their own browser. However, by maintaining local copies of templates, and loading files directly over the network (using the syntax HREF="file://C:\Documents\Lunar.doc", for example) we can achieve a great deal this way.


Client-side Scripting and Components

Before Active Server Pages came along, there were Web browsers that could interpret script code embedded in the HTML, and host Java or ActiveX components. ASP is a very powerful environment that enables quick and easy creation of a huge variety of Web pages. However, there are times when the fact that ASP can only execute script and use objects on the Web server creates limitations, such as we've discovered with client/server applications. Quite often, these limitations can be overcome by using client-side code and a range of special objects, in tandem with pages generated by ASP.

Why Use Client-Side Features?

In the next two chapters, we'll be covering the ways that we can program the client in detail. As just a simple example of the power this provides, think back to the task of performing validation of data entered on a form like our earlier logon code. If the user is supposed to supply a six character user name, and they enter only four characters, verification is bound to fail—but it still involves a trip across the network to the Web server, and back again, just to inform them that they didn't enter enough characters. Client-side code can do this validation, and inform the user of their mistake, before the form is actually submitted to the server.

But we know that it's generally much easier to perform complicated computations and tasks on the server and send back a completed page consisting of solely HTML, than it is to send a page and attempt to perform many of the same tasks via HTTP (especially with today’s technology). And since we know the kind of browser our page will be viewed on, we can modify our page so that it suits that browser well. So there are two sides to the argument.

Sharing the Processing Load

To decide how you split the processing in your applications, keep in mind the different layers of the client/server model, and the arguments for and against client-side programming in your particular environment. We saw earlier that the business rules should exist on the client-side where possible, and the length of an acceptable user name is a valid business rule. However, as we've seen earlier, actually processing the user name is a server-side job—to maintain security, concurrency and efficiency.

There are also the concerns of browser compatibility. However, next-generation browsers like Internet Explorer and Netscape Navigator have capabilities that are reasonably standardized, and make client-side interactivity a reality. For example:

Need to display data in a format more robust and interactive than allowed by HTML? Just add an object to your page and let it do the work.

Need to provide immediate feedback to someone entering text on a form without requiring that they send another request to your server? Use client-side code to validate their input.

Want to show some snazzy animation or graphic, but don’t know how to do it in HTML? Insert a Java applet or ActiveX control and control it with code in the web page.

The MSN Investor page at http://investor.msn.com shows an excellent example of how client- and server-side code can be combined to produce a really interactive Web page. It uses an ActiveX control to track your portfolio and provide current information on stocks.

New Things to Learn Client-Side

Based on all we've seen up to now, you might be wondering exactly how we can accomplish these wonderful new tricks. How does the browser know what to execute as script code? How exactly does the script code interact with the browser, and with objects on the page? How do objects get from the server to the client in the first place? These are the subjects we’ll be talking about for much of the next chapter, and we'll be putting the techniques to use with ASP in the rest of this book.

Fortunately, there are a lot of similarities between client-side and server-side scripting, and experience working with Active Server Pages code makes it easy to pick up the new topics that we'll be introducing. In many cases, the script language you’re using with ASP at the moment can be used inside a browser. And because you’re used to using objects with ASP, you won’t have to learn a completely new way to use objects within HTML.

The Undefined Programming Platform

The major change is the platform or computer system our code will be running on. If we’re using ASP, we know a lot about our system: it has certain software packages (like database management systems) installed, it’s running a Microsoft operating system, and so on. In other words, it’s a relatively constant environment that we have some measure of control over. The client system is a completely different ballgame.

People viewing our pages may be doing so from a variety of operating systems, with many different browsers, and with different software installed. If you’ve used the Browser Capabilities component we introduced in Chapter 3, you’ve had some experience with the differences in browsers. Some support Java applets, others support Java applets and ActiveX controls, and others support no objects at all.

The same thing occurs for scripting languages: the browser may be able to interpret VBScript and/or JavaScript, or it might not know what a script actually is at all. In fact, our pages may even experience browsers who understand different dialects of these languages. For example, the version of JavaScript in Navigator 3.0 differs from both the versions in Internet Explorer and Navigator 2.0. With ASP, we’ve been using VBScript version 2.0, but many of our Internet Explorer users may only have version 1.0. While the difference between these languages is often minor, it illustrates how different browsers can be, and how this must be a consideration when we design pages with client-side functionality.

In fact, it's a bit like writing a program in Fortran and then giving it to someone else, who may know nothing about programming, to compile. And just for fun, not telling them what language it actually is, but letting them see what happens with the compiler they use for all their other daily jobs.

The Undefined Software Platform

Another important difference in platforms is the software that is installed or available on each system. We know that we can always use the Browser Capabilities component when writing ASP code, because we know that it's installed on our server. When we use objects in our HTML pages, we don’t have any sort of assurance that this is the case. Just because I have a cool ActiveX control on my machine doesn’t mean that you do too. We need to make sure that the code gets sent to the client machine so that it can be executed.

Both browsers that support objects, Navigator and Internet Explorer, have mechanisms to make sure that this happens, and we’ll cover this in Chapter 8. Of course, if you are developing for the corporate Intranet, you can control your whole environment. Selecting the correct browser software solves the problem as far as both the scripting language and object support are concerned. And you can also more closely control what software is installed on each client.

If you’re an old client-side scripting master—as if those really exist in a field as new as this one—you might want to skip ahead to the 'examples' section of Chapter 8. These hands-on examples show the code you’d need to write to perform some common tasks that aren’t possible with just ASP, as well as showing how ASP can be used to enhance previous client-side only web pages.

Choosing Your Applets and Controls

Before we dive into our client-side excursion, we'll finish up this chapter with a look at some of the object terminology you'll be seeing throughout the rest of the book. This will also help you to understand some of the pressures that Web developers are under when designing applications that use client-side objects. Again, on the corporate Intranet, you can control your environment and make more free choices about the technologies you decide to run with.

The two most common external objects used in client-side programming today are Java Applets and ActiveX Controls. Both of these technologies provide the same end-result: a portion of the page dedicated to whatever visual representation the object provides for itself, and a portion of the browser’s processing time for whatever computations the object needs to execute. Objects can range from a simple button to a complete word-processor or spreadsheet in a window.

Looking at Java Applets

Web browsers first supported applets. Created with the Java programming language (similar to C++), these small lumps of code run on any system that can interpret the ‘bytecodes’ that a Java compiler produces. Because Java-enabled Web browsers run on Windows, Macintosh, and Unix operating systems, Java applets can be viewed on any of these platforms. Although some vendors are developing full-blown applications with Java, the primary use of the language today is in creating applets that only run inside Web pages.

Currently Java applets don’t have any standard way to communicate between each other, though Sun has proposed a standard called Java Beans that achieves this. Microsoft already supports applet interaction through the same component object model (COM) that it uses for communication between ActiveX controls and other OLE/COM components. Java applets in today’s Web browsers run inside of a ‘sandbox’, which limits their functionality to operations that can’t hurt the computer they’re running on. Both Netscape Navigator and Internet Explorer support Java applets, although their methods of connecting script code to applets differ greatly.

Looking at ActiveX Controls

Browsers that can host ActiveX controls provide an alternative way to add objects to a Web page. ActiveX controls are components that can plug into any ActiveX control host. Controls have been around for a long time and there are literally thousands of pre-written ones implementing every function under the sun available for purchase or download. In addition to Internet Explorer and the next version of Navigator, a wide range of other applications, including Visual Basic, all Office 97 applications, Visual C++, and Visual J++ can also use these ActiveX controls.

While Java applets are platform independent, ActiveX controls are language independent—they can be created with a wide variety of languages, including C, C++, Java, and Visual Basic. Microsoft has announced their intention to support ActiveX controls on other platforms. Currently, controls only run on Windows operating systems, though COM has been ported to both the Mac OS and Solaris, and we will soon be able to create controls for use on these operating systems.

ActiveX controls in a web page have full access to the computer and can control all of its parts. This is useful because it doesn’t limit developers in what they can do with a control although, without proper safety measures, a malicious control could possibly harm the computer it's running on. Controls achieve safety through a method known as code signing. We’ll cover this topic in more detail in the following chapters, when we talk about security and code download.

So what does all this mean for us? If we're creating a page with an object that needs to be viewable on many platforms, Java is the way to go. If we're developing with Windows and our clients are going to be running a Windows operating system, we might want to use an ActiveX control (or group of controls) instead. This way we can take advantage of the huge base of previously written controls, and the easy development of custom controls offered by Visual Basic 5.0.


Summary

In this chapter, we've wandered across a lot of topics concerned with client/server applications, their design, and implementation. We've also talked about using the processing capabilities of modern browsers to spread the processing load, and minimize the network bandwidth our applications require.

In this part of the book, you're going to be seeing a lot more on these and other related topics. Our aim is to change your way of thinking from being server-centric to application-centric. After all, if you look at all the best sites out there on the Web at the moment, it soon becomes obvious that they are very cleverly designed and constructed client/server applications. And for the internal office network, as you develop your own Intranet, this is the kind of technique that will offer you the fastest payback, and the optimum efficiency.

The main points of this chapter are:

Developing applications with Active Server Pages is a very different task to the traditional static Web sites we are used to seeing. We need to understand how client/server theory and practice are applied to our design and development efforts.

By using client-side programming techniques, we can spread the processing load between the client and the server. We aim to place the data capture and business rules layers of the traditional client/server application model on the client wherever possible.

To make client/server programming work on the Web, using HTTP, we take advantage of the Application and Session objects provided by ASP to preserve state. Unlike a traditional LAN-based application, this is the only way we can provide consistency over the network.

Once we can maintain state in our application, we have ways of linking each phase of our application's environment to the appropriate client. The whole process is no longer anonymous like a traditional Web site.

We need to understand the importance of good application design before we can create professional and efficient client/server applications. To help, we can take advantage of the many prewritten components available, or even create our own as required.

So now, we're ready to look in more detail at the specifics of creating client/server applications. The next two chapters are dedicated to learning how we implement and integrate client-side processing with our server-based Active Server Pages.


Client/Server - Programming the Client

In the previous chapter, we looked at how to go about building client/server applications using Active Server Pages. We discovered that, in order to achieve the kind of flexibility and efficiency we want, we have to look at how we can spread the processing load between the client and the server. To do this, we have to understand two main topics: how we can program the browser (the client), and how we can link the processing to that going on in ASP on the server.

These two topics are the subject of this chapter and the next. We won’t be providing detailed coverage of every possible client-side topic, since this isn’t a book on programming Internet Explorer or Netscape Navigator. However, we want to introduce the concepts and tools, so we can determine when a particular task might be better accomplished with some addition of client-side functionality. To make this decision we need to know what technologies are available on the client, and how they’re used.

This chapter shows how code can be added to an HTML page, and how the browser object model is used. The next chapter talks about using objects on the client, and shows some real-world examples of how combining these techniques with Active Server Pages can be helpful.

So, specifically, in this chapter, we’ll cover:

  • The mechanics of writing scripting code on the client browser.

  • How to connect the script to the browser's objects and events.

  • The browser object model, and how we use it in our programs.

So let’s get right on and look at how script code fits in on client-side.


Scripting on the Client

So how exactly do we write code that runs on the client and uses objects? We’ll start by looking at the HTML page itself and we'll examine where the code goes and how it’s connected to objects (both to respond to events and to manipulate the objects themselves). After this we’ll cover the details of the browser including the sorts of objects that are available for use in our scripts.Later we’ll talk briefly about the capabilities of the two browsers we're most likely be targeting while writing client-side code: Netscape Navigator and Internet Explorer.

While we’ll show you some examples in JavaScript, we’ll tend to stick to VBScript as our primary scripting language, as it is our first choice language in the rest of the book. However nearly everything should remain applicable to both languages—it’s just a matter of making the translation between one syntax and the other. Netscape Navigator supports the same scripting object model that is used by Internet Explorer. In fact, Netscape invented this object model with Navigator 2.0—the first browser to support client-side scripting.


Scripting and HTML

If you’re a developer, you’ve probably seen plenty of "Hello World" examples in your lifetime, but we're going to ask that you put up with just one more. It sets the stage well for the rest of this chapter. If you haven’t, then consider this just one more privilege of learning how to program for the client-side of the Web. Our "Hello, World" sample is going to be simple. The code we’ll write displays a page with some text and a button. Clicking on the button displays a plain message box with the text Hello, World in it. As it turns out, there a number of different ways that we can do this, and we’ll cover them all in this section by writing different code that performs the same end function.

If you have access to your computer, fire up a copy of NotePad (or whichever HTML editor you prefer) and enter the following text:

<HTML>
<HEAD>
<TITLE> Hello Client-Side World </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Our First Client-Side Code </H1>
<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello">
<SCRIPT LANGUAGE="VBScript">
Sub btnHello_onClick
  Alert "Hello World!"
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Save the file as something descriptive, like Hello World.htm, and start up a copy of Internet Explorer. Load up the page we just saved, and click the Say Hello button. If you’ve entered everything OK, you’ll see a message box proclaiming to everyone that you’ve entered the client-side world:

Connecting Code to the Page

So how does this work? Outside of the <SCRIPT> and </SCRIPT> tags (and the code between them), everything on this page probably looks familiar, because it’s just a standard Web page. Our HTML code is specifying a page with a title, a heading, and one button. The button is created with a single line of HTML code:

<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello">

If we’re using a button simply to submit a form, there isn’t any point in giving it a name. However, when using a button with server-side or client-side scripting, we need a way to refer to the button, so we give it a name – btnHello. Using prefixes like btn (signifying a normal button) with our form elements and objects makes it easy to determine what element we’re talking about when writing code. There's a list of the common prefixes in Appendix B.

But what about the <SCRIPT> block? Let’s go over the different parts of this HTML.

<SCRIPT LANGUAGE="VBScript">

All browsers that support scripting in HTML recognize the <SCRIPT> tag. This tag simply tells the browser that all of the text between itself and the closing </SCRIPT> tag is script code that should be interpreted and executed at the appropriate time. The optional LANGUAGE attribute can be used to specify a certain scripting language. Internet Explorer supports both VBScript and JScript (Microsoft’s implementation of JavaScript) while Navigator currently only natively supports JavaScript. Both browsers treat the code inside a <SCRIPT> tag without a LANGUAGE attribute as JavaScript.

The next three lines might look familiar – they’re very similar to a subroutine we’d write on the server in ASP code, with two exceptions.

Sub btnHello_onClick
  Alert "Hello World!"
End Sub

Triggering an Event

The first difference is in the declaration itself. Instead of a name we choose ourselves, we have the name of the button, an underscore, and the word onClick. This is an event declaration, and it’s often used in client-side programming with objects. Like most objects, the button has properties and methods, but it also has a third feature associated with it, namely events. Methods and properties are a way for the programmer to look at the object’s state, or tell it to do something. Events, in contrast, are a way for the object to tell the programmer that something has happened to it.

When an event occurs, the programmer has the option to do something about it. Different objects have different events, depending on what the object does. Buttons have an event named onClick that is fired when the button is clicked. As we’ll see later, other objects may not have an onClick event, but they will most likely have other events that signify different things have occurred.

In our code, the first part of the event handler, btnHello, is just the name of the object that the event is coming from, and the onClick part is the name of the event we’re handling.

Displaying Message Boxes

The only remaining part of this subroutine is the line:

 Alert "Hello World!"

This surely isn’t a line we would have seen while writing ASP code, because it pops up a message box that suspends processing—not something we’d want to happen on a server! On the client-side, however, something like this can be very useful to give feedback to users. The code here just displays a message box on the screen with the text Hello World!. The Alert keyword is actually a method of the Window object, but we'll look at that a little bit later.

This example is simple, but it shows some very important foundations for our future work with client-side scripting. Any code to be executed by the browser needs to be placed inside both the <SCRIPT> tag, and generally includes an appropriate event handler that will executed.

However, there is a way to cause code outside of an event handler to be executed as the page is interpreted, but it isn’t used as commonly as the event handler syntax. In addition, this functionality is of almost no use to someone using ASP—it mimics what ASP can do, but with much less power. We’ll talk about this technique briefly later in the chapter.

Other Ways to Connect Up our Code

The previous example required us to name our event handlers rigidly—in the form ObjectName_EventName. There are a few other ways to connect code to an event, and we’ll explore them here.

Specifying Scripts as HTML Attributes

First off, we can specify the routine that should be called, when we write the HTML that creates the object. If you still have the above example open in NotePad, replace the <INPUT> and <SCRIPT> lines with the code below:

...
<BODY>
<CENTER>
<H1> Our First Client-Side Code </H1>
<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello" ONCLICK="Pressed"
LANGUAGE="VBScript">
<SCRIPT LANGUAGE="VBScript">
  Sub Pressed
   Alert "Hello World!"
  End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Reload the page, and press the button and you get the same result. Even though we’ve changed the code a bit, we’re still seeing the message box displayed by our subroutine. The code is different, but not by much. All we’ve done is change the name of the subroutine in the <SCRIPT> block, and add the ONCLICK and LANGUAGE attributes to the <INPUT> tag:

<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello" ONCLICK="Pressed"
LANGUAGE="VBScript">

When the browser sees this, it knows to look for and run the code in the routine called Pressed when the button is clicked. The routine can be named anything we like, because the linkage is made explicitly in the <INPUT> tag, not implicitly by the name of the routine. The <INPUT>, <A> (anchor), <FORM>, and <BODY> tags all support this syntax, but with different events. Keep in mind that this method only works for these specific tags, and not for other objects.

This is fine until we start inserting our own objects and want to script them also. So we'll need an alternative method that can be used to hook up script code for all objects. But first we'll look at how JavaScript can be used interchangeably with VBScript.

Using JavaScript Code

Code can be hooked up with events in both VBScript and JavaScript, but JavaScript doesn’t support the syntax we showed you. To see it in action, you'll need to replace the code we’ve been using so far with this JavaScript code:

...
<FORM>
<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello" ONCLICK="Pressed()"
LANGUAGE="JavaScript">
</FORM>
<SCRIPT LANGUAGE="Javascript">
  function Pressed()
  {
   alert("Hello World");
  }
</SCRIPT>
...

The only real differences here are the addition of parentheses to the function name in the <INPUT> tag and the actual change from VBScript to JavaScript code in the <SCRIPT> block. Notice, though, that we need to use Pressed(), instead of just Pressed, in the <INPUT> tag's ONCLICK argument, because JavaScript uses only functions and not subroutines.

Other Ways to Specify Scripts in HTML Attributes

As mentioned previously, the method of adding an attribute to the HTML tag to specify the name of the routine to execute, only works for certain HTML elements. Once we go beyond the integral parts of HTML to use objects in our pages, we’ll need a more robust way to connect code with these objects. The ObjectName_EventName method we used in our first example will work correctly, or we can use the following alternative.

This method creates separate script blocks for each event we’ll handle, naming the event and the object in the opening <SCRIPT> tag. Here’s the code:

...
<BODY>
<CENTER>
<H1> Our First Client-Side Code </H1>
<INPUT TYPE="BUTTON" NAME="btnHello" VALUE="Say Hello">
<SCRIPT FOR="btnHello" EVENT="onClick" LANGUAGE="VBScript">
   Alert "Hello World!"
</SCRIPT>
</CENTER>
</BODY>
</HTML>

The above code says to the browser: when the object called btnHello (FOR="btnHello") fires an event called onClick (EVENT="onClick"), then execute the following code using the VBScript interpreter (LANGUAGE="VBScript").

In this method we do away completely with a separate named subroutine or function, and instead we keep the code in a script block of its own. In effect, the <SCRIPT> tag together with the FOR and EVENT attributes is our subroutine declaration, because this is what tells the browser when to fire the code inside the <SCRIPT> block. This style will work with events from any form element, or from any arbitrary object that we might insert using the methods we’ll talk about in the next chapter.

There is an even more compact method, though it's generally only suitable for simple, one line routines like our "Hello World" example. This is called inline scripting. We just put the actual code we want to execute in the <INPUT> tag, as the ONCLICK argument:

...
<BODY>
<CENTER>
<H1> Our First Client-Side Code </H1>
<INPUT TYPE="BUTTON" VALUE="Say Hello" LANGUAGE="VBScript" ONCLICK="Alert 'Hello World!'">
</CENTER>
</BODY>
</HTML>


Reading Properties and Executing Methods

In the last few pages, we’ve concentrated on the different methods that we can use to connect code with events fired by objects on a page. However we've stayed simple as the only code we’ve been executing is a simple routine that displays a message box on the screen. If that’s all you ever want to do, then you can get by with skipping the rest of this chapter. If, however, you’re like the rest of us, and need to do other things with your code, then you’ll be interested in this section which shows how code executes the methods of different objects, and how you can read and modify an object's properties.

Switch to or open up NotePad again, and enter this new code:

<HTML>
<HEAD>
<TITLE> Methods And Properties </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Using Methods and Properties </H1>
<FORM NAME="frmTest">
New URL:
<INPUT TYPE="TEXT"  NAME="txtURL"><p>
<INPUT TYPE="BUTTON" NAME="btnURL" VALUE="Change URL">
</FORM>
<SCRIPT LANGUAGE="VBScript">
Sub btnURL_onClick
Dim newURL
newURL = Window.Document.frmTest.txtURL.Value
Window.Alert "The current URL is " & Window.Location
Window.Alert "The new URL will be " & newURL
Window.Navigate newURL
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Next load the page into Internet Explorer:

If you enter a valid URL into the text box and press the Change URL button, then you'll get a message box displaying the current URL. If you click OK to dismiss this first message box, another message box appears which displays the URL entered in the text box. After clicking OK a second time, the browser loads the page specified by the URL we entered, and displays it.

The code that does this looks a little more complicated than last time, but it’s quite logical—and it shows many things we’re interested in learning about. Specifically, the code on this page reads properties and calls a method, and—as a bonus—it even uses a variable to do some of its work.

How It Works

Since we’ve covered how to hook up code to events, we’re now free to focus just on the code inside the <SCRIPT> block. We’ll talk about each section in turn.

Dim newURL

The Dim newURL line declares a variable named newURL, which will be valid for the duration of this subroutine execution. This line isn’t absolutely necessary, because VBScript doesn’t require variable declaration. However, it’s always good practice and makes the code we write easier to read and understand.

It’s the next line that’s more interesting. The horrendously long string starting with Window is actually just a way to access the property we’re interested in, which in this case is the value inside the text box on the form:

newURL = Window.Document.frmTest.txtURL.Value

Generally, accessing properties (to read or write to them) is just accomplished by using this syntax:

Assigning to a variable | VariableName = ObjectName.PropertyName
Setting a property | ObjectName.PropertyName = VariableName

In our case, we actually have to access three additional properties in succession, before we get to Value, the one we’re interested in. Document is a property of the default Window object, frmTest is a property of the Document object (because in our HTML we created a form in the document called frmTest), and txtURL is a property of the frmTest object. Finally, after this long string, we get to what we want. Don’t worry about understanding why exactly we needed to use Document, frmTest, or txtURL here, it's more important to understand how we set and retrieve properties in general. It shouldn’t be too hard – it’s identical to how it’s done in server-side code.

Specifying the Default Window Object

In this example, we always enter the Window object wherever it applies. This isn’t strictly necessary, because the Window object is the default object when we write client-side code. Look at the two lines of code below:

Alert "Hello World!"
Window.Alert "Hello World!"

Both of these lines accomplish the same thing, but the first line doesn’t bother to say Window. This is fine, because Window is always assumed if it’s not entered. In most of our examples, we’ll be taking the shortcut and not specifying Window if it’s not necessary.

The next two lines call our old friend the Alert method, displaying a message box with the current location and then a message box with the location we’re jumping to.

Window.Alert "The current URL is " & Window.Location
Window.Alert "The new URL will be " & newURL

The current location comes from the Location property of the Window object, and our new location is stored in the newURL variable we set earlier. We just concatenate these two values into strings with some descriptive text, and display them in their own message boxes.

Navigating to a New URL

Navigate is a method of the Window object. It takes a URL as a parameter, and changes the current location to that URL:

Window.Navigate newURL

This example just shows how we can call a method of an object using VBScript. It’s simple: use the same ObjectName.MethodName syntax like we would in ASP on the server.

So we’ve covered how our scripts are located in HTML, how they’re connected to events, and how methods and properties are accessed. We’re now ready to get into the second major part of this chapter and talk about how to control the browser itself.


The Browser Object Model

As you’ve learned and worked with ASP, you’ve come to depend on the server object model documentation a lot to answer many queries. When do we need the properties of the Server object? Where is that Cookies collection? Is it the Response object, the Request object, or both? The object model provided by ASP is how we interface with the server, and it’s of utmost importance.

As you might expect, on the client-side, the browser object model is just as important. All of the interactions between our code and the browser take place via the object model, and without a good knowledge of how it is set up, our code either won’t work as well as it could, or it won’t work at all!

For the rest of this chapter, we’ll be covering the different objects in the browser object model, explaining how they relate to each other, and showing you some more examples. We need to understand exactly why we have to say things like Document.frmTest.txtURL.Value.

An Overview of the Browser

In the ASP object model, there are five main objects: Server, Response, Request, Session, and Application. These top-level objects have properties, methods, and subordinate objects where appropriate. Although it’s set up a bit differently, the browser object model follows the same principle of providing access to the features of the browser. Instead of five separate objects, the browser has one top level object called Window. The rest of the browser’s objects are located beneath the Window object in a logical hierarchy of functionality.

The Window object represents the browser itself. It has a number of self-contained properties, like status (the current text of the status bar at the bottom of the browser) that reflect and provide access to the browser. Its methods perform operations that make sense for the browser window, like opening another window and navigating to a new page. And, last but not least, it fires events every time it finishes loading a page, or completes unloading a page in preparation for moving to a new URL or shutting down.

To script programmers, the Window object’s subordinate objects are just as interesting as its immediate properties, methods, and events.

  • The Document object: represents the document currently displayed

  • The Frames collection: represents the frames in the current window

  • The History object: represents the history list of the browser

  • The Location object: represents the current URL that the browser is displaying

  • The Navigator object: represents information about the browser itself, like the version number

*** Insert diagram from 0448 VBScript Page 148 - the object model.

*** Annotate: ' Each frame is in effect a window object ' (as in VBScript book).

Of these objects, the Document object is by far the most utilized by the average script developer. It provides access to objects in the page currently displayed in the browser with the Link, Anchor, and Form collections. If we think back to our previous series of Hello World examples, we’ll realize that we’ve been using the Forms collection of the Document object from the beginning of this chapter.

The rest of this section covers each of these elements in a bit more detail, focusing especially on the Window and Document objects.

Covering every last aspect of the scripting object model would occupy many chapters (and indeed does in many books about client-side scripting). In this chapter we’ll give an overview and cover the most important elements. For more information, please check out the Wrox Press book Instant VBScript, and the Scripting Object Model document available from Microsoft's Web site at http://www.microsoft.com (search for scriptom).

Since the Window object is the root of many useful objects in the object model, we’ll start by looking at it first.


The Window Object

In the previous code in this chapter we’ve already used many parts of the Window object. The Alert method, the Location object, and the Document object and its descendants are all subordinate to the grand-daddy of them all, the Window object.

The table below shows the properties, methods, and events supported by the Window object. The entries in the Properties column that include the string (object) are objects in themselves, accessible through the Window object.

Properties | Methods | Events
name | alert | onLoad
parent | confirm | onUnload
opener | prompt |
self | open |
top | close |
location (object) | setTimeout |
defaultStatus | clearTimeout |
status | navigate |
frames (object) |  |
history (object) |  |
navigator (object) |  |
document (object) |  |

We’ll leave the descendant objects for now, and concentrate on the rest of the Window object—don’t worry, there’s plenty to keep us occupied for a while. In the following examples, we’ll explicitly use the current Window object for the sake of clarity. Don’t forget that, since Window is the default object on the page, most code will work identically without a qualifying Window prefix (note that we always have to use it when declaring an event handler like Window_onLoad).


The onLoad and onUnload Events

We’ll often want to perform some action immediately after a page is loaded, or before someone viewing our page jumps to another link. The Window object provides the onLoad and onUnload events to tell our code when these things happen. Fire up NotePad again and enter the following few lines of HTML and code:

<HTML>
<HEAD>
<TITLE> Window Events </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Events of Window </H1>
Go to the <A HREF="file:c:\">root directory</A> of the C: drive.
<SCRIPT LANGUAGE="VBScript">
Sub Window_onLoad
  Alert "onLoad fired"
End Sub
Sub Window_onUnload
  Alert "onUnload fired"
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Now load this file into Internet Explorer. As soon as the page has loaded and is displayed, we hear a beep and see a message box displayed. This action is caused by the code we’ve connected to the Window’s onLoad event.

Now dismiss the message box and click on the single link on the page. Before we see the contents of the root directory of our C: drive, we get another message box, this time from the onUnload event.

Placing message boxes in the onLoad and onUnload events isn’t very useful— there aren’t many cases where we’d want to do this in real life. However, our example shows how simple it is to execute code when our page is loaded or unloaded.

Note that any action that causes the page to be unloaded, not just clicking to another link, will fire the onUnload event. If the current page has a code in an onUnload handler and we refresh it, or even shut down the browser, the code will be executed.


Window Object Properties

The Window object has a number of non-object properties, which we'll look at briefly in this section. In addition we’ll take a look at the Location object, since it reoccurs in other parts of the object model hierarchy. We’ll leave the other object properties until later in this chapter.

The Name Property

This property returns the name of the Window object, and is read-only. If the current Window object doesn’t have a name, it returns an empty string.

Parent, Opener, Self and Top

These properties return references to a given Window object. In this sense, they aren’t used directly, any more than the Window object itself is used directly. Instead, they’re only used as references to access other properties or methods. For example, the following code will give a syntax error:

Alert "Window.Parent is " & Window.Parent

But this code will work fine, returning the Name of the Parent window:

Alert "Window.Parent.Name is " & Window.Parent.Name

If the current Window object has a parent (like it would if it was part of a frameset), then Parent returns the Window object of the current window’s parent. If the current window doesn’t have a parent, i.e. it occupies the whole browser window, Parent returns the current window’s Window object.

Top is a bit like Parent, and they sometimes return a reference to the same object. However, there is one major difference. Top always returns a reference to the top-level frame in a frameset, while Parent only returns the Window object of the frame immediately above it.

Opener returns a reference to the Window object of the window that opened the current window, or returns nothing if the current window wasn’t opened in code (i.e. by a Window.Open statement). Finally, Self just returns a reference to the current Window object. Window and Window.Self both return the same reference.

In a simple page without frames, these properties aren’t very useful. However, as soon as we start experimenting with frames or multiple browser windows, we’ll find that these properties come in handy.

Status and defaultStatus

Both Status and defaultStatus can be used to set the text displayed in the status bar at the bottom of the browser. The code is as simple as:

Window.Status = "Display me"

and

Window.defaultStatus = "Display me"

In the current implementation of the Internet Explorer object model these properties do the same thing, and are both write-only if you’re using VBScript. This means we can set the text of the status bar, but we can’t read what’s there already, or what we’ve set it to.


Window Object Methods

The last major portion of the Window object’s programmability is embodied in its methods. The methods allow us to tell the browser to do a certain job, or perform some task that it knows how to accomplish.

The Window's Dialogs

The Window object contains three methods that display basic dialogs. We’ve already seen the Alert dialog in our examples. It’s about the simplest dialog we could experience—it just displays one text string and an OK button, but it’s exactly what we need in a lot of cases.

The other two dialog methods are Prompt and Confirm. The Confirm method displays a message box with the string specified in the method call, but instead of providing an OK button, it displays both OK and Cancel buttons, allowing the user to choose to confirm or abort an action. The method returns True value if the user presses OK, or False if the user presses Cancel.

Prompt is another way to request user input. This method displays yet another message box, with whatever string is passed as a parameter, but this time instead of OK or Cancel buttons, it displays a text box allowing the user to enter an arbitrary string. The code below uses all three of these methods, and shows the correct calling syntax for each method:

<HTML>
<HEAD>
<TITLE> Window Dialogs </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Window Dialogs </H1>
<INPUT TYPE="BUTTON" NAME="btnTest" VALUE="Click Me">
<SCRIPT LANGUAGE="VBScript">
Sub btnTest_onClick
Dim retValue
retValue = Confirm("Press OK or Cancel")
Alert "Confirm returned " & retValue
retValue = Prompt("Enter a string", "Default String")
Alert "Confirm returned '" & retValue & "'"
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Pressing the single button on the page brings about a series of dialog boxes, first a Confirm dialog, then an Alert displaying the results of the previous Confirm. After these two dialogs, a Prompt dialog is displayed, and another Alert then shows the result. The second parameter to the Prompt method is the default string that will be displayed in the user entry text box.

If we’re using VBScript, we can get increased functionality with the MsgBox and InputBox statements and functions. Some languages, like JavaScript, don’t have built in dialog functions like VBScript does, and so these functions of the Window object can come in handy.

The Open Method

As is fitting for the object representing the browser window itself, the Window object provides methods to open new browser windows, close a window, and navigate to a new location. We’ll cover the Open method first.

Open is a simple method with many options that increase its complexity. In its simplest form, it just creates a new browser window, pointing to a given URL. For example, the following code creates a window named myWindow, which displays the Wrox Web page:

Window.Open "http://www.wrox.com", "myWindow"

The complexity comes from Open’s optional third parameter, which we’re not using in this line of code. The extra parameter provides a fine level of control over the way the new window is displayed.

The following aspects of the window can be controlled by specifying values in the third parameter of our call to the Open method.

Parameter Text | Variable Type | Function
toolbar | boolean | display a toolbar
location | boolean | display the location text box
directories | boolean | display the special link buttons
status | boolean | display a status bar
menubar | boolean | display the menus at the top of the window
scrollbars | boolean | display scrollbars if the document is larger than the window
resizeable | boolean | allow the window to be resized
width | integer | the width of the window (in pixels)
height | integer | the height of the window (in pixels)
top | integer | the top position of the window (in pixels)
left | integer | the left position of the window (in pixels)

Using this list of optional parameters, we can create a huge variety of windows with different properties. The next few lines of VBScript code create some interesting new windows. Try pasting them into a page and running them to see what you get.

Window.Open "http://www.wrox.com/", "myWindowOne", "toolbar=no, menubar=no, location=no, directories=no"
Window.Open "http://www.wrox.com/", "myWindowTwo", "width=100, height=100, resizeable=no"
Window.Open "http://www.wrox.com/", "myWindowThree", "menubar=no, toolbar=yes, location=yes"

"Browser created by Window.Open: with no toolbar, menubar, location, or directories"

"Browser created with Window.Open: has location and toolbar, but no menubar"

The value of each parameter that we don’t define is unpredictable, so be sure to explicitly turn on or off each feature you do or don’t want to appear in your new window.

In the Window.Open code we’re ignoring the return value of the Open call. This is fine if we don’t care about what happens to the window we’re creating. However, if we want to manipulate it after the Open call, we need to save a reference to the window. Fortunately, Open’s return value is just this reference. We'll show you an example of this in the next section.

The Close Method

The Close method doesn’t have the variety that the Open method does, but it’s still powerful. For example it can end a user’s session, and shut down their browser! Admittedly, in most cases, this would be rather rude—but it does have its uses. In addition to closing the current browser window, it can be used to close other windows that we’ve opened with a call to the Open method. We just need be sure to save a reference to the windows when we open them.

Enter the following code into a new file and save it:

<HTML>
<HEAD>
<TITLE> Window Open And Close Tester </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Window.Open And Window.Close </H1>
<INPUT TYPE="BUTTON" NAME="btnOne" VALUE="Open One">
<INPUT TYPE="BUTTON" NAME="btnDOne" VALUE="Close One"><p>
<INPUT TYPE="BUTTON" NAME="btnCloseMe" VALUE="Close Me">
<SCRIPT LANGUAGE="VBScript">
Dim objNewWindow
Sub btnOne_onClick
  Set objNewWindow = Window.Open("http://www.wrox.com/", "myWindowOne", "toolbar=no, menubar=no, location=no, directories=no")
End Sub
Sub btnDOne_onClick
  objNewWindow.Close
End Sub
Sub btnCloseMe_onClick
  Window.Close
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

Next load it into your browser:

When this page loads, we see three buttons. Try clicking the Open One button. After a moment, we see a new browser window (incidentally, without a menubar, toolbar, location text box, or extra link buttons) appear on our screen. Now press the Close One button, and our new window disappears. Don’t press the third Close Me button just yet! We don’t want to shut down this browser until we’re done experimenting with the code.

The explanation for this behavior is in a few lines of code. First, we declare a variable called objNewWindow to store the reference to the window we’re going to create. Then our Window.Open method actually creates the window, returning a reference to be stored in objNewWindow:

Dim ObjNewWindow
Sub btnOne_onClick
  Set objNewWindow = Window.Open("http://www.wrox.com/", "myWindowOne", "toolbar=no, menubar=no, location=no, directories=no")
End Sub

After this our code to close the window is simple, we just use the objNewWindow variable and the Close method:

Sub btnDOne_onClick
  objNewWindow.Close
End Sub

The code for the third button just shows how Close can be used with the Window object to terminate the current browser instance.

Timers: setTimeout and clearTimeout

These methods of the Window object can be used to execute a function after a certain amount of time. Their use is relatively simple. SetTimeout takes the name of a function, and a time value in milliseconds. After the time value has passed, the function is called automatically.

For example, the following code calls a routine named TimerFunc after 5000 milliseconds (5 seconds):

ID = Window.setTimeout("TimerFunc",5000)

Once we’ve started a timer with setTimout, we may find that we want to cancel it so that the function specified in the setTimeout call isn't executed. This is where the clearTimeout function comes into play, assuming we’ve saved the return value of the setTimeout function. In the line of code above we’ve saved our return value in a variable called ID, and it’s this variable that we’ll use in our call to clear the timer:

Window.clearTimeout ID

If we call clearTimeout with an ID value that doesn’t exist, then nothing will happen and any timers we have active will continue to work.

Note that the setTimeout call only executes the function that it is passed once. If we want to have a routine called repeatedly, we need to reset the timer with another setTimeout call within the function that’s called by it.

The Navigate Method

The very useful Navigate method brings us to the end of our Window object odyssey, at least where the non-object properties are concerned. Navigate takes a string parameter and navigates the window object it is called from, to the URL in that string. We saw Navigate in action way back, when we were still talking about connecting code to events. Its use is logical as the line of code below simply redirects the current browser to the URL of the Wrox Press home page:

Window.Navigate "http://www.wrox.com/"

Note that we’re not limited to using Navigate with just the current Window object. We can also use it in combination with the top, parent, and opener properties to act on other windows or parts of the window – all Navigate requires is that it be called from a valid reference to a Window object.


The Window's Location Object

The Window’s Location Object

An alternative to using Navigate is to change the HRef property of the Window object’s Location object. The effect is the same. The browser window always shows the page at some URL—so it makes sense to have some way to represent the various properties of this spot on the World Wide Web or on an Intranet. The Location object is the way we do it.

Paying attention to this object now will save us some time later— the properties of the Location object are reused more than once in the rest of the browser object model. You’ll find that the Document object has its own Location property, and that all of the links on a page are represented by a set of properties that mimic the properties of Window’s Location object.

The mother of all Location properties is HRef. Out of the eight total properties that are a part of Location, seven are derived from the HRef property—and the eighth property is HRef itself. HRef returns the current URL for the page in the browser. If the page is the top level page of a frameset, it returns this top level URL.

In addition to reading the current location, we can change it simply by setting this property. For example, the following line of code redirects the browser to the Wrox Press home page. Window, in this instance, is optional:

Window.Location.href = "http://www.wrox.com"

The remainder of Location’s properties are born from the HRef property. To illustrate what we mean, suppose we were viewing a page with the URL:

http://www.wrox.com:80/asp/book.htm?abc.

The following table shows how each of the properties would be set (a hash mark in this context is a # in the page’s HTML):

Property | Value | Description
href | http://www.wrox.com:80/asp/book.htm?abc | complete URL
protocol | http: | URL’s protocol
host | www.wrox.com:80 | hostname and port number
hostname | www.wrox.com | name of host
port | 80 | port number (default is 80)
pathname | /asp/book.htm | path after host
search | ?abc | any query string
hash | (nothing – would contain any hash specified with #) | any hash value


The Document Object

The Document object represents the HTML document currently displayed by the browser. Its properties reflect the characteristics of the page, and allow script code to manipulate and react to different conditions. Among the most interesting properties are the anchors, links, and forms collections, which represent all of the anchors, links, and forms respectively, on the page. The Document object also supports some methods that can be used to change the HTML displayed by the browser. These methods duplicate much of the functionality that ASP provides, but differ in that the HTML manipulation is done after the document is transmitted from the server instead of before. When these methods are used, the server sends an HTML document with embedded script code, and the browser interprets it and displays the resulting HTML. The Document object has no events.

This section covers the Document object and it’s descendants, with one exception. One of the most used parts of the Document object is the Elements collection of the Document's Form object. This part of the object model is used when interacting with the elements of an HTML form, like buttons, text boxes, radio buttons, and so on, and is important enough to warrant its own section. We’ll cover the Elements collection and the Element object as soon as we’re through with the Document object. Finally, when we cover the anchors, links, and forms collections, we’ll also talk about the objects that make up these collections: Anchor, Link, and Form.

So, without further ado, lets get right into it by talking about the methods that are part of the Document object.


Methods of the Document Object

Like we’ve said before, the methods of the Document object are all concerned with modifying the HTML of the document. As usual, a quick example is often the easiest way to explain what we mean. Fire up NotePad once again, and enter the following code:

<HTML>
<HEAD>
<TITLE> Document Methods </TITLE>
</HEAD>
<BODY>
<H1> Document Object Methods </H1>
<SCRIPT LANGUAGE="VBScript">
Document.Write "The current time is " & Now & "."
</SCRIPT>
</BODY>
</HTML>

Load up the page and look at the results. It's displaying the current date and time—even though we didn’t enter it in our HTML or have the file processed on the server before it was sent to the client. The language interpreter on the client machine is still interpreting our code—it’s just generating HTML instead of responding to an event and doing things like popping up message boxes or navigating to a new page. If we view the source for this page we’ll still see our script code and not the results, unlike if the script was executing on the server where it would just send us HTML.

All of the Document object’s methods work this way – they modify the HTML as they’re interpreted. If you’re curious you could try a little experiment to show that ASP code is indeed executed earlier than client-side code. Add a few lines of ASP code to the example above to read the current time on the server and output it to the page. When the browser finally finishes rendering the page, you’ll see two time values that differ by a second or two—even if the browser and server are running on the same machine.

The Write and WriteLn Methods

Lets take a look at the Write method in a little more detail. This method simply takes a string, and outputs it as part of the HTML stream. Because the text is actually interpreted by a script engine, it can include variable references and function calls, in addition to plain text. The interpreter executes all of the calls and resolves all variables, before sending a final string to the browser.

Since the string is going to the browser, it can (and should) include whatever HTML markup tags are needed. For example, the following Document.Write line displays the current date and time in the Header 1 font of the current browser:

Document.Write "<H1>" & Now & "</H1>"

Calls to the Document.Write method can be interspersed with standard HTML, just as calls to the Response.Write method can be intertwined with HTML on the server.

WriteLn is nearly identical to Write, with one difference that may be important to us depending exactly upon the point at which we need to use the Write methods. WriteLn writes a string to the document just like Write, however it appends a new-line character to the end of each string. Since the HTML specification ignores white space and new-line characters when interpreting standard text, the browser output may look the same, independent of which Write method is used. The only place we’ll see a difference is when the output we’re writing is inside a formatting tag that pays attention to line-feeds. For example, when WriteLn is used inside of a <PRE>...</PRE> block, separate WriteLn statements will result in text that is displayed on separate lines.

One last question you might have regards what happens if Write or WriteLn are used after a page is already loaded—as part of an event handler, for example. We're going to postpone the answer to this question until we’ve covered the Document’s Open, Close, and Clear methods in the next section. You’ll see why afterwards.

The Open, Close and Clear Methods

As we’ve seen in the example above, it’s possible to use the Write or WriteLn methods by themselves. Sandwiching our calls to Write and WriteLn between a call to the Document object's Open and Close methods gives us more control over when the strings are outputted to the page. In effect, using these methods is like using the buffering ability ASP provides with the Response object.

Although the documentation claims that calling Open after the output stream already has data will clear the output stream, we haven’t been able to see this behavior with Internet Explorer 3.01. Our recommendation is to experiment with these methods on the latest version of your browser, and determine how close your browser, and the browsers your customers may have, come to the original specifications. This omission on the part of the browser isn’t catastrophic—in many cases this functionality isn’t necessary. Perfect software is great, but certainly not a reality in an imperfect world.


Non-Object Document Properties

Besides the object-based properties, i.e. the Location object and the Anchor, Link, and Form collections (which will be covered in a moment), the Document object has a number of other properties that we might find useful in our client-side programming. We’ll briefly outline these properties next.

The Color Properties

As is fitting for an object that represents the current HTML page, there are a number of Document object properties that represent the color scheme of the current page. All of these properties return a seven character string when queried, in this form: #RRGGBB, where the R, G, and B values represent the current color setting for red, green, and blue, in hexadecimal.

Property | Description
linkColor | Color of non-visited links in the current document
vLinkColor | Color of visited links in the current document
aLinkColor | Color of active (clicked on but not visited) links.
fgColor | The current foreground color
bgColor | The current background color

These properties are set to read-only after the page has been parsed and displayed, so they can’t be changed in response to button clicks or other external input. They can, however, be set in script blocks – just make sure the code is parsed when the page is loaded, like it is with Write and WriteLn.

The Title Property

The Title property simply returns the current title of the document. The value of this property is whatever the HTML author placed between the <TITLE> and </TITLE> tags. This property is set to read-only after the page has loaded.

The Cookie Property

Cookie is a very useful read and write property, and can be used in conjunction with ASP’s Response.Cookies collection. This property returns or sets the cookie associated with the current page. Reading the Document.Cookie property returns the keys and values of the current page’s cookie, but doesn’t give other information. While this information isn’t returned when querying the property, we can set such things as expiration date and path when setting a cookie using client-side code.

To read the cookie into a variable named MyVar, we use the following code:

MyVar = Document.Cookie

After reading the value we can use the string-manipulation capabilities of our scripting language to find the part of the cookie we’re interested in.

To set the value of a key named MyKey to MyValue, we use this code. If MyKey already exists, its value will be replaced with the contents of MyValue:

Document.Cookie = "MyKey=MyValue"

Finally, if we’d like to set the path or other properties, like the expiration date, we just append them to the end of the key/value string, like this:

Document.Cookie = "MyKey=MyValue;expires=Monday, 01-Jan-98 12:00:00 GMT"

Cookies can also be used solely on the client-side to exchange information between pages. On the server we’re used to using the Session object to remember information from page to page, and script to script. There isn’t a handy object like this on the client, but simply by setting the cookie property of the document with a key/value pair that holds our variable name and its value, we can remember the contents of variables across pages. This method doesn’t work with object references, only with text and numerical values, but it can still be very useful. Microsoft’s VBScript site has a sample that shows the code needed to add this functionality to our pages.

The LastModified Property

This read-only property returns the date that the current document was last modified. Note that this property is only up to date as far as the most recently retrieved copy of the document—which may be older than the last modified version if our browser caches any pages (as most do).

The Referrer Property

On paper, the Referrer property returns the URL of the document that contained the link to the current page. In the real world, this is less than true. Internet Explorer 3.0’s Referrer property always returns the URL of the current page.


The Subordinate Objects of the Document Object

OK, we’ve covered a lot of Document’s functionality in the last few pages, but we’re not finished yet. We still have some of the most useful things to talk about. Not surprisingly, these are actually objects in themselves. We’ll be talking about the Anchor, Link, and Form objects (as well as their respective collections) and the Location object (which, all by its lonesome self, doesn’t have a collection). All of these objects are generated when the page is parsed, and they reflect the properties of the page currently in the browser.

Like we’ve said, three of the four objects we’ll be talking about here are actually stored in collections. Objects inside collections can’t be referenced in code without the collection being named first in some manner. After covering the objects themselves, we’ll talk about some special cases that can arise when programming with collections. You've already had experience with many ASP collections, like Request.Cookies, and Request.Form, and you can rest assured that using collections on the client is nearly identical—only the names of the collections, the objects, and their properties and methods, are different.

The Link Object

The Link object represents something you’re very familiar with if you’ve ever browsed the web—a hypertext link on a page. The Links collection is made up of Link objects, one for each anchor tag containing an HREF attribute (<A HREF="...">) on the page. We can’t change the links on the current page when browsing, and so, not surprisingly, all the elements of this collection, and their properties, are read-only. The link object has nine properties and two events, but no methods.

Link Object Properties

Guess what? The properties of the Link object (with one addition) are identical to the properties of the Location object we covered earlier. We won’t repeat them here, but if you need to you can refresh your memory a few pages back.

In addition to the eight properties that it shares with Location, a Link object also possesses an additional property called Target. This property is blank unless the link has a TARGET attribute used to refer to a new browser window or frame. For example, the Target property of the following URL is MainFrame:

<A HREF="http://www.mysite.com" TARGET="MainFrame">

Link Object Events

Viewers of our page can do things to links: they can click them or move their mouse over them. The events supported by Link allow us to respond appropriately to these actions.

The Link object has an onMouseOver event that is fired when the user moves the mouse over a link. We can do some cool tricks with links by hooking up code to this event. Since links really don’t have a name per se, it’s easiest to specify the desired behavior in the anchor tag itself:

...
<A HREF="http://bossman" onMouseOver="MyFunc">Bossman<A>
...
...
<SCRIPT LANGUAGE="VBScript">
Sub MyFunc
  Alert "Moved"
End Sub
</SCRIPT>
...

In this example, the combination of the anchor tag’s onMouseOver attribute and the MyFunc subroutine causes an alert message box to be displayed whenever the mouse is moved over the link. Use this for demonstration purposes only. Doing something exactly like this could be annoying to the viewer of our page— they’ll have a hard time clicking on the link if they’re always trying to dismiss a message box first!

Link objects also have an onClick event that works the same as the onMouseOver event, except that it is only fired when the link is actually clicked on. To hook up code to this event we just add an onClick attribute to the HTML for our link, like this:

<A HREF="http://bossman" onClick="MyFunc">Bossman</A>

If we’re not going to do much in the function, we might find it easier to just include our code within the anchor tag, as in-line script. We outlined how to do this in the 'hooking up the code' section at the beginning of this chapter.

The Anchor Object

The Anchor object is one of the simplest objects we’ll see in any object model, and it isn't very useful in most client-side programming. In HTML an anchor is simply an <A> tag. In the object model, we have access to a collection of these tags via the Document object’s Anchors property. Anchor objects have one property, Name, which is set to the NAME attribute of the given tag.

Internet Explorer 3.01’s Anchors collection only includes HTML elements with a NAME property and without an HRef property.

The Form Object

In contrast to the Anchor object, the Form object is supremely useful to client-side programmers. Its properties encompass such information as the current ACTION address, the METHOD, and the encoding for the form. Its sole method and event encapsulate form submission. In addition to the normal properties, each Form object has an Elements collection that provides access to each HTML element (the text boxes, buttons, etc.) on a given form. We’ll cover this very important collection as soon as we finish talking about the Form object itself.

The Form Object Properties

The five properties of Form are Action, Encoding, Method, Target, and Elements. This section talks about the first four properties. These are derived from the opening <FORM> tag. So, like we did with the Location object, we’ll invent a tag and then show the resulting property values. Our tag for this demonstration will be:

<FORM NAME="myForm" ACTION="http://www.mysite.com/search" METHOD="GET" TARGET="NewWindow" ENCTYPE="text/html">
Property | Value | Description
Action | http://www.mysite.com/search | Action for form processing (usually a URL)
Method | GET | Form data submission to server (GET or POST)
Target | NewWindow | Name of target window to display results in
Encoding | text/html* | Encoding for the form

* IE 3.01 doesn’t display or allow this property to be set correctly

These properties can be read or set to determine the current form settings, or possibly to change the behavior of the form before it is submitted.

Submit and onSubmit: The Form’s Method and Event

The Form object’s single method (Submit) and event (onSubmit) handle form submission. Calling Submit submits the form, with the same result as a viewer clicking on a Submit button in the form.

As you’ll see in the simple code in this section, and in a detailed example in the next chapter, client-side code can be very useful in performing validation of data entered on a form before the form is submitted. By doing this, we save a trip to the web server and back just to inform a user that they entered invalid data. Keep in mind that this only guarantees that information coming from our page is properly validated and acceptable. It doesn’t actually mean that any data received is OK.

For example, there’s nothing stopping someone from writing an application that sends data to our Web server, and in this case the data will never have seen the validation code in our Web page. The morale of the story is: if it’s extremely important that only certain data ends up in your database, then you'd better check it at the server end too. However, most applications don’t require this depth of protection.

The page below submits the form when the Submit button is clicked, unless the user has entered "no" in the text box—in which case the form is not submitted:

<HTML>
<HEAD>
<TITLE> Form Submit </TITLE>
</HEAD
<BODY>
<H1> Form Submit </H1>
<FORM NAME="myForm" ACTION="http://mysite.com/" METHOD="GET">
   <INPUT TYPE="TEXT" NAME="txtOne">
   <INPUT TYPE="SUBMIT" NAME="sbmTest">
</FORM>
<SCRIPT LANGUAGE="VBScript">
Function myForm_onSubmit
   If Document.myForm.txtOne.Value = "no" then
      myForm_onSubmit = False
   Else
      myForm_onSubmit = True
   End if
End Function
</SCRIPT>
</BODY>
</HTML>

Looking closely at the code on this page, you might notice something odd: the handler for the onSubmit event is a function, not just a normal subroutine. Every other event handler we’ve written has always been a subroutine, so why the change now? The answer is in the way Internet Explorer and Navigator are set up to process the onSubmit event. In this one single case, they look for a return value and act accordingly. If this event returns False, the form isn’t submitted. If it returns anything else, the form is submitted normally.

An alternative to this method is to create a normal (i.e., non-submit) button with an <INPUT TYPE="BUTTON"> tag. In the handler for the onClick event of this button, we perform our validation. We only submit the form, using the FormName.Submit method, when the data meets our requirements. The next chapter includes a more in-depth example of client-side validation.

The Elements Collection

Each Form object has an Elements collection, which represents the HTML form elements inside a pair of <FORM>...</FORM> tags. Any element that can be created with HTML (including objects created with the <OBJECT> tag) can be represented in the Elements collection.

As we’ll see in the next section, it’s important to correctly name the form where the object we’re interested in resides (both in script code and in HTML). If this information is incorrect, the browser won’t be able to find the object we’re talking about, and will give us a rude error message saying that the object doesn’t exist! If we don’t identify the object correctly, as far as the browser is concerned, it doesn’t exist.

Once we have access to the elements of a form, we might want to know how they act. We’ll cover the HTML form elements next.

HTML Form Elements and the Element Object

HTML form elements can be very different. We can create text boxes, buttons, check boxes, and more with HTML. Accordingly it will probably come as no surprise to you, having worked with these elements before, that the properties, methods, and events for these elements are also different. Fortunately, however, similar objects usually have the same properties, methods, and events, making our job of understanding them easier.

Rather than cover how to use each and every one of these elements, and duplicate a lot of code and text in the process, we’ll instead provide a table showing the properties, methods, and events for each object, and some general comments about these characteristics.

For a much more detailed coverage of this topic, check out the Wrox Press book Instant VBScript.

If you’re interested in actual code (and who isn’t?), be sure to take a look at the in-depth examples in the next chapter, and at the code samples throughout this chapter.

Element | Properties | Methods | Events
button, reset, submit | form, name, value | click | onClick
checkbox | form, name, value, checked, defaultChecked | click | onClick
radio | form, name, value, checked | click, focus | onClick
password | form, name, value, defaultValue | focus, blur, select | None
text, textarea | form, name, value, defaultValue | focus, blur, select | onFocus, onBlur, onChange, onSelect
select | name, length, options, selectedIndex | focus, blur | onFocus, onBlur, onChange
hidden | name, value | None | None

Many of these properties will be familiar to you from prior reading and work, but we’ll review them briefly below.

The Element Object's Properties

Fortunately for us, each object has a Name property. If it didn’t, we’d have a hard time referring to the object in code, and the object model wouldn’t be much use to us. Other useful properties include Form and Value. Form returns a reference to the element’s form object, while Value gets or sets the value of the element. While all the elements have a Value property, the actual contents of this property vary depending on the object itself. For example, the Value of a text element is the string in the text box, while the Value of a button is the button’s caption.

The Element Object's Methods

Some combination of the click and focus methods is common to all elements except for the hidden element (which has no methods or events). The click method causes the same result as a user clicking on the object. Calling the focus method of an object moves the current input location, or focus, to the object. For example, the code:

txtOne.Focus

moves the input cursor to the text box object named txtOne. The blur method is the converse of focus; it removes the focus from the current object. Finally, the select method, a feature of the password, text, and textarea input elements, selects the current contents of the object. This has the same effect as if the viewer of the page had clicked and dragged the mouse across the text in the input element.

The Element Object's Events

We’ve already seen one input element event many times: onClick. Each object that has a click method also has an onClick event, where code is placed ready to be executed when the object is clicked. Other interesting events that can be used in a similar way are onFocus (called when the element receives the focus), onBlur (called when the element loses focus), and onChange (called when the Value property of the object changes).

Referencing Objects in Collections

Now that we’ve covered the most important collections in the Document object, it’s a good time to refresh our memory on how collection objects need to be referenced in code. Just like with ASP collections, items in the collections of the browser object model can be referenced one of two ways: by name or by index.

The choice of these collections has a practical aspect: whenever we refer to an object in a form (a very common client-side task) we need to make sure we reference the object correctly. Remember that the browser creates a Form object in the Forms collection for each <FORM> tag on the HTML page, and an item in the Form’s Elements collection for each element in the form. All collections have the additional property Count, which returns the number of objects that are currently held in the collection.

To illustrate this concept, we’ll use the Form and Elements collection. Suppose a page with a single HTML form laid out like this:

<FORM NAME="myForm">
<INPUT TYPE="BUTTON" NAME="btnOne" VALUE="Click Me"><BR>
<INPUT TYPE="TEXT" NAME="txtElement">
</FORM>

After the page is parsed and displayed the Count property of the page’s Forms collection would hold the value one, as the page boasts a single form, named myForm. The Count property of the myForm.Elements collection would be two, because there are two input elements inside myForm.

Why does this matter, outside of knowing how many objects are in a collection? When referring to objects in code, the index numbers of the collections can be used instead of, or in tandem with, the names of the objects. For example, the Action property of myForm could be referred to with this code, which uses the name of the form:

Document.myForm.Action

or with this code, which uses the index:

Document.Forms(0).Action

Note that, while Count returns 1, the collection is indexed from 0 to Count – 1.

Similarly, the form’s elements could be named in code like this:

Document.Forms(0).btnOne.Value
Document.Forms(0).txtElement.Value

or by index, like this:

Document.Forms(0).Elements(0).Value
Document.Forms(0).Elements(1).Value

It’s important to remember that we need to add this relatively long qualifier when referring to input elements (or OBJECTs) inside a form. If we don’t do this, the browser will think we’re trying to refer to a control that’s not on a form, i.e. that we’re talking about a control that’s on the page itself. Using simply btnOne.Value instead of one of the longer lines of code above will cause an Object required error (unless of course an element named btnOne exists outside the form). Without specifying the form that the object resides in, the browser can’t tell what form element we’re talking about.

Form elements or objects created with the <OBJECT> tag that are placed outside of a <FORM> tag should be named without a preceding object reference—they don't belong to any form, and so they aren’t a part of any form’s Elements collection. And because objects are only created when an <OBJECT> tag is encountered while parsing the page, we need to make sure that any <OBJECT> tags come before script code that references them.


Wrapping Up the Document Object

Now that we’ve fully covered the very important Document object, you should be able to understand why we spent so much time on it. It’s one of the most useful objects in the browser object model. Understanding how Document is organized will make your client-side programming quicker and more painless.

Wrapping up the Document object also leaves us with just a few more objects to cover before we've seen the entire object model.


History, Navigator and Frames

The last part of the object model that we still need to discuss is the remainder of Window’s properties: the ones that are actually objects in themselves. These objects are much simpler than Window and Document, and they aren’t used as commonly as these objects either. However, they can be very useful in certain situations, and warrant a few pages of explanation. Here's the object model again, so you can see how they're related to the rest of the hierarchy.

*** Insert diagram from 0448 VBScript Page 148 - the object model. Repeated from earlier in chapter.

The History object, Navigator object, and Frames collection all lie immediately off of the Window object in the browser hierarchy. These objects, in the order listed, provide information about the browser’s history list, the browser version, and the frames in the current document (if any).


Using the History Object

Using the History object we can navigate through the current user’s history list. It’s important to note that this object doesn’t provide the exact text of each stop the user has made on the World Wide Web, so we can’t loop through each location and snoop out where the user has been before they hit our page (well… at least not directly). We can, however, use History’s methods to go backward and forward any number of links.

The three methods of History are forward, back, and go. Forward and back are, not surprisingly, the opposite of each other. These methods simply take a positive integer and move forward or back in the history list that specified number of locations. The location is relative, in contrast to the go method, where the location is absolute. So, to implement a function that returned the user to the last page they visited, we’d just need to add the following code to a routine and provide a way for the user to execute this code (we’ll show an example of this in the next chapter):

History.back 1

The go method is also interesting, although it’s hamstrung by problems in the current 3.01 implementation of Internet Explorer. Theoretically (according to the documentation) the go method should take an integer and move to that absolute position in the history list. Calling go with the number 1 should cause the browser to display the first page in the history list. Internet Explorer 3.01 doesn’t implement this behavior exactly, but if you have the time or inclination you might like to play around with this method to see exactly how it behaves in your browser.


The Navigator Object

This object provides detailed information about the current browser, including its name and possibly the build number and even operating system. The sample output we got from our copies of the Windows NT 4.0 version of Internet Explorer 3.01 and Netscape Navigator 3.01 are outlined in the table below:

Caption: Navigator Object Results with Internet Explorer 3.01

Property | Value
appCodeName | Mozilla
appName | Microsoft Internet Explorer
appVersion | 2.0 (compatible; MSIE 3.01; Windows NT)
userAgent | Mozilla/2.0 (compatible; MSIE 3.01; Windows NT)

Caption: Navigator Object Results with Netscape Navigator 3.01

Property | Value
appCodeName | Mozilla
appName | Netscape
appVersion | 3.01 (WinNT; I)
userAgent | Mozilla/3.01 (WinNT; I)

You’ll probably notice a few things about these results. Firstly, the userAgent property is the same as if we'd examined Request.ServerVariables(HTTP_USER_AGENT) on the server with ASP code. Secondly, the userAgent property is the concatenation of the appCodeName and appVersion properties. Thirdly, the odd names and divisions for these properties. Don’t spend too much time thinking about it— it’s what Netscape came up with when they wrote Navigator 2.0. Some of the original developers of Navigator also worked on Mosaic—the browser that started it all. They wanted Navigator to be a gorilla that would stomp all over Mosaic, hence the name.

So, if we’d like to take some action on the client depending on the exact browser version that someone viewing our pages is using, the Navigator object is the way to go. To do the same thing on the server with ASP, we can use either the HTTP_USER_AGENT header, or the more capable and powerful Browser Capabilities component.


Manipulating Frames in Script

The Frames object is a collection of all of the frame windows in the current frameset. Each frame is a Window object in itself, and has its own Document object to represent the HTML in the frame. This really isn’t as confusing as it might sound—it mimics the organization of frames without an object model. The top-level frame subdivides the browser window and loads separate HTML pages for each frame.

It can be confusing to use this property though. By default the Window and Document object that a script refers to are the Window and Document objects of the current frame. This means that, if we execute a script in the left-hand frame of a two-frame window, the Document, Window, and Frames objects the script uses will be those of the left-hand frame. Since the left-hand frame has no sub-frames, examining the Count property of the Frames collection will return 0.

However, if we use the Top (or, in this case, Parent) property of the left-hand frame’s Window object, we can gain access to the top-level frameset that loaded both the frames. Examining the Count property of this Frames collection will show the expected value of 2, because this top-level page has loaded two frames.

We’re giving this brief coverage here because it’s both a simple and complex topic. In the sense that the Frames collection has only one property, Count, and that it just provides access to other objects that we’ve covered before, like Window and Document, it is a simple object. However, the relationships between the different frames in a frameset can be complex, and to best understand how it works, we should really see some code. A more in-depth example of using frames with the object model is part of the next chapter.


Summary

So that’s it! We’ve worked through a tremendous amount of material in this chapter, but at least we’ve covered the object model thoroughly. You should now have a deep enough understanding to understand the detailed examples in the next chapter, and to know when and how to use the object model in your own client-side programming.

The main points of the chapter are:

  • All modern Web browsers support a standardized object model, for which we can write script using any of the available scripting languages such as VBScript and JavaScript.

  • The object model gives us a way to link our script to the browser itself, and to the pages it is displaying. We can emulate and automate most of the tasks that are possible on the browser.

  • The concept of attaching script to events means that we can react to things that happen in the browser. This is not something that we are used to with server-side scripting.

In the next chapter, you'll stop thinking about the browser as something separate from the server. Instead, as in a true client/server situation, you'll start to think about the actual combination of the ability to create dynamic web pages on the server, while executing code on the client as well. For this, you need to be comfortable with how the browser can handle script that is sent from our Active Server Pages on the server in the first place. We'll show you how this works out in practice. We’ll talk about using new objects, and show you many real-world examples of how client-side code can help to share the processing load.


Client/Server Integration

We're now midway through our jaunt into client-side territory. We spent the last chapter learning about how script code looks in HTML, and how the browser object model provides programmatic access to the functionality of the browser. In this chapter, we'll continue our look at client-side programming. First we'll talk a little more about how we can use objects outside of those that the browser provides. Then we'll move on to the main part of this chapter—real-world examples showing how adding client-side functionality to our pages can overcome programming difficulties, and improve on what ASP allows us to accomplish.

Along the way, we'll see four separate examples. The first, and simplest, shows how we can make our page more 'usable', by simply changing the default focus on an input form. From there, we'll move on to look at how we can use client-side code to validate the entries on a form before we submit it to our server. We'll also have a closer look at using frames in a Web page, with an example that uses ASP to make them responsive to user input. Finally, we'll see how we can improve the user interface by using objects that are not part of the browser itself.

All these examples are aimed at demonstrating some of the ways that the combination of Active Server Pages, client-side scripting and objects can be used to create client/server applications. These, after all, are what real-world use of the browser, especially on a closed network like a corporate Intranet, is all about.

The main points we're covering in this chapter are:

  • A comparison between objects on the client to those on the server.

  • How we create objects in HTML with the <APPLET> and <OBJECT> tags.

  • How we can manipulate these client-side objects in code.

  • How we can create client-side code dynamically on the server.

So, without further ado, let's talk about new objects...


Creating Objects on the Client

You'll probably have noticed that no matter whether we're interacting with browser objects on the client, or server-side objects with ASP, that the code we write is very similar. The last chapter showed how we could connect client-side code to objects, how we could respond to events fired by objects, and how we could call methods and access the properties of objects. We also talked about a number of the objects provided by the browser itself. What we haven't covered is how we create instances of additional objects.

On the server, we'd use the <OBJECT> tag with RUNAT=SERVER or the CreateObject method of the Server object, like this:

<OBJECT RUNAT="Server" ID=MyObject ProgID="MyProjectName.MyClassName">
</OBJECT>

or:

MyObject = Server.CreateObject "MyProjectName.MyClassName"

On the client, things work a bit differently, and the objects themselves are quite different from those we've used on the server. However, the great thing is that our code will generally stay the same, in the same way that the syntax doesn't change much when we move from the server to the client. We create objects and hook up code differently, but the code itself is the same—greatly simplifying our task.


Object Creation in HTML

Client-side objects, like those we discussed briefly in Chapter 6, are created with a variety of HTML tags. The two most common are the <OBJECT> tag and the <APPLET> tag. APPLET is generally only used to specify Java applets, while OBJECT can be used, depending on the browser, to specify any arbitrary object of any type.

Using Java Applets

The use of the <APPLET> tag is relatively simple. Here's a sample example:

<APPLET CODE="MyClass.class" WIDTH=10 HEIGHT=10>
</APPLET>

The CODE attribute's value is the name of the Java .class file that implements the applet. With this name, the browser can request that the server send the .class file. When it arrives, it can interpret it and display the results within the browser, or in a separate window if the applet has been so designed. APPLET also supports the WIDTH and HEIGHT attributes that tell the browser how wide and how tall the space it devotes to the applet should be.

However, hooking up script code to a Java applet isn't as straightforward. How we do it depends on whether we're using Internet Explorer or Navigator, and sometimes (as with Navigator) can even require a complete recompile of our Java applet. We're used to using Active Scripting languages with ASP, and Navigator and Internet Explorer enable script code to be connected to Java applets in very different ways. Since we're not going to be covering these dissimilar methods, we won't spend too much time talking about them here. These browsers don't ship with any Java applets we can use for a demonstration anyway. If you do have the .class file for a Java applet on your machine (perhaps from experimentation with Java development) feel free to load up a page and compare the HTML with that used for ActiveX controls—which we'll look at next.

Using ActiveX Controls

Their programming model is simple, and will be very familiar to someone with ASP experience. ActiveX Controls are created in HTML with the <OBJECT> tag. This tag, developed and endorsed by the World Wide Web Consortium (W3C), is extendable and can be used to insert any arbitrary object (including Java applets). Currently, its most common use is with ActiveX controls.


The Client-side <OBJECT> Tag

This is what a client-side <OBJECT> tag might look like in HTML:

<OBJECT CLASSID="clsid:79176FB0-B7F2-11CE-97EF-00AA006D2776" ID="spnTest"
  CODEBASE="http://activex.microsoft.com/controls/mspert10.cab"></OBJECT>
</OBJECT>

A lot different from the Server.CreateObject method we're used to. It is, of course, the same basic format as the other way of creating objects on the server, using an <OBJECT> tag. Although it doesn't look exactly identical, it is doing the same thing. The string used with a call to CreateObject on the server is called a Programmatic ID or ProgID. This syntax isn't supported on the browser (yet), so the long ugly string making up the CLASSID attribute has to do the same job. It's known as a Class ID or CLSID. You may recall from Chapter 3, however, that we can also use the CLASSID method on the server if we're feeling particularly obtuse.

The second part of the tag, the ID="spnTest" attribute, is simply telling the browser that this object will be referred to in our code by the name spnTest. Finally, the CODEBASE attribute specifies a URL where the control can be downloaded from if it's not already present on our machine. This last attribute isn't supported in the server-side version of the <OBJECT> tag.

ProgIDs, CLSIDs, and Versions

On the server, ProgIDs and CLSIDs are complementary—each ProgID has an associated CLSID and vice-versa. ProgIDs are generally easier for a human to read and understand, so they're commonly used when an ActiveX object needs to be identified. They have one problem though, especially in the Internet world. Suppose I create an object for a card game and give it the ProgID "CardServer.Hand". You visit my page, download the control and create the object, and play my card game. Everything is fine and dandy until you happen to visit someone else's page—for sake of argument let's say they live in Australia, mainly because I'd like to visit Australia.

My Australian colleague and I don't converse very often, and so we've unwittingly created two completely different objects with the same ProgID of "CardServer.Hand". What happens now? Your machine doesn't know whether it can use the object it already has, or if it needs to go out and get a new one. There is no way it can tell whether the control it already has is different from the one on the page in Australia. Either way, whether it keeps the existing one, or goes and gets the new object, one of the pages that uses the "CardServer.Hand" object won't work properly, because we'll only have one of the objects on our machine.

CLSIDs solve this problem. A special algorithm generates each CLSID number when the object is developed. This algorithm guarantees that the CLSID won't be ever duplicated, on this world or in this galaxy, for thousands of years. So we can be sure that an object we design or use in our pages won't ever conflict with another object somewhere else on this planet (or off it!). On the other hand, on the server we should be safe when using the ProgID, because we ought to know what objects we've got, and therefore be able to prevent conflicts.

Object Safety and Code Download

One topic that concerns everyone, especially with the increasing reports of rogue components circulating on the 'Net, is particularly important: how does someone viewing our pages know that the objects we've included will be safe to run on their machine? The code is actually running on their machine, and it's not just a scripting language with a few buttons anymore—it's very powerful. With that power comes the ability to do damage. There are two commonly used methods for providing object safety, sandboxing and code signing. We'll look at both of them before wrapping up this section, by talking briefly about the way that the objects get from the server to the client in the first place.

Sandboxing

The method of safety used by Java applets, and also by the VBScript and JavaScript scripting languages, is known as 'sandboxing'. This name refers to the practice of limiting the capabilities of these languages to actions that are defined to be safe to the machine. For example, since direct access to the hard drive of the machine could allow someone to erase files indiscriminately, sandboxed languages don't often support disk I/O. Disk access is 'outside the sandbox' and the language can't access it. While this method can ensure reasonably safe code, it can be tremendously limiting to the developer. How many applications at our local software store don't use our hard drive? Instead, applets often save information back across the network to the server that's supplying the pages.

Code Signing

An alternative that preserves access to the full capabilities of the machine is called 'code signing'. With this method, the code can do whatever it wants, but—if it does something bad—the user knows who caused the problem and can take appropriate actions. A digitally signed piece of software identifies the person who created and signed it, and that the code hasn't changed since it was signed. One common way to describe code signing has been to think of it as 'shrink-wrapping for the Internet.' The box of software you pick up at the local store isn't guaranteed to be safe to your computer—it could reformat your hard drive just as easily as it displays graphics on your screen. However, since you bought it in a box at the store, you know that whatever company distributed it vouches for its safety, and (in most cases!) feel confident enough to install and run it on your computer.

The mechanics of code signing are based on publicly known algorithms and cryptographic techniques, and are not owned by any company or organization. If you've used the PGP (Pretty Good Privacy) encryption system, you've used some of the same techniques that code signing does. We'll be covering code signing, cryptography, and digital certificates in more details in the next couple of chapters.

Finally, it's important to note that code signing is complementary to sandboxing, and not used in competition with it. Any string of 1's and 0's, including Java applets, can be digitally signed to provide authentication. In fact, both Microsoft and Netscape support, or have announced support for digitally signed Java applets. If the applet is signed, the browser can relax the security restrictions and let the code move outside of the 'sandbox' and do more with the computer.

Code Download

On the server the objects we use are usually installed in the conventional way by a setup program. Imagine if we had to manually install each object that a certain web page used before we could view the page. We would probably be reticent about using objects unless we could be sure our users had already installed them, or they would be willing to go through the annoying process of location, download, and installation.

Not surprisingly, today's Java- and ActiveX-enabled web browsers have automatic methods for downloading and installing code. With Java applets, the only files that are needed are the .class file for the given applet and any dependent .class files it uses. When the browser sees an <APPLET> tag in the HTML, it knows to go to the server and retrieve these files in addition to any graphics or other objects embedded on a page. Both Netscape and Microsoft have different methods of packaging more than one class file into a compressed archive for quicker transmission over the network. Netscape uses ZIP files while Microsoft uses the same CAB and INF file formats that are used to distribute other applications.

The CAB file format is another compressed file format that has been used for some time by Microsoft to reduce file sizes. The part of Internet Explorer that performs the download and authentication process for software is called the Component Download Service. This element can use installation scripts to download one or more files depending on various factors, such as if the file is already installed on the machine, or if the version number of a currently present file is older than may be needed. Internet Explorer can also recognize another file with the .inf syntax used by some Windows setup programs. With files of this type, we specify a list of files and locations, and Internet Explorer downloads only those files it needs. This method can even be used to download different versions of controls for different operating systems or processor types.

If you are developing components for use with Navigator or Internet Explorer, you'll surely need to become more familiar with these issues. However, this introduction is appropriate for a book of this scope, and should be enough to give you an understanding of what actually happens when the browser sees an <OBJECT> or <APPLET> tag in our page.

To learn about creating ActiveX controls, look out for the Wrox Press book Instant VB5 ActiveX Control Creation.


An <OBJECT> Example - Using a Spin Button

Let's see an example to demonstrate what we're talking about. Open a new file in NotePad and enter this:

<HTML>
<HEAD>
<TITLE> Objects </TITLE>
</HEAD>
<BODY>
<CENTER>
<H1> Objects </H1>
<FORM NAME="frmTest">
  Spin Value: <INPUT TYPE="TEXT" NAME="txtSpinValue"><P>
</FORM>
<OBJECT CLASSID="clsid:79176FB0-B7F2-11CE-97EF-00AA006D2776" ID="spnTest"
  CODEBASE="http://activex.microsoft.com/controls/mspert10.cab">
</OBJECT>
<SCRIPT LANGUAGE="VBScript">
Sub spnTest_Change
  Document.frmTest.txtSpinValue.Value = spnTest.Value
End Sub
</SCRIPT>
</CENTER>
</BODY>
</HTML>

If you load this page into Internet Explorer, you'll notice that a text box and two spin buttons. You've probably seen these in other Windows applications, but perhaps not in an HTML page.

Spin buttons aren't one of the standard HTML form control elements, so we've created one ourselves with an <OBJECT> tag. Click on the up and down buttons and notice how the value in the text box changes—the code in our page connects the spin button's Change event with the text box, so the text box is updated each time the spin button is clicked.

The spin button control we're using here is from the Forms 2.0 Object Library. If you've installed the ActiveX Control Pad, Visual InterDev, the HTML Layout Control, or Internet Explorer 3.01 or 3.02, you'll have this file installed and registered on your system. However, with the additional CODEBASE attribute we've added, the file will be automatically downloaded from the Microsoft site if it's not already present on the machine you use to view this page.

Displaying the Spin Button's Value Property

We'll now briefly go over the code that causes the text box to interact with the spin button control. With the background we acquired from the last chapter, it's easy to understand what the script code on this page is doing.

We're using the ObjectName_EventName method of connecting code to an object's event. The spin button control fires an event called Change whenever its value changes. It also supports events called SpinUp and SpinDown that are only fired on an up or down click, respectively. For our purposes Change is fine, so that's the only event we'll handle.

<SCRIPT LANGUAGE="VBScript">
Sub spnTest_Change
  Document.frmTest.txtSpinValue.Value = spnTest.Value
End Sub
</SCRIPT>

The code that we execute when the value changes is simple. The spin button object's current value is stored in a property called Value. Because we've named the object spnTest, we can access this number with the code spnTest.Value. The rest of the code on this line is just performing an assignment to the Value property of the text box (named txtSpinValue) on our form (named frmTest). We see the results of this assignment on the HTML page when the value displayed by the text box changes as we click the spin button.

Presetting the Value with a Parameter Tag

Now imagine that the page is not an .htm file, but an Active Server Page on our Intranet server. The spin button and text box display the number of items on a customer's order. If they are reviewing an existing order, it needs to be set to the value of the current order, ready for the customer to increase the quantity, if need be.

The <OBJECT> tag we used earlier sets the value of the spin button when it's created in the page to the default of zero. How do we go about setting it to a different 'default' value? The answer is to add a <PARAM> tag to the code. This example sets the value to 10:

<OBJECT CLASSID="clsid:79176FB0-B7F2-11CE-97EF-00AA006D2776" ID="spnTest"
  CODEBASE="http://activex.microsoft.com/controls/mspert10.cab">
  <PARAM NAME="Position" VALUE="10">
</OBJECT>

In effect, all it's doing is setting a property to a default value. In fact, the code uses Position rather than Value, but the result is the same. When the object is created, its value will be 10.

The only real way to work with objects like this is to use Microsoft's ActiveX Control Pad, or an equivalent such as Visual InterDev, which understands how to insert objects and set the properties.

Doing It From a Database

This still doesn't answer the whole question, though. The value won’t be a static number, it will be a value in a database. No problem, because the page is an .asp file rather than a normal .htm page. It can get the value and insert it into the <PARAM> tag, as well as into the text box:

...
<%
  Set oConn = Server.CreateObject("ADODB.Connection")
  oConn.Open "Contacts"
  Set oRs = oConn.Execute("SELECT * FROM Orders WHERE OrderNo = 'PR0172'")
  strExistingQty = CStr(oRs.Fields("Quantity"))
  oRs.Close
%>
...
<FORM NAME="frmTest" ACTION="AmendOrder.asp>
  Spin Value:
  <INPUT TYPE="TEXT" NAME="txtSpinValue" VALUE="<% = strExistingQty %>">
  <P>
</FORM>
...
<OBJECT CLASSID="clsid:79176FB0-B7F2-11CE-97EF-00AA006D2776" ID="spnTest"
  CODEBASE="http://activex.microsoft.com/controls/mspert10.cab">
  <PARAM NAME="Position" VALUE="<% = strExistingQty %>">
</OBJECT>
...

When it arrives at the client, ready to be displayed in the browser, the values have been filled in, and of course the ASP code is not sent anyway:

...
<FORM NAME="frmTest" ACTION="AmendOrder.asp>
  Spin Value:
  <INPUT TYPE="TEXT" NAME="txtSpinValue" VALUE="10">
  <P>
</FORM>
...
<OBJECT CLASSID="clsid:79176FB0-B7F2-11CE-97EF-00AA006D2776" ID="spnTest"
  CODEBASE="http://activex.microsoft.com/controls/mspert10.cab">
  <PARAM NAME="Position" VALUE="10">
</OBJECT>
...

This is just a simple example, there are many other things we could do, for example we should be more thorough when querying the database of course. However, it's enough to illustrate the point that we are now firmly into client/server territory. The server creates the page containing the existing values, but the client is 'active' in the sense that it can handle all the work of setting the text box value without needing further involvement from the server.

Beyond Properties: Calling Methods

Like we've said, dealing with objects on the client is very similar to working with objects on the server. Where the methods of an object are concerned, they're actually exactly the same. Suppose we've already created an object, and assigned it's reference to a variable named objVar. Further, suppose that this object has a method called myMethod. Whether we're executing code in the browser or on the server, the way we call this method is identical:

objVar.myMethod

The syntax is also the same regardless of whether we're using JavaScript or VBScript, although it will differ slightly if the method accepts parameters. Method calls in VBScript do not use parentheses, while JavaScript method calls do.

Beyond Properties: Handling Events

While intrinsic ASP objects like Application have events, objects we create and use on the server don't generally fire events. Our exposure to these objects is limited to method calls and property access. As you've seen in the previous chapter, many objects on the client-side do fire events—it's one of the most commonly used ways to execute code.

The syntax to connect code to an object's event is exactly the same for user-created objects as it is for objects in the browser object model, and depends on the style we choose to connect our code to the event. Different methods of hooking up event code were discussed at length at the beginning of the last chapter. Our choice may depend on the scripting language. In VBScript, we can use:

<SCRIPT LANGUAGE="VBScript">
Sub ObjectName_Event1Name
   ...
End Sub
Sub ObjectName_Event2Name
   ...
End Sub
</SCRIPT>

JavaScript doesn't support the Visual Basic system of multiple event handlers in one <SCRIPT> block. Also since adding the event name to the <INPUT> tag is only supported for intrinsic elements that can be created with HTML, this leaves only one style for the combination of user-created objects and JavaScript:

<SCRIPT LANGUAGE="JavaScript" FOR="ObjectName" EVENT="Event1Name">
...
</SCRIPT>
<SCRIPT LANGUAGE="JavaScript" FOR="ObjectName" EVENT="Event2Name">
...
</SCRIPT>

This style can also be used with VBScript code if you wish.


Getting Into Client/Server

For the rest of the chapter we'll focus on four separate examples that illustrates what we can do with a combination of client-side and server-side scripting, with a few objects thrown in, to improve our pages. In the first example we'll show an extremely simple way in which we can improve the friendliness of forms on our web page, and then in the second we'll look at an extensible block of code we can use to pre-validate form input on the client instead of the server. These first two examples show some common things to make our pages better that can't actually be done with ASP at all.

The last two examples continue to demonstrate some neat client-side features, but use ASP as an integral part—generating different client-side code depending on settings on the server. The third example explains a few different methods to control and synchronize frames—some. The final example uses a home-grown control created with Visual Basic 5.0 to give a taste of how powerful objects can be on the client and how ASP can be used to generate dynamic scripts and object insertion. Again, we're using VBScript code. However, as long as you understand the concepts behind the examples, there's nothing stopping you from rewriting them in any language supported by the browsers that your clients or customers will be using.

As with all of the sample code in the book, the code from this chapter is available from our web site: http://www.rapid.wrox.com/books/0723/


Setting the Focus

Users of Windows applications are fickle—they actually expect the user interface to be easy to understand, straightforward, and logical. This means anything from menus that are ordered so we can easily find program options, to text boxes that automatically start with the text most commonly entered. It used to be difficult to achieve this friendliness with HTML. Only a few input elements were available to request data from the user, and fine layout of these elements was often hard to manage. Fortunately, HTML has advanced substantially over the last few years, and, with objects and script code, many previously impossible things can now be accomplished.

The ChangeFocus Example

The first example involves setting the input focus to the text box the user is most likely to want to enter data into on the 'Login' page. We'll be able to do this with only one line of script code. In Windows 95, Windows NT, and a number of other operating systems, the system's logon dialog includes user ID and password text boxes.

In most cases just one person uses a machine most of the time, so the logon dialog automatically presets the user ID text box to the ID of the last person who logged on, and puts the cursor in the password text box. All we have to do is sit down, enter our password, and press Return (assuming we were the last person to use the machine). This is a lot better than having to manually enter our ID, press Tab to move to the password field, enter our password, and, finally, press Return.

Our example page (changefocus.htm) shows a hypothetical logon screen with user ID and password fields:

If you load this page into the browser and then look at the cursor, you'll find that it's in the password field, not the user ID text box. Normally, when an HTML page loads, the input focus is automatically positioned on the first form element that can take input. This isn't what we want because it requires the user to manually tab to the password text box every time they logon. Fortunately, the little bit of script code we've added to the file takes care of this problem.

The page in this example always shows the same user ID, johndoe, and doesn't do anything when the Submit button is clicked (take a look at the source and see that the ACTION attribute of the form is set to a non-existent file called DeadLink.asp). We'll see what we could do about this in a while, and for a more complete example of a logon page look back at Chapter 6, or the bug report case study in Chapter 12.

The ChangeFocus Code

The HTML file for this sample, ChangeFocus.htm, contains only normal HTML, except for one small script block. The HTML creates a form called myForm, with a text box called txtUserID, a password text box called pwdPassword, and a single Submit button.

But the script block is where the action occurs:

<SCRIPT LANGUAGE="VBScript">
Sub Window_onLoad
   Document.myForm.pwdPassword.Focus
End Sub
</SCRIPT>

The <SCRIPT> tag tells the browser that the following lines are code. The next line, Window_onLoad, says that the code inside this subroutine (that is, everything until the End Sub) should be executed immediately after the page is loaded. Everything up until now has been just plumbing, the next line does our work by calling the Focus method of the pwdPassword password text box.

To call the Focus method, we first need to get a reference to the password text box. That's what everything on this line (except for the .Focus) is for. Think back to the last chapter and remember that the Document object represents the current document in the browser. Each Document object has a collection of Form objects, one for each form. In the same way, each Form object has its own collection of Element objects that represent each input element or object in a form.

The Form collection can be accessed by the numerical index of the form, or by its name as we've done here. Similarly, Elements can be accessed by an index or by the individual element name. In our example, we've used the name of the form and the name of the password text box instead of their indices, but this line of code would accomplish the same thing:

   Document.Forms(0).Elements(1).Focus

Once we have a reference to the password text box, all that remains is to call the method we want, which in this case is Focus.

So, the page loads, the Window_onLoad event is called, and the focus is set to the password text box, ready for input. Our users are happy they don't have to press Tab many times every day, we get a huge raise, and retire. Sound far fetched? OK, maybe… but keep reading, we still have more interesting things to talk about in this chapter.

Using ASP to Create the Page

From the earlier spin button example, and the examples in Chapter 6, it's easy to see how we could set the user ID text box in an ASP file and send that to the browser instead of an .htm page. If it was part of an application, we might already have it stored elsewhere, so we would just use it in the VALUE attribute of the <INPUT> tag:

<INPUT TYPE="TEXT" NAME="txtUserID" VALUE="<% = strStoredUserID %>">

And of course, we could then emulate the behavior of the standard Windows logon dialog more closely, by checking if we'd actually got a user ID before we set the focus:

<SCRIPT LANGUAGE="VBScript">
Sub Window_onLoad
   If Document.myForm.txtUserID <> "" Then
      Document.myForm.pwdPassword.Focus
   End If
End Sub
</SCRIPT>


Client-Side Form Validation

One of the easiest and most practical applications of client-side code is input validation. When Microsoft and Netscape first started talking about adding scripting to HTML people asked why, and this was one of the first responses the marketing people gave. The horizons have expanded greatly with objects, but this remains one of the most beneficial uses of script code.

The Form Validation Example

The code for this example (validation.htm) is a bit more complex than the last one, but it should be accordingly more useful. The screenshot below shows a form with four fields called, in turn, Name, Age, Email Address, and Next Birthday. Each field (with the exception of Age—we don't want to offend anyone!) must have a certain type of input before the form can be submitted.

What is Valid Input?

So how do we determine what constitutes a valid input? The Name field is easy—it just requires some text. We're not going to try to make any judgements about whether or not a name is really a name. Email is similar—we couldn't expect to know if the user name or domain name was real. However, we do know that every valid email address always contains the @ character. Our validation criteria for the Email will just be that—we'll accept the input if we see a @.

How about the Next Birthday field? A date is more likely to follow a recognizable pattern, so we'll accept the input if it follows any date format our computer understands. With VBScript, it's taken care of easily in just a single line. In addition, we'll add one more parameter. Since we asked for the date of the user's next birthday, we'll need to check that the date hasn't already occurred.

Now load up the page validation.htm and enter some different combinations of input. If you enter everything it's asking for, you'll be rewarded by a 404—Object Not Found reply. We don't want to do anything real with the data at the moment, so—again—the ACTION attribute in our form points to DeadLink.asp. In a real application we would take the validated data and insert it into a database or perform other processing. If we try to sneak some bad data to the server you'll see dialogs like these:

With invalid input, after pressing OK to dismiss the message box, you'll see the same page as before pressing Submit. The form data hasn't been sent to the server, and the page is still waiting patiently for input it can accept.

The Validation Code

We could come up with additional data types ad infinitum, but these will get us started. Requiring some input, an email address, and a date (in a certain range) are some of the most common requests made on HTML forms. In addition, as you'll see when we look at the code, it can be easily extended to incorporate validation for any type of data.

The non-script HTML for this page isn't anything we haven't seen before. After printing a header, it creates a six-element form with these elements:

Name | INPUT Type
txtName | TEXT
txtAge | TEXT
txtEmail | TEXT
txtBirthday | TEXT
sbmMyForm | SUBMIT
rstMyForm | RESET

*** can we get the table alongside the paragraph above?

As always, the action occurs in between the <SCRIPT> and </SCRIPT> tags. All of our code for this example is in one routine, the onSubmit handler for our form object.

Function myForm_onSubmit
...
...
End Function

This is different to the other event handlers we often see. Remember that myForm_onSubmit is a Function, not a Sub. Functions return a value, and subroutines don't. With a form, we might want to cancel the form submission. Remember our discussion about this in the last chapter—the browser is set up to pay attention to the return value of this handler. If we return True, the form is submitted; if we send back False, nothing happens—it's like the user never pressed Submit in the first place. An alternative method is to create a normal button with an <INPUT TYPE="BUTTON"> tag and submit it using the Submit method of the Form object.

With onSubmit out of the way, we can focus on the code that does the validation. Since we might want to use this code with a wide variety of different forms, it's been set up so that adding and removing validation for various form fields is easy. The function itself can be subdivided into two parts: the validation code that may change for each form, and the supporting code that prints the error message.

Supporting the Validation Code

Take a look at the all of the code outside of the specific validation sections. This is the code we're talking about:

Function myForm_onSubmit
   Dim msgError
   Dim fRef
   Set fRef = Document.myForm    'get myForm reference
   '... VALIDATION CODE GOES HERE ...
   If msgError <> "" Then   'print error message if validation failure
      MsgBox "There was a problem with your submission:" _
             & Chr(13) & msgError & Chr(13) & Chr(13) _
             & "Please change your input and submit again."
      myForm_onSubmit = False
   Else
      myForm_onSubmit = True
   End If
End Function

This code first prepares for the validation. Then, if a problem with the user's input has been detected, it displays a message box explaining the problem and cancels the form submission. If the input is OK, the form is submitted.

We first declare two variables, msgError and fRef. Like we've said, declaring variables isn't absolutely necessary, but it's good practice and makes our code easier to read. The msgError variable is what we'll use to store a string describing the problem(s) with the input. We'll write to it in the validation code, and display it if we decide to cancel the form submission. After we declare fRef, we initialize it with this line of code:

Set fRef = Document.myForm    'get myForm reference

The variable fRef isn't absolutely necessary, but it simplifies our repeated references to the myForm object. This allows us to use refer to the form in our validation sections with the code:

fRef.txtName.Value

instead of:

Document.myForm.txtName.Value

In addition to simplifying our code, this technique makes it run faster. The script engine takes time to resolve each reference, so we shortcut this process by only performing the Document.myForm resolution once, and saving the result in a variable for later use.

After these initialization steps the validation code is allowed to do its work. We'll talk about these routines in a second, just understand for now that whenever the code detects a problem with the input it adds some text describing the problem to the string variable msgError. This is the basis for the code at the end of the routine. Since msgError starts as an empty string, we know that if it's still an empty string when the validation code is complete then there were no problems with the user's input, and we can submit the form by setting the return value of the myForm_onSubmit function to True.

If msgError holds anything other than the empty string, then we've found a problem. In this case we display a message box, wrapping the strings There was a problem with your submission: and Please change your input and submit again around the text in msgError describing the problem. We also set myForm_onSubmit to False to cancel the submission. The newline character, Chr(13), is used to separate the lines in the message box.

Validating the Fields

The only thing left is to understand how each field is validated. Of course this depends on exactly what constitutes a valid input. Take a look at the code for the Name field, where we just want to make sure the user has entered some text:

   If fRef.txtName.Value = "" Then
      msgError = msgError & Chr(13) & "Name must have a value."
   End If

We first test to see if the value of the txtName text box is equal to the empty string. If it is we append a newline character and the error string Name must have a value to the msgError variable and store it back into msgError. We need to keep any previous contents of msgError intact, so we can't just assign our error string to the variable—if we did this anything that was there previously it would be overwritten. Name is the first field, so we can be sure there won't ever be anything in msgError yet, but it's a good practice to stay consistent between validation steps and we will need to append in all the code after this. Plus, it gives us a space between the default error text, and makes our message box look nicer.

If the user has entered something into the txtName text box, everything is cool and we can move onto the next field without doing anything to msgError. All of the validation code follows this pattern: 1 - test the input and, 2a - write msgError if there's problem, or, 2b - don't write msgError if things are OK.

The only additional wrinkles are how we handle different kinds of validation, and how we deal with fields that must meet multiple criteria. Fortunately, the validation code for the Email field demonstrates both of these tasks.

When checking Email, we need to make sure it not an empty string, and that there is a @ character somewhere in it. We could check both of these in a single statement, but then our error message wouldn't give the user any clue about the specific thing that was wrong with their input. Instead we'll use an additional ElseIf clause in our code:

   If fRef.txtEmail.Value = "" Then
      msgError = msgError & Chr(13) & "Email must have a value."
   ElseIf InStr(fRef.txtEmail.Value, "@") = 0 Then
      msgError = msgError & Chr(13) & "Email must have an ""@"" sign."
   End If

We first check to see if anything exists in the text box, because we can't possibly find an @ if the text box is empty. If the user hasn't entered anything in txtEmail the validation for this field fails and we write the same error string as above into msgError.

A Valid Email Address

If something does exist, we can use the VBScript InStr() function to see if the string includes an @. In its simplest form, InStr() (short for In String) takes two strings, one string to look in, and one string to look for. If it finds the second string in the first string it returns an integer specifying the location in the first string where the second string begins. If it doesn't find the string, it returns 0.

It's easy to understand how this code works then. If Instr doesn't find an @ in the value the user has entered it returns 0, and we know to write the message Email must have an ""@"" sign to the error string. We want the string displayed in the message box to include double quotation signs around the @, so we use the VBScript convention of specifying two double quotes when inside a string.

A Valid Date Value

Finally, we need to cover the validation of the Birthday field. In this we need to check three things: that txtBirthday has a value, that the value is a date, and that the date isn't in the past (since we asked for the user's next birthday). There are a lot of things to check, but they're actually very easy to do using the date manipulation ability of VBScript. The whole of the validation code is only five lines long:

   If Not IsDate(fRef.txtBirthday.Value) Then
      msgError = msgError & Chr(13) &_
      "You must enter a date for the birthday field."
   ElseIf CDate(fRef.txtBirthday.Value) < Now Then
      msgError = msgError & Chr(13) &_
      "Next birthday can't be in the past!"
   End If

We wrap our first two criteria into one test using the IsDate function. IsDate takes a variable and returns True if the value in the variable can be converted into a date, or False if it can't. Fortunately, the empty string can't be converted into a date, so IsDate returns False both when nothing has been entered in the txtBirthday text box, and when the value entered can't be converted to a date. We can give a meaningful error message for both cases (and avoid using an additional ElseIf clause) by saying You must enter a date for the birthday field.

Once we're sure we have a valid date, we can compare it to the current date to make sure the user hasn't tried to sneak in with a date that has already gone. We can easily accomplish this by first converting the value in the text box to a date using the CDate function and then comparing our date value with the current date returned by the Now function. Since we're comparing two dates we just need to use the < (less than) operator to determine if our user's next birthday has already occurred. If it has then we write an appropriate error message to msgError and we are done.

From this discussion you can see how easy it is to add your own tests to the framework. All you need to do is plug in an If...Then block containing your validation test, and write to msgError if you detect a problem.

Adding ASP to the Validation Example

Again, there are plenty of ways that we could integrate this example with ASP. In fact it's likely that the results from a form like this will ultimately be stored in a database, so ASP would be an obvious way to manipulate the information after it has been submitted. You'll see some excellent examples of how this can be done in Chapter 12.

So, like the previous example, you could allow users to edit their details, by fetching them from the database with ASP, and building a page that contained the current values as the defaults. And, again as featured in the Chapter 12 example, we could quite easily add features such as sending a confirmation message to the user by email, using the address they had just entered.

Client-side vs Server-side Validation

One point to remember, and something we discussed in Chapter 6, is that you need to be aware of the limitations of client-side data validation. It is a great way of reducing server load and network traffic, because you can ensure that only data of the appropriate type is submitted from the form. However, there are a couple of problems.

What you can't do with just client-side validation, is be absolutely sure that the data is valid when it gets to your server. Unscrupulous users could send data from their own forms to your application—remember that the URL of the page that handles the data is freely visible in the original form page. In the next chapter, you'll see how we can reduce this risk, by using digital certificates and secure channels to communicate the results.

This also leads to the other concern we expressed in Chapter 6. If the method of validating the data requires some 'secret formula', it's not a good idea to do it in a scripting language on the client, where everyone can view the source of the page and see the code. Instead, you might want to use a custom control on the client, which hides the formula inside the control, or validate this data once you get it back at your server.


Manipulating Frames with VBScript and ASP

The first two examples in this chapter showed some things that can be done easily with client-side scripting. However, they didn't actually incorporate any of the features of ASP, we only suggested ways that this could be done. In this and the next examples, we'll be using ASP in conjunction with script code on the client.

While ASP is great at building the individual pages for a web application dynamically, its features are rather limited when it comes to handling the frameset pages that are so common to today's sites. Using the Redirect method of the Response object we can redirect to a single page, but we can't target it to a specific frame—only the browser can do that. The combination of client and server code in this example shows how scripting languages on both sides of the Web connection can, together, manage framesets and the pages inside the frames.

The Frames Example

This is more complicated than the first two examples we looked at. Before we launch into an explanation of what this example does, you might like to bring up frameset.asp in your browser and see for yourself. Experiment by clicking on a few of the links in the left hand pane. We'll spend some time discussing the code that makes this sample work, and you'll understand it better if we're familiar with the samples behavior in the browser. Since this is now an ASP file, you also need to be sure that it comes from your server and is loaded via HTTP.

You'll notice that clicking on a page link on the left hand side loads that page into the right hand frame, and also updates the left frame itself so that only links to the pages not currently shown are available. This makes sense—if we're already viewing the second page we shouldn't be able to jump to it again.

Since we're viewing page two in the right hand frame, it's not visible in the list in the left hand frame.

Now click over to the third page, and you'll see a list of options in the main frame. If you choose one of the Option links, rather than a Page link, you'll see the left frame refresh with a list of the remaining options, and the right frame with the actual page you selected. If you watch closely, you'll see that the left frame is refreshed a split second before the right frame is updated. We'll talk about why this happens later in this section.

So that's what this example does: it displays a variety of pages in the main section of a two-pane frameset and keeps the left-hand pane in sync—and it uses both client and server-side scripting to accomplish this.

If you've designed a site with frames before, you know that creating a two-pane frameset with a navigation frame and contents frame isn't too difficult. The frameset HTML file contains the size and URL information for the frames, so that they can be created and laid out. Each of the pages is a separate file, as specified in the frameset file, and these are loaded into the frames just created.

Once the pages have been rendered, new pages are targeted to a specific frame by adding the TARGET attribute to the links that modify the other frame. For example, our frameset has two frames called NavBar and MainFrame. To change the contents of MainFrame from a click on a link in NavBar, we'd just use:

<A HREF="mypage.htm" TARGET="MainFrame">Go To My Page</A>

However, with this method, the left-hand frame doesn't change to reflect what is shown in the main frame. Our example does allow this, and this significant improvement comes from the combination of ASP and client-side code.

Where would we use something like this? The answer is any place that we currently use (or could use) frames, but want to give the viewer of our site more feedback as to what is happening. Or perhaps we'd like to customize the available options based on which user is accessing our site, or what part of the site they're viewing. We can easily extend this example to the specific needs of our site, but first we should understand how the code works.

How It Works

In contrast to the first two examples (and the next one), this sample consists of more than a few files, although the interesting work takes place primarily in two files: frameset.asp and navbar.asp. It's no coincidence that these files are the only Active Server pages of the whole lot. The rest of the pages are simple HTML files—that in the real, non-book-example, world would hold the site's content. So we can keep things straight, all of the files are listed below:

Filename | Purpose
frameset.asp | The top-level frameset page
navbar.asp | Navigation bar (left-hand pane) page
pageone.htm | Page one
pagetwo.htm | Page two
pagethree.htm | Page three (with option links)
option1.htm | Option page one
option2.htm | Option page two
option3.htm | Option page three
option4.htm | Option page four

Creating the Frameset with ASP

We first need to understand how the frameset.asp page works. This is the page you loaded into your browser earlier, and it's where everything starts. The ASP code in the page sets the values of two variables, NavBarPage and MainFramePage, depending on the value passed to the page in the URL. We'll see this in a while—just accept for now that they are set up correctly. So, frameset.asp sets up the frameset with different pages:

... the ASP code that sets NavBarPage and MainFramePage goes here ...
<HTML>
<HEAD>
<TITLE> Frameset Demonstration </TITLE>
</HEAD>
<FRAMESET COLS="150,*">
   <FRAME NAME="NavBar" SRC="<%= NavBarPage %>" SCROLLING="AUTO">
   <FRAME NAME="MainFrame" SRC="<%= MainFramePage %>" SCROLLING="AUTO">
</FRAMESET>
</HTML>

The <FRAMESET COLS="150,*"> line creates a frameset consisting of two columns. The width of the left-hand column is 150 pixels, and the right hand column takes up the remainder. The two <FRAME> tags provide the information about each frame. The NAME attribute defines the frame's name, and is important because it's what we use in our links and script code to refer to the frame. The SCROLLING attribute turns scrolling on if needed. But what of the SRC attribute with that familiar ASP code string <%= ... %> ?

Selecting the Pages to Display

We're telling ASP to insert whatever is in the variables NavBarPage and MainFramePage into the <FRAME> tags. The important thing is how these variables are set, and to understand this we need to look at the code we didn't show in the listing above. The page expects to find an argument in the query string, containing a parameter called Nav. In other words, it expects to be called with a URL like this:

http://www.yoursite.com/frameset.asp?Nav=all

Here's the code itself:

<%  'get Nav and use it to determine which frames to display
    'and which parameter to call navbar.asp with
NavChoice = Request.QueryString("Nav")
Select Case NavChoice
   Case "pageone"
      NavBarPage = "navbar.asp?BarChoice=pageone"
      MainFramePage = "pageone.htm"
   Case "pagetwo"
      NavBarPage = "navbar.asp?BarChoice=pagetwo"
      MainFramePage = "pagetwo.htm"
   Case "pagethree"
      NavBarPage = "navbar.asp?BarChoice=pagethree"
      MainFramePage = "pagethree.htm"
   Case "all"
      NavBarPage = "navbar.asp?BarChoice=all"
      MainFramePage = "main.htm"
   Case Else   'choose all
      Response.Redirect "frameset.asp?nav=all"

End Select  %>
... HTML code is here ...

The first line of real code uses the QueryString collection of the Request object to get the value of the Nav parameter, if it exists, from the URL. We store that value in the variable called NavChoice. With this information the code uses a Select Case statement to set the variables depending on what was specified in the URL. By using all, we can ask for the main content page (main.htm) to be displayed in the main frame, and the navigation bar to show links to all three pages.

If there is no value for Nav in the query string, as when we first loaded the page, the Else part of the Select Case construct is executed. This uses the Response object's Redirect method to refresh the page, showing the main content page and all of the links. Of course, we could have copied the code down from the all clause immediately above it—it has exactly the same effect. However, this is better style because it means we only have to change our code in one place if we decide to modify the default action.

Looking at the clauses themselves, it's easy to understand what they are doing. They put the name of the file that should be loaded into the main frame into MainFramePage. When our Nav option is pageone it's logical to think that pageone.htm should be the page we see, and so on. However, notice that setting the NavBarPage variable is a little different—we're loading the same file navbar.asp in each case. What our code does, however, is add a different parameter to the query string each time.

The Dynamic Navigation Bar

This is because the navbar.asp file changes its behavior, depending on the BarChoice value included in the URL. The part of navbar.asp that we're interested in is this:

<% BarChoice = Request.QueryString("BarChoice")
Select Case BarChoice
   Case "pageone" %>
      <TABLE WIDTH="100%" BORDER="2" CELLPADDING="5" CELLSPACING="2">
         <TR>
            <TD ALIGN="CENTER"><A HREF="javascript:GoPageTwo()">
            Page Two</A></TD>
         </TR>
         <TR>
            <TD ALIGN="CENTER"><A HREF="javascript:GoPageThree()">
            Page Three</A></TD>
         </TR>
      </TABLE>
      <P><CENTER><H3> Page One </H3></CENTER>
... similar code for "pagetwo", "pagethree" and "all" goes here ...

This works almost identically to the code in frameset.asp. First it stores the value of the BarChoice part of the URL in a variable of the same name, and then uses this variable in a Select Case construct to output the appropriate set of <TABLE> and <A> tags. When BarChoice equals pageone, the code only outputs links to pagetwo.htm and pagethree.htm. The code for the pagetwo, pagethree, and all options is nearly identical to the code here, so we haven't listed it. You can check it out in the navbar.asp source.

Changing Both Frames with Client-Side Code: Method One

Now that we've talked about how the two most important pages are created, we can discuss that new enigma we've unearthed: the use of javascript:function-name() in the anchor tags in navbar.asp. As we said earlier, it's possible to change the contents of one frame, when a link in another is clicked, by using the TARGET attribute in normal HTML. However, in our case, what we want to do is update both of the frames in our frameset, and HTML doesn't provide a way to do this. Fortunately, our task can be accomplished with a little client-side code. We show two different methods to accomplish this in the sample, and we're ready to discuss the first now.

If you remember back to the object model discussion in the last chapter, you'll recall the Frames collection of the Document object, and the Location object. Frames provides an interface to each frame in a frameset, while the Location object gives information about the current page displayed in the frame. Each frame and window has a Location object. It's no surprise, then that two lines of code can change the contents of both of our frames:

parent.frames("NavBar").location.href = "navbar.asp?BarChoice=pageone"
parent.frames("MainFrame").location.href = "pageone.htm"

We need to remember that, to access the Location objects of the frames in our frameset, we need to go back one step to the top-level frame. The Frames collection of the NavBar and MainFrame frames are both empty because these frames don't have any sub-frames. However, the parent frame of NavBar and MainFrame includes both NavBar and MainFrame. In the code above we use the Parent property of the (default) Window object to access the correct collection. Once we have the reference to the correct frame, setting the HRef property of the Location object causes the current window to display whatever URL is specified.

The only thing we haven't talked about is how the code above actually gets executed. Normally we connect client-side code to an event raised by an object. For example, we might execute code when form button is clicked, i.e. the onClick event is fired. In this case we'd like to execute the code when a link is clicked. Link objects do have an event called onClick, and we could specify that our relocation code be executed in response to this event. We're not going to use it in this case, because then we'd have to follow the unsightly practice of specifying an HREF attribute whose value is the empty string (because our code would be doing all the work). Holding the mouse over a link with no value can confuse viewers who depend on the status bar to see where they're going. In addition, in a more graphical site we might want to do the same thing but with an image map, and these don't have onClick events like Link objects do. Instead we'll directly specify the code to executed in the HREF attribute. This is where the javascript:function-name() syntax comes into our lives.

Using javascript: function-name() with VBScript

We've been using VBScript in the last few chapters, but Internet Explorer doesn't support the vbscript:subroutine-name syntax. It does support the equivalent javascript:function-name(). The clever part if that, if we specify the name of a VBScript subroutine instead of a JavaScript function it still works. The VBScript routine is executed correctly, and this is exactly what we do in on this page of the sample. The parts of navbar.asp we haven't see yet contain these routines:

<SCRIPT LANGUAGE="VBScript">
<!--
Sub GoPageOne()
   parent.frames("NavBar").location.href = "navbar.asp?BarChoice=pageone"
   parent.frames("MainFrame").location.href = "pageone.htm"
End Sub
... more similar routines here ...
-->
</SCRIPT>
...
<TD ALIGN="CENTER"><A HREF="javascript:GoPageOne()">Page One</A></TD>

Putting this all together, we can see that clicking the Page One link causes the browser to execute the code in the GoPageOne subroutine, and this code loads pageone.htm into the main frame and reloads navbar.asp with BarChoice equal to pageone. The rest of the Page links follow the same format. They call a VBScript function that changes the HRef properties of both frames to the correct URLs. When navbar.asp reloads, it changes the page links displayed, and everything is ready to go again.

The Option Links on Page Three

The last major feature of this example, which we haven't talked about yet, is the set of option links on page three. If you haven't already seen these, open up page three by clicking on the appropriate link.

Click on one of the option links in the main frame. The NavBar frame reloads, displaying links to all three pages and to the options that weren't selected. The page for the option we clicked is displayed in the main frame.

We could have implemented this in the same way we did the page navigation, but we've chosen an alternative method so that we can keep all of the navigation code in navbar.asp. If we used the first method, we would have to add many additional functions to the third page, making the content more difficult to modify independently of the script code, and further melding the site's logic and content—not a good thing.

In addition, suppose we had option arrays like this on each of our three pages. All of a sudden we need to maintain navigation code in four documents instead of one. Finally, as you'll see when we look at the code that implements this in navbar.asp, we are able to use the similarities between the text strings to move most of the code into a loop, further simplifying our design. In a real world situation we'd be far more likely to use entries from a database to populate this list, but even then the same code can apply—using the unique identifier for each database record instead of our arbitrary series of options from 1 to 4.

Changing Both Frames with Client-Side Code: Method Two

Those are the benefits, but how exactly does this second scheme work? Our first clue is the HREF attribute of each of the links on page three. Here's the code for the first link:

<LI><A TARGET="NavBar" HREF="navbar.asp?BarChoice=onelink&id=1">Option One</A>

We're using the TARGET attribute, which is the good old HTML-only way of targeting a page with a frame different to the one containing the link. The HTML here changes the contents of NavBar (which, since this code is in MainFrame, is the other frame) to the URL "navbar.asp?BarChoice=onelink&id=1". This code doesn't do anything about changing the page loaded into MainFrame, yet the page itself does indeed change.

It's only possible to directly change one frame or window with the TARGET attribute of the anchor tag. However, with some strategically placed client-side code, we can change more than that. In our example, code in navbar.asp is changing the contents of MainFrame, using the value of the BarChoice parameter—onelink in this case—to determine which block of code to execute. The value id parameter that follows it is used by the onelink code, as we'll see in a moment. Think about that for a second. The sequence is:

  • Click on a link in frame MainFrame that points to NavFrame and navbar.asp,

  • The navbar.asp page reloads and NavFrame is updated,

  • Code in navbar.asp reloads MainFrame.

For example, the first link on page three passes the query string BarChoice=onelink&id=1 to the navigation bar file navbar.asp. We've already looked at the pagexxx values of the BarChoice parameter that this code can handle, so let's look now at the final choice, onelink. The Select Case clauses for the other pages consisted of a block of HTML. The clause for onelink is a little more complicated, including two sets of ASP script in addition to the same HTML. We'll start with the familiar and move quickly into new territory.

The Navigation Bar 'onelink' Code

We see the same old table listing page options at the top of the finished onelink frame, and it's no surprise—the first visible HTML generated by navbar.asp for this option is the same as we've seen before in the other categories. After this table we display the list of options that aren't currently displayed. If we're currently showing Option3.htm in the main frame, we'd like to display links to options one, two, and four in the navigation bar. Our ASP code for this looks like:

<% For i = 1 to 4
      If CInt(CurrOption) <> i Then  'print link %>
         <A HREF="navbar.asp?barchoice=onelink&id=<%= i %>">
         Option <%= i %></a><p>
<%    End If
   Next %>

And it generates this HTML:

<A HREF="navbar.asp?barchoice=onelink&id=1">Option 1</a><p>
<A HREF="navbar.asp?barchoice=onelink&id=2">Option 2</a><p>
<A HREF="navbar.asp?barchoice=onelink&id=4">Option 4</a><p>

The code loops once for each element in our array of options, printing out a link every time, except for when the value stored in CurrOption is the same as our loop index. At the top of the onelink code we set a local variable called CurrOption to the value of the id portion of the query string. So calling onelink with an additional &id=3 causes CurrOption to be set to 3. This is the first use of the id parameter we've talked about—the second is in the code that loads the main frame.

Creating VBScript Code Dynamically

If you've looked at the entire code for the onelink clause you noticed this somewhat nasty looking section right at the beginning of the block:

Response.Write "<SCRIPT LANGUAGE=" & Chr(34) & "VBScript" & Chr(34) & ">" _
                & Chr(13) & Chr(10)
Response.Write "<!--" & Chr(13) & Chr(10)
Response.Write "Sub Window_OnLoad()" & Chr(13) & Chr(10)
Response.Write "On Error Resume Next" & Chr(13) & Chr(10)
strTemp = "Parent.frames(" & Chr(34) & "MainFrame" & Chr(34) _
        & ").location.href = " & Chr(34) & "Option" & CurrOption _
        & ".htm" & Chr(34)
Response.Write strTemp
Response.Write Chr(13) & Chr(10)
Response.Write "End Sub" & Chr(13) & Chr(10)
Response.Write "-->" & Chr(13) & Chr(10)
Response.Write "</SCRIPT>" & Chr(13) & Chr(10)

But, before we let ourselves be scared away by this mess, take a look at the nice and simple HTML code it generates for the browser:

<SCRIPT LANGUAGE="VBScript">
<!--
Sub Window_OnLoad()
   On Error Resume Next
   Parent.frames("MainFrame").location.href = "Option3.htm"
End Sub
-->
</SCRIPT>

What we have here is a block of ASP code on the server that generates a block of code that is to be executed on the client. This is very powerful—we're actually changing our client-side code 'on the fly' to suit our purposes.

Let's first understand how navbar.asp is using this code to reload MainFrame, and then we'll talk a little more about the ASP code that generated the code in the first place. The client-side code we end up with is relatively simple. Immediately after the HTML page has finished loading, the Window_onLoad event fires. In the code that is executed, we're using the Frames collection and Location object to load the contents of Option3.htm into our main frame. The only wrinkle is the handy placement of On Error Resume Next to avoid any unsightly errors in the navigation frame if a non-existent URL is accidentally specified in the next line.

All the ASP code does is to generate this small four-line subroutine by using a series of Response.Write calls. It uses Chr(34) to generate double quotes that can't be specified directly in the code because they will be interpreted as server-side code, and the line feeds and carriage returns so the final output is neatly placed on separate lines.

Moving the Code to Our Own Site

That's all for this sample. We've covered a lot in the last few pages, and really showed how ASP can be used with client-side code to generate pages that weren't possible with just HTML or client-side code alone.

In moving the concepts in this example to your own site, you would keep much of the existing frameset.asp and navbar.asp code, modifying it to point to the actual files in your site. The rest of the files (page* and option*) would be replaced by your content.


User Interface Improvements with Objects

We can wrap literally any code we might be able to execute in an application, inside an object for use on the World Wide Web. The last of our examples shows one of the many ways that objects can be used in HTML pages to better our user's perception our site. The object in this example, and, correspondingly, the sample itself, are both simple—but they'll give you an idea of what is possible.

This example uses a home-made ActiveX control object, created in Visual Basic 5. While we will be examining how we can build our own server components in this book, we won't even begin to get into creating client-side controls or objects. Rest assured it can be done, easily, with version 5.0 of Visual Basic.

For a detailed look at ActiveX control creation, see the Wrox Press publication Instant VB 5.0 ActiveX Control Creation.

The User Interface Example

The single ASP page in this example asks first for a number in a single text box on a form:

Clicking Submit returns a page, generated on the fly, containing as many objects as were specified on the first page. The object itself is an extremely simple bar graph control. This sample uses an ActiveX control created with Visual Basic 5.0, although we could use a Java applet if we had one available—the code would be the same.

This example demonstrates the basic principles behind using the client-side <OBJECT> and <APPLET> tags to improve our ASP pages, and it does something that would be impossible with straight HTML—it displays data in a graphical format.

The UIImprovement Code

For the last time, let's dig into the code and see what's going on. One trick we use here, which you've seen earlier, is to have a single ASP page that both prompts for the initial input, and displays the page with the results on it.

Producing Two HTML Pages From One ASP File

Although we're generating two distinctly different pages in this example, all of our code resides in one .asp file. Our input form contains a hidden field named hdnSecond. When the page is first loaded, the value of this parameter in the Request.Form collection is an empty string, because we've never set it to anything. This makes the first If ... Then clause True, displaying the form and not the results. The entire page then, is dictated by this If...Then...Else...End If block:

<%
If Request.Form("hdnSecond") = "" Then
   'show input form with that includes this line:
   <INPUT TYPE="HIDDEN" NAME="hdnSecond" VALUE="Go">
   ...
Else
   'show result page with <OBJECT> tags
   ...
End If
%>

When the user presses the Submit button, the form is submitted to the same page because we used the Request object's ServerVariables collection, with the SCRIPT_NAME parameter, as the ACTION parameter of the new <FORM>. The SCRIPT_NAME member of the collection returns the name of the currently executing script page—which is where we want to send our data for processing again. However, this time the test on hdnSecond returns False because hdnSecond has been set to "Go" in the input form.

The Results Page With <OBJECT> Tags

The second block of code generates the HTML for a page that contains a number of <OBJECT> tags—depending on how many the user originally specified:

If they have accepted the default choice of three controls, we get a page with this HTML:

<HTML>
<HEAD>
<TITLE> User-Interface Improvements Using ASP </TITLE>
</HEAD>
<BODY>
<H1>ASP Generated Page With Objects</H1>
You chose to have 3 controls on this page. Here they are:<p>
<OBJECT ID="SimpleGraph1"
   WIDTH=155 HEIGHT=143 CODEBASE="control/graph.cab"
   CLASSID="CLSID:BD374B2A-8155-11D0-9472-9498CA000000">
   <PARAM NAME="Size" VALUE="100">
   <PARAM NAME="Caption" VALUE="Graph Control 1">
</OBJECT>
<OBJECT ID="SimpleGraph2"
   WIDTH=155 HEIGHT=143 CODEBASE="control/graph.cab"
   CLASSID="CLSID:BD374B2A-8155-11D0-9472-9498CA000000">
   <PARAM NAME="Size" VALUE="66.6666666666667">
   <PARAM NAME="Caption" VALUE="Graph Control 2">
</OBJECT>
<OBJECT ID="SimpleGraph3"
   WIDTH=155 HEIGHT=143 CODEBASE="control/graph.cab"
   CLASSID="CLSID:BD374B2A-8155-11D0-9472-9498CA000000">
   <PARAM NAME="Size" VALUE="33.3333333333333">
   <PARAM NAME="Caption" VALUE="Graph Control 3">
</OBJECT>
</BODY>
</HTML>

Each of the three object tags is identical, except for the values of the ID, NAME and SIZE attributes in the <PARAM> tags. To differentiate the controls, and show how powerful ASP can be, we've modified these properties as we generated the page on the server. All that the requesting browser sees is the final HTML file; it doesn't know where or why the values are set as they are, it just does its job and renders the page.

The Magic Within the Page

The great thing is how little server-side code really needs to be used to generate this HTML:

...
Else
   'show result page with <OBJECT> tags
   ...
   <% NumControls = Request.Form("txtNumControls") %>
   <H1>ASP Generated Page With Objects</H1>
   You chose to have <%= NumControls %> controls on this page.
   Here they are:<P>
<% For i = 1 to NumControls %>
     <OBJECT ID="SimpleGraph<%= i %>"
       WIDTH=155 HEIGHT=143 CODEBASE="control/graph.cab"
       CLASSID="CLSID:BD374B2A-8155-11D0-9472-9498CA000000">
       <PARAM NAME="Size" VALUE="<%= 100—((i—1) * (100 / NumControls)) %>">
       <PARAM NAME="Caption" VALUE="Graph Control <%= i %>">
     </OBJECT>
<% Next %>
...
End If

We first store the number of controls requested by the user in a variable called NumControls, then print out a string confirming this number. After this, all we have is a simple For ... Next loop that iterates NumControls times, printing a complete <OBJECT> tag each time. We append the loop index variable, i, to the string when producing the ID and PARAM NAME values. We also use i and some mathematics to generate a different SIZE value for each control.

The Control Directory

These <OBJECT> tags assume that the CAB file holding the VB 5.0 control is in a directory named control, beneath the current directory (i.e. in the control subdirectory of the directory where uiimprovement.asp lives). If the file graph.cab is in another location, and the control isn't already installed on your system, the graphs won't be displayed. You can fix this problem by moving the CAB file to the correct directory and refreshing the page.

And that's it. Again, our code in a real world situation with a more powerful control would likely be more complex, but the basic idea is the same. With ASP we generate dynamic <OBJECT> and <APPLET> tags on the fly, as we need them, even to the point of specifying different initialization parameters for each control.


Summary

We've covered a lot of ground over the last two chapters. You should now be able to judge when adding some client-side code or objects could improve your pages, both by adding responsiveness and reducing the workload placed on the server. And of course, the examples demonstrate ways that you can achieve tasks that just aren't possible using only server-side code.

The one big problem with client-side coding is the uncertainty about the environment of the browser. However, the two market leaders are tending to standardize on the features they support, while squeezing other browsers out altogether. As long as you ultimately design for these, you can be sure that the majority of viewers will benefit from your pages.

The main points in this chapter have been:

  • There are often tasks we can't accomplish using just server-side code, and ASP. For this reason, client-side scripting, and objects like Java applets and ActiveX controls, can come in very useful.

  • Client-side programming can also reduce network load and server processing requirements, especially with verification of information before submission.

  • Spreading the processing load between server and client means that careful design is required, otherwise the application becomes almost impossible to maintain.

  • Remember that client-side users can't be sure, with the examples we've see so far, that information submitted from form is not being intercepted or used improperly. At the same time, the server can never be really sure that the information they are receiving in genuine.

This last point is very important, and is the one thing likely to prevent the increasing spread of electronic commerce. In the next chapter, we change tack to look at this subject in a lot more detail.


Professional IE4 Programming

Published by
Wrox Press Ltd.
30 Lincoln Road
Olton, Birmingham, B27 6PA

Printed in Canada

Copyright © 1997 by Wrox Press

All rights reserved. No part of the contents of this book may be reproduced, stored in a retrieval system or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording or otherwise, without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

The author and publisher have made every effort in the preparation of this book to ensure the accuracy of the information. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Wrox Press nor its dealers or distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book.

ISBN 1-861000-70-7

Trademark Acknowledgements

Wrox has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Wrox cannot guarantee the accuracy of this information.

Credits

Authors
Mike Barta
Jon Bonnell
Andrew Enfield
Dino Esposito
Brian Francis
Richard Harrison
Alex Homer
Stephen Jakab
Sing Li
Shawn Murphy
Chris Ullman

Additional Material
Dan Kohn

Editors
Anthea Elston
Jeremy Beacock

Index
Simon Gilks

Technical Reviewers
Jon Bonnell
William Buxton
Diane Castillo
Andrew Enfield
Brian Francis
Richard Harrison
Stephen Jakab
Rick Kingslan
Sing Li
Shawn Murphy
Tony Pittarese
Larry Roof
David Steinmetz
Nick Temple
Omar Khan

Cover/Design/Layout
Andrew Guillaume
Graham Butler


Chapter 1: Introducing Internet Explorer 4

During a keynote speech at COMDEX in 1990, Bill Gates – Chairman and CEO of Microsoft – announced the concept of "Information at Your Fingertips" and gave his vision of the future. He painted a picture of the consequences that desktop computer technology would have in many areas of everyday life. Four years later, Bill Gates gave another COMDEX speech updating his original theme. While he talked about the effects of the recent rapid changes in the technology, there was still no reference to what no doubt will cause the biggest revolution in the IT industry since the PC – the Internet.

It wasn't until December 1995 that Microsoft publicly acknowledged the significance of the Internet and announced an overall Internet strategy. Later, in March 1996, Microsoft delivered a promise to produce a comprehensive set of Internet technologies, products and tools at their Professional Developers Conference.While Microsoft joined the Internet game relatively late, they rapidly gained momentum and have since released an incredible range of innovative Internet products. These products have provided users rich Internet experiences and organizations the mechanisms to develop high impact, secure and mission critical Internet solutions.

One of the most powerful and compelling pieces in the Microsoft Internet jigsaw is Internet Explorer – a Web browser offering users an increasingly sophisticated medium for experiencing the Internet.It wasn't until Internet Explorer 3.0 - Microsoft's first ActiveX compliant browser – that there was serious competition for the market leader Netscape's Navigator. However it caught up very fast. IE3 came with a host of other client Internet applications for communications and collaboration with others. Internet Explorer – version 4.0 (IE4 for short) takes it one step further. Without a doubt, the power of IE4 provides the potential to forever change the direction of Internet development. In this chapter we'll give you a quick introduction, in no particular order, to all of the new technologies that go into making up IE4.

  • Browser integration with the Desktop – these blur the boundary between desktop, network and Internet.

  • Webcasting and Offline Browsing – allows you to subscribe to web sites and then have updates automatically downloaded to your cache allowing you to browse them off line.

  • The Active Desktop – turns the Windows desktop into a web page.

  • Multimedia Controls - methods of enhancing the sound and graphics on your web pages without requiring third party add-ins.

  • Tools for Communication – there are packages for e-mail, newsgroups, multi-user conferencing facilities and for facilities live download of audio and video.

  • Dynamic HTML – an upgraded version of the language, that allows dynamic manipulation of elements on a web page, without the need for a page refresh.

We'll also take a look at the underlying ActiveX technology that provides much of the power in Internet Explorer 4.0, but first we'll begin by looking at what precipitated the need for a new version of the browser.


A Document-centric Computing Environment

The great leap forward in Internet Explorer 3.0 was that, in addition to handling standard HTML documents, the ActiveX technologies enabled IE to be a universal client capable of handling a broad variety of document types. This provided the first step in Microsoft's direction to move towards a document centric computing environment (as opposed to the traditional application centric approach).

IE3 still left a gap in their aim of seamless browsing of resources across the desktop, the LAN and the Internet. At this time, we accessed local and LAN resources with the Windows Explorer and Internet resources with the Internet Explorer. The basis of the next release of Microsoft IE was to address this goal by integrating their browser technologies with the operating system and Windows shell to provide a single view of all resources irrespective of where they were located.

Originally targeted for a later IE5 release was the implementation of Dynamic HTML, the W3C extensions to standard HTML to allow complete control over the complete page to provide totally interactive applications. However, the decision was made for the IE4 release to be delayed and to include this important functionality.

IE4 will provide the user interface for the next generation of the windows operating systems – Windows NT 5.0 and Windows 98. We'll now take a brief look at the significant features of IE4.


Browser Integration with the Desktop

When we first install IE4, an option to install full integration with the Windows shell is given. If this option is selected, we get a number of changes that blur the current boundary between the desk, network and Internet, the most noticeable including:

  • Single explorer - enhances the Windows Explorer to provide a single application that provides both file / directory management and Internet document browsing.

  • Consistent navigation – users now navigate resources in a consistent manner irrespective of whether they are located on the desktop, LAN or Internet.

  • Start bar menu – has been changed to include direct access to Web favorites and extensions to the Find to search on the Internet

  • Web view – provides a new view allowing the contents of a folder to be displayed as an HTML page. This is accessible via a Customize This Folder wizard accessible from the View menu which leads the user through the creation of a Web View.

  • Task bar extensions – currently takes the CoolBar style with a series of bands and can include an Address bar and QuickLinks for quick access to the Web. Bands can be dragged away from the task bar and docked to an edge of the screen; also users can create their own task bar bands of the following types:

  • Folder band – references a system folder and works like a traditional toolbar

  • HTML – the contents of a web page is hosted within the band

  • Custom code – functionality written in any language is hosted within the band

The new task bar extensions can be viewed at the foot of the screen, just right click on the mouse button and select Toolbars from the menu to switch them on.


Webcasting and Offline Browsing

One of the aims of IE4 was to reduce the time and the associated costs of browsing the web yet increase productivity. This is achieved by:

  • 'Push' technology or Webcasting

  • Channels

  • Subscriptions

  • Offline Browsing – downloaded content can be read when it's convenient

Webcasting

Webcasting is the automated delivery of up to date information for selected sites. The information is downloaded from a page, to the user's cache. This reverses the role of IE4, as previously, the user would actively have to search the web for the information required. Now it's possible to tell IE4 what information you require and then get IE4 to do the hard work for you. This new model of retrieving information is termed 'push' technology, as the server is now appearing to transmit or broadcast the information, to the receiver (IE4), which assumes a role similar to a television or radio. There are two methods for achieving this.

Channels

Channels allow a user to enable a frame on the Active Desktop to receive Web content. A number of leading Web content providers will offer channels that will deliver in depth, up to the minute content for a number of different chosen topics, e.g. news, sport, entertainment etc. With IE4 you already get MSN, NBC news, Disney, Pointcast channels and a channel guide as standard on the Internet Explorer channel bar. When looking at channels, the browser assumes a special full-screen format to aid viewing:

It's also possible to create your own channels. This is done with the aid of a Channel Definition File (CDF), which determines the hierarchical organization of the channel and the updating schedule. CDF is effectively a mini HTML-like language that has been defined in a meta-language XML (eXtensible Markup Language). The creator of a channel provides a list of URLs in the CDF that describe what content IE4 must retrieve. This is downloaded to the local cache and then displayed in the channel frame. This whole process is covered in detail in chapter 16, together with a case study which puts together a sample channel in chapter 17.

Subscriptions

Subscriptions are a mechanism to avoid the wasted time spent checking lists of favorites site to determine if any updates have been made and to allow offline browsing.Users may subscribe to a Web site to schedule the regular automatic download of any changes to the pages. If any changes are detected, then the user can be notified with an icon change (on the Task Bar or Favorites menu), or by the delivery of an email message. When you add a favorite to your folder, you are now asked whether you wish to just add the item to your folder, subscribe to the site, or even download the page for the purposes of offline browsing.

You can also update your subscriptions manually from the options on the IE4 Favorites menu.

Offline Browsing

Previously a lot of user time was often spent just retrieving information rather than actually reading the material.IE4's offline browsing facilities solve this problem, which previously required the user having to be connected to the network to be able to read Web content. Subscribed content can now be downloaded and then users can disconnect and still work with the cached information. This optimizes Internet connect time and gives user the freedom to read the pages when convenient, i.e. while traveling on a plane or train when they cannot have a live connection


The Active Desktop

Prior to IE4, the Windows desktop had been used to display a favorite bitmap picture and locate a series of shortcut icons for quick access to applications and documents. Available customization options of the desktop had been limited to a few aesthetic choices. IE4 now provides the Active Desktop where this area can come alive by having access to the same functionality as any Web page i.e. HTML, Scripting, Java applets, ActiveX controls and floating frames.

Active Desktop Items

Active Desktop Items are just small sized web pages embedded in floating frames or document division elements. These frames include the content specified by a URL shortcut. They are very similar to channels but differ because they are intended as brief headlines and links. The items are only meant to be entry points on the desktop into more detailed information. Here's a customized desktop item, which provides links to Dynamic HTML examples from our Instant IE4 Dynamic HTML programmer's reference:

It's downloadable from the site http://www.stonebroom.com, if you follow the Dynamic HTML link on the default page. Locate the Add to Active Desktop option at the foot of the page, and then click on it, to add the item to your desktop.

Active Desktop Items are resident on the desktop and can be moved and resized as required by the user by dragging handles that appear whenever the mouse cursor is moved over the item. Other examples of Desktop Items include:

  • News tickers/ stock market updates

  • Frequently accessed Web applications / sites, for example, a company intranet

  • Notifications / pop up messages

  • Web channels with 'pushed' content

  • Access to search engines

Desktop Items can either be added by users to the desktop or created by specifying either a Web site (i.e. a URL) or a picture. These items complement Active Channels so that you can place links to your channels, within your desktop item. All Desktop Items are added to the IE4 subscription folders and the content is automatically refreshed whenever an update is detected.

Active Themes

The Active Desktop will provide a great opportunity for Web authors to create imaginative and useful desktop components. Part of the Windows 95 Plus Pack included Themes - a series of bitmaps, cursors, icons and sounds providing a consistent theme to the desktop. Microsoft will be releasing Active Themes 2.0 - a hypertext version of this original product.

Channel Screen Saver

IE4 provides a Channel Screen Saver that can be enabled by selecting the Screen Saver tab in the Display properties – accessed either via the control panel or from the menu obtained by right clicking the desktop. The Screen Saver can be configured to display subscribed and favorites sites each for a predetermined time.


Multimedia controls

Microsoft already distributes a set of high level APIs and controls known as DirectX. These are commonly supplied with games and are used to enhance the graphics and sound. In IE4, a component of DirectX known as Direct Animation is included, which provides rich animation features and strong integration with Dynamic HTML.

Direct Animation

Direct Animation comprises of four multimedia ActiveX controls that can provide powerful Web page effects in a fashion that uses up minimal bandwidth and do not require excessive scripting.

  • Structured graphics – provides vector graphics that can be scaled, rotated and filled. Since it's very time and memory consuming to manipulate photographic images (JPGs) or even graphic images such as GIFs, IE4 provides these as a low memory consuming alternative, which has the added benefit of being much easier and quicker to manipulate.

  • Sprite – can be used to create animated images and you can control their playback at a very fine level of detail. Sprite controls can move over backgrounds and they look after the restoration of the original background underneath after the control has moved.

  • Path – allows you to moves objects dynamically on a web page over time, following a pre-defined path. These paths can take on many shapes such as ovals, rectangles, polygons or even spline curves.

  • Sequencer – allows you to control the sequence in which other controls are used and the timing of when actions such as scripts or methods of ActiveX controls are called.

Here is an example of the structured graphics control in action that consists of a Stop Sign image that rotates along its three axes, and some text that scales dynamically:

You will need to view the page at http://rapid.wrox.co.uk/books/0707 in order to actually see the animation take place.

DirectShow

There is a second part of DirectX that is included with IE4, which is known as DirectShow. This is a media architecture which provides audio or video playback from the Internet. DirectShow supports all of the most common multimedia types including MPEG, AVI, WAV and QuickTime. The way to make use of this architecture within IE4 is via the ActiveMovie Control. This can be used to add multimedia support to all of your applications and can be manipulated via a programming language such as Visual Basic or Visual C++. Here's an example that utilizes the Active Movie Control:

Filters

IE4 also provides two sets of multimedia effects that have been added to cascading style sheets. Originally in Platform Preview Release one, they were also implemented as ActiveX controls, but since Platform Preview Release two it has become possible to use them within Cascading Style Sheets instead. These effects are known as filters and there are two different types.

  • Visual Filters - a collection of effects allowing you to alter the appearance of graphics and text, such as by flipping an image upside down, or by making text appear to glow, for example. The results of this filter persist.

  • Transition Filters – a collection of effects allowing you to make graphics and text appear/disappear in a variety of patterns and these come into effect over a specified period of time.

Here's a screenshot demonstrating a visual filter which makes the text appear to glow:

You can find this example page at http://rapid.wrox.co.uk/books/0707.

We look at all four of the Direct Animation controls, the Active Movie control and multimedia filter effects in detail in chapters 11 and 12 of this book.


Communication Tools

Some additions to IE4, which can be optionally installed, are a set of tools for communication and collaboration that can be tightly integrated with other items on the Active Client. These include:

  • Outlook Express for messaging (Email and newsGroups)

  • NetMeeting for conferencing and application sharing

  • NetShow for the receipt of video and audio broadcasts

We're not going to cover Outlook Express or NetMeeting within this book since they can't be accessed or controlled from a web page. However we do look at NetShow in chapter 13, in our overview of components, because this is scriptable from a web page. As this is a basic overview of all IE4's new features, will very briefly touch upon what each of the products can do.

Outlook Express

Outlook Express provides support for the common messaging protocols (POP3, SMTP, NNTP, LDAP and IMAP) and a number of productivity features. Features include:

  • Hierarchical folders for both email and newsgroups

  • Support for multiple mail server and newsgroup servers

  • Facilities to format messages using HTML , enabling the rich formatting that we are accustomed to with Web pages

  • Support for Secure MIME (S/MIME) that enables messages to be encrypted (using public / private keys) and digitally signed

  • Inbox assistant so that messages can be automatically moved, copied or forwarded

  • Support for Lightweight Directory Access Protocol (LDAP) to provide access to white pages servers

  • Support for the messaging protocols provided by Outlook Express can be incorporated into your own applications.

NetMeeting

NetMeeting provides multi-user conferencing facilities over the Internet or corporate enterprise and enables users at remote locations to participate in meetings as if everyone was at the same location. This obviously cuts down on the much time wasted spent travelling to meetings. Features include:

  • A text based chat application allowing multiple users to hold conversations

  • Facilities for Internet telephony and video conferencing allowing users to talk face-to-face in real-time

  • The ability for all conference participants to see the same view of an application running on one machine, for example, watching a PowerPoint presentation or discussing a problem around an electronic whiteboard

The conferencing facilities provided by NetMeeting are open and extensible, and can be incorporated into your own applications. A NetMeeting Software development Kit is available from Microsoft.

NetShow

The NetShow client component is an important part of IE4 and is used for receiving and playing the broadcasted show. Features include:

  • Delivery of audio, illustrated audio (synchronized sound and image) or video content in real time

  • The show is played as received and unlike some previous multimedia Web technologies it avoids the frustrating delays of users having to wait for the complete file to be received before commencement

  • NetShow provides a high quality playback yet uses sophisticated compression algorithms reducing storage requirements and network bandwidth

  • Simple integration with IE4 provides high impact and exciting Web pages – alternatively, the show can be played in its own external frame

  • Hyperlinks can be included in the show's visual images


Dynamic HTML

IE4 delivers the next generation of Web Browser technologies including Dynamic HTML. Dynamic HTML is a collection of HTML and CSS specifications from W3C, forming part of the new HTML 4.0 standards, which provide the ability to dynamically update the contents and styles of a Web page without refreshing the page.

While Dynamic HTML still supports many of the tags and attributes available with the previous generations of browsers, what's new is that full access to all items in the browser is available from a scripting language. This also means that web pages designed for Dynamic HTML will degrade gracefully on earlier browsers such as IE3. It frees the user from the straitjacket of static pages, by providing many new features. The most significant of these are:

  • All of the elements on the page are now accessible to the scripting languages and also with some extra effort, so is any software component that has been installed on the desktop.

  • Extensions to the browser object model, in particular the document object, mean that there's far more hooks for the scripting languages to use.

  • Extensions to style sheets to bring support in style sheets in line with the CSS1 recommendations.

  • Dynamic redrawing of any page or section of a page, without the need for a full-page refresh.

  • Absolute positioning of elements on the screen with x and y type coordinates, including a third dimensional coordinate, the zindex, which allows a desktop publishing format of layout.

  • New event handling techniques, including a facility called event bubbling that reduces the amount of script code previously required. When an object is notified of an event it can either process it or delegate the processing to the parent object.

On top of this, most of IE4's new features, such as multimedia controls are directly programmable with Dynamic HTML, and we'll spend the rest of the first section of the book getting you up to speed with it.

Scriptlets

A late addition to IE4 was the innovation of scriptlets. These are reusable components that have been created in Dynamic HTML. These components are scripts that are written by the developer according to certain criteria. They can be added to web pages, with the <OBJECT> tag, in the same way that ActiveX controls and Java applets can. They can be written in either JavaScript or VBScript, but aren't limited to just these two languages. Effectively by embedding a scriptlet within a page, you are embedding a browser within a browser. These may not seem like an overly useful thing to do, but when you realize that you can follow these principles to embed a browser within any application from Word to Access, by using ActiveX, you can then see how almost any application can suddenly be Internet enabled. We look at scriptlets in more detail in section 3.

Dynamic HTML in action

While we shall be delving deeper throughout this book into Dynamic HTML technologies and the associated programming techniques – we'll now take a look at an example which demonstrates the following capabilities:

  • Dynamic styles - the menu options are displayed in a frame on the left hand side of the display - as the mouse pointer hovers over the option text, the menu text changes color and adopts additional decoration

  • Dynamic content - help text is displayed in a frame at the base of the display - as the mouse pointer hovers over various items, the help text is updated

  • Absolute positioning - the home page title text is displayed twice using two colors with the second copy slightly out of phase from the first - this gives a shadow effect and the impression of three dimensions

  • Multimedia controls - a music control is used to play some Beethoven style music and a visual effect control is used to invert the logo whenever the mouse pointer hovers over it

  • Data binding - we use the Microsoft Tabular Data Control (TDC) to retrieve information (a list of Wrox book titles) from a text file and insert the information in a table on the Web Page

The example is available from the Wrox web site at http://rapid.wrox.co.uk/books/0707. The opening page provides an ordinary looking two frame page, but the frame on left contains a menu which reacts dynamically to user interaction by highlighting an item when the mouse pointer moves over it.

If you click on the Order item, you'll be able to see data binding in action. The resulting list of books is dynamically generated from the database, and you can create an order for several books client-side, in the same way as you can with a CGI script or Active Server Pages and server interaction.

Take some time to browse the source code as well, from the View Source option, because while you might not understand it all yet, it demonstrates that many of the pages were generated with relatively little code.


Activating the Internet

Of course, a large part of IE4's strength is derived from the way it harnesses existing web integration technologies. One of the primary strengths of the web is its ability to be viewed from many different platforms, the same code being executed successfully on rival platforms. Two differing, but not necessarily mutually exclusive, visions of how this can be achieved are ActiveX and Java. IE4 is designed to be able to execute both Microsoft's own ActiveX technology and Sun's Java. We'll take a little time to examine both of these technologies now as they form a vital part of the future of the web and their support was central to IE4's design.


ActiveX

ActiveX has to be one of the most misunderstood and ambiguously used expressions in the Internet technology arena. When Microsoft released their first set of Internet tools in March 1996, they announced ActiveX technology – which in all truth was just a new marketing name for their existing OLE technology. ActiveX (or 3rd generation OLE technology) is a framework that allows software components to co-operate even if they have been written by different vendors, at different times using different tools and different languages, and if the objects are located in the same process, same machine or distributed over multiple machines. Put simply, ActiveX provides the software plumbing between software objects and insulates the component developer from such complexities.

Many people get confused because ActiveX actually encompasses a number of technologies and is not just one thing. Each ActiveX technology defines the interfaces between the objects to implement the particular technology. For the Internet, examples include:

  • ActiveX Documents – enables the browser to support non-HTML documents

  • ActiveX Scripting – enables script logic, included with a downloaded Web page or a server-side ASP page, to be executed

  • ActiveX Controls – provides a method of packaging client components for reuse across platforms and development environments which can then be dynamically downloaded as needed and used within a web page

  • ActiveX Server Components – enables the Web Server (IIS and ASP) to interface to server software components

To most users, ActiveX is transparent and they just see the effects of these technologies - it is irrelevant to them whether it is ActiveX or black magic that is operating behind the scenes. However, it is different for the developer - we shall see later how the Internet ActiveX technologies provide a powerful environment for creating interactive Web content.

Underneath ActiveX is the generic Component Object Model (COM) which defines the binary interface between objects. The original specification of COM always allowed for co-operating objects to be located on different machines, but this was not implemented until Windows NT 4.0 and was then called Distributed COM (DCOM). Because of this, in reality DCOM and COM are now the same animal. This is all we really need to know about COM/DCOM, although they will be mentioned throughout the book.

There does exist a small anti-ActiveX camp (with members typically having investments in alternative technologies!), that are concerned this will make the Internet as Windows-centric as today's desktops. However, since ActiveX is the most advanced component software platform around, there is no doubt that it will build on its current momentum and play a major role in the next wave of Internet application development.

Corresponding to Microsoft's recent focus on Internet technology has been their aim to embrace industry standards and support openness. The ActiveX technologies have been given to an independent consortium called The Active Group and Microsoft has been working with a number of third parties to support ActiveX on non-Windows platforms including UNIX and Apple Macintosh. NCompass Labs http://www.ncompasslabs.com have an ActiveX Plug-in for the Netscape Navigator browser.

Let's now see ActiveX in action.

ActiveX Documents

As part of their move towards seamless browsing, ActiveX Documents are fundamental to Microsoft's aim to achieve a document-centric world rather than the current application-centric notion. ActiveX Documents are based on the previous OLE DocObjects technology that allowed in-place activation or visual editing. For example, an MS Excel spreadsheet embedded in a MS Word document could be edited directly within the Word frame, with the menus and toolbars adapting to make available the Excel facilities.

If you look at the size of the IE4 executable, IEXPLORE.EXE, you will see it is so very small for an application that is as rich in functionality as IE4. In fact, this executable is just a simple process that loops to handle windows messages and provide a container for ActiveX Documents. Normally IE4 displays HTML – this is handled by a software component providing Web Browser support and using the IE4 container for displaying the page. The Web browser functionality is provided by an object implemented in SHDOCVW.DLL.

As IE4 is an ActiveX Document container, it can support any applications that support the interfaces (i.e. methods, properties and events) that are defined by the ActiveX Document specifications. This includes the Microsoft Office products and other application vendors that are rapidly supporting this technology. This means that as we navigate the numerous resources within an Intranet, we no longer need to invoke separate word processors, spreadsheets, etc. to access the various items of information, IE4 can do it for us:

ActiveX Scripting

Scripting is an easy way to make our Web pages come alive and allows the browser to interact with the user and software components (i.e. trap events, invoke methods and access properties). It involves incorporating high level script commands into the HTML document that automatically get invoked by the browser when loaded or by the user clicking on something.

Rather than just develop script processing for IE4, Microsoft has used ActiveX to provide a flexible architecture for adding any scripting language to an application. ActiveX Scripting allows script hosts to invoke scripting services within script engines. Further the hosts and engines can be from different software vendors and can implement different languages, since the plumbing between the two is handled by COM/DCOM. The ActiveX Scripting specifications define the interfaces that a script host and script engine must support. The script language, syntax and execution rules are defined by the vendor of the script engine. All script logic is interpreted on the fly by the script engine, there is no concept of compiling the scripts. This is shown as follows:

IE4, ASP and Visual InterDev are examples of script hosts, but you can now easily add scripting to your own applications. VBScript and JScript are examples of script languages, but again, you can also develop your own. VBScript is a subset of Visual Basic for Applications (as used by the Microsoft Office Products) which in turn is a subset of the popular Visual Basic programming language. JScript is Microsoft's implementation of the Netscape's JavaScript, although Microsoft and Netscape are currently working on standardizing their versions of JavaScript.

The real power of scripting comes with the ability to interact with other objects. This enables accessing an object's properties, invoking methods and detecting events, and of course all this happens using COM/DCOM under the covers. Accessible objects are either:

  • Intrinsic (built-in) objects exposed within the script host – often referred to as an object model.

  • Executable software components: which are packages of reusable code that usually serve a specific function and don't have to be resident on the browser and can therefore be downloaded when needed.

Client-side scripting can be inserted into an HTML page using the <SCRIPT> tags pair, and so can be incorporated within minimal impact. To identify the script language, the LANGUAGE attribute is used, as shown in the following template.

<HTML>
<HEAD>
   <TITLE>Document title
   </TITLE>
   <SCRIPT LANGUAGE="VBSCRIPT">
      Client-side VBS scripting logic
   </SCRIPT>
</HEAD>
<BODY>
</BODY>
</HTML>

If you wish your page to viewed normally in older browsers that cannot process script as well, then you can hide the script within HTML comment tags <!-- and -->.

The client-side scripting can interface to all element objects in a web page (e.g. tags, images, text, etc.), browser objects (e.g. windows, frames, history, etc.) and 'talk to' any object that has been included in the page. From this, we can see that scripting is what really enables the Dynamic in Dynamic HTML. We can now change any part of the Web page as a user interacts with the page, as demonstrated in our Wrox Press orders example earlier.

While there are third parties supplying alternative scripting languages (e.g. Perl etc.), most people will use one of the two Microsoft supplied core options, VBScript or JScript – but which is best? Well the truth is that there is very little difference in between the capabilities of the two. Both languages are capable of being used by those with minimal programming experience. At the time of writing, IE3/IE4 are the only browsers that support VBScript. Thus, if you are aiming for mass penetration on the Internet then you should probably restrict the scripting to JavaScript, although to remain functional on both IE and Netscape browsers you will require a lot more than just one simple JavaScript program, because the two implementations of Dynamic HTML are so different. If you are developing for an Intranet, where you have control over the infrastructure and can enforce IE4 as the choice of browser, you can choose to use VBScript.

Active Server Pages and Scripting

Active Server Pages (ASP) is a technology that provides a server-side application framework. You may wonder why we're discussing a server-side technology in a books focussed primarily on the client-side. This is because ASP provided a robust object model that also allowed the web page developer to do things dynamically. Later in the book we will take a look at how can combine the two technologies, but first we need to take a brief look at what ASP can do.

ASP allows executable script logic to be embedded within an HTML page. It enables the scripting to interface to a number of internal objects, which automatically handle many of the menial tasks, and so simplifies the script logic. ASP uses server-side scripting to dynamically create HTML responses. The content generated is typically based on the following features:

  • User's identity

  • Parameters in the HTTP request

  • Interacting with other objects (e.g. ASP objects, multi-tier client / server business objects, middleware to access databases and legacy systems, BackOffice components, etc.).

ASP provides a number of built in objects and useful components. The built-in objects simplify many of the common server-side tasks such as handling the HTTP request/responses, the user's session and the Web environment.

Server-side scripting is inserted in an ASP file using either the <SCRIPT> tags pair or <% and %> delimiters. To distinguish client-side from server-side scripting, the latter's <SCRIPT> tag should include the RUNAT="SERVER" attribute and value. For example:

<HTML>
<HEAD>
   <TITLE>Document title
   </TITLE>
   <SCRIPT LANGUAGE="VBSCRIPT" RUNAT="SERVER">
      Server-side VBS scripting logic
   </SCRIPT>
   <SCRIPT LANGUAGE="VBSCRIPT">
      Client-side VBS scripting logic
   </SCRIPT>
</HEAD>
<BODY>
    <% Server-side VBS scripting logic %>
</BODY>

Script that runs on the server can always run VBScript, because it has to be running on IIS which is a Microsoft Product. The script which runs on the client(browser) has to be either JavaScript or VBScript, depending on the browser or preference.

Prior to scripting, all user interactions had to be handled by communication with the Web Server. Script logic now enables client-side processing to occur and can help to avoid some of the time-consuming communication with overburdened servers. It should be noted that the ASP server-side scripting is not exposed in the generated HTML file, where as client-side scripting could be inspected by simply viewing the HTML source code. This means that if you are concerned about the people copying your clever code or seeing your confidential business rules then you should engage such logic in the server. In practise, you will find that both server-side and client-side scripting have specific uses and that the design of your Web application will require a combination of the two.

ActiveX Controls

An ActiveX Control is a software component that gives a specific item of functionality to a parent application (i.e. an ActiveX Control container). IE4 is such an application that can host ActiveX Controls within a Web page. The forerunners to ActiveX Controls were OLE Controls (for 32 bit applications) and the original, but limited, VBX controls (for 16 bit Visual Basic applications only).

ActiveX Controls expose properties and methods and can fire events, using COM/DCOM under the covers. The combining of this with ActiveX Scripting provides our goal of user interactivity. For example, clicking a button would fire the associated event and could cause a script to invoke a method on a control, which may then effect the results seen in the Web page. ActiveX Controls cannot directly communicate with each other. However, this can be achieved via script logic.

ActiveX Controls can contain sophisticated logic and add features to Web pages that could never be achieved with standard HTML even with scripting. It is estimated that there are over 1,000 commercial ActiveX Controls on the market; examples of cool controls include:

  • Citrix WinFrame Control – enables a Windows application to be run on the server but displayed in a Web page http://www.citrix.com

  • Black Diamond Surround Video – provides facilities for 360-degree images that can be panned by mouse movement http://www.bdiamond.com

  • MacroMedia ShockWave – provides extensive facilities for creating web based animations http://www.macromedia.com

  • The ActiveX site – provides access to a large library of ActiveX controls from a wide variety of vendors http://www.activex.com

One great advantage that ActiveX Controls provide is that they are automatically downloaded and installed, as needed. If a Web page references an ActiveX Control that is either not installed or is out of date, the deployment of the control is automatically handled by a helpful facility within IE4 called the Internet Component Download Service (ICDS). ICDS is also responsible for handling the Authenticode processing. Authenticode is Microsoft's and Verisign's Digital Signing system. It provides a mechanism to digitally sign and authenticate code to independent vendors. It will present the user with a certificate dialog highlighting details of the component's origin and can be used to hold vendors accountable to their software, whenever it causes problems:

{bmc0707_01_11.bmp}

Or alternatively, IE4 will flag a problem such as the control being is unsigned or not in the exact format as when released by the author. If this last case happens, it could mean that the control has been tampered with and isn't safe to use.

ActiveX Controls are compiled into native machine code. This means that if you intend to support multiple target client platforms you must produce a separate version for each. You should also remember that code size is important as many users connect to the Internet using slow modems, and might not be patient enough to wait for a fat control to arrive.

The OBJECT Tag

An ActiveX Control is embedded into an HTML page by specifying a number of attributes between the <OBJECT> tag pair. For a visual control, this will be displayed in the page at the point where the tag is placed. Every ActiveX Control is uniquely identified by a sequence of numbers called a Class ID or Globally Unique Id (GUID). For example, the Class ID of the Microsoft NetShow Client Control is {2179C5D3-EBFF-11CF-B6FD-00AA00B4E220}. Since it is obviously not possible to remember such sequences for the numerous controls that are available, we need a quick way finding the Class ID.

One easy way is to use the Insert ActiveX Control facility in the Microsoft FrontPad (that ships with full installation of IE4) & FrontPage HTML editors. By selecting this menu option, the user is able to select an ActiveX Control from all controls installed in the Registry, and this will automatically insert the <OBJECT> tags with the appropriate Class ID. This automatic insertion is very useful since the nature of Class IDs tend to make typos frequent; it is very easy to key a wrong digit and not realize it until the control fails to instantiate.

Control properties are specified using the <PARAM> tag that takes two attributes – NAME and VALUE, for the property name and the value to initialize the property to. The following example shows what the code for a Tab control looks like:

<HTML>
<HEAD>
<TITLE>Tabs</TITLE>
<SCRIPT LANGUAGE="VBScript">
   <!—to be added -->
</SCRIPT>
</HEAD>
<BODY bgcolor=cyan>
<OBJECT ID="TabStrip1" WIDTH=69 HEIGHT=107
 CLASSID="CLSID:EAE50EB0-4A62-11CE-BED6-00AA00611080">
    <PARAM NAME="ListIndex" VALUE="3">
    <PARAM NAME="BackColor" VALUE="16776960">
    <PARAM NAME="Size" VALUE="1834;2822">
    <PARAM NAME="Items" VALUE="One;Two;Three;Four;">
    <PARAM NAME="TabOrientation" VALUE="2">
    <PARAM NAME="MultiRow" VALUE="-1">
    <PARAM NAME="TipStrings" VALUE="One;Two;Three;Four;">
    <PARAM NAME="Names" VALUE="One;Two;Three;Four;">
    <PARAM NAME="NewVersion" VALUE="-1">
    <PARAM NAME="TabsAllocated" VALUE="4">
    <PARAM NAME="Tags" VALUE=";;;;">
    <PARAM NAME="TabData" VALUE="4">
    <PARAM NAME="Accelerator" VALUE=";;;;">
    <PARAM NAME="FontCharSet" VALUE="0">
    <PARAM NAME="FontPitchAndFamily" VALUE="2">
    <PARAM NAME="TabState" VALUE="3;3;3;3">
</OBJECT>
</BODY>
</HTML>

Remember that the <OBJECT> tag attributes, including <PARAM> are all editor generated and you wouldn't have type these in yourself.

The tab control is inserted in the page as follows, but if you click on it, there is no response:

An important attribute in the <OBJECT> tag is the ID that is used to provide an easier name to reference the control; in particular it is used by the scripting language to apply any logic to the object. We have to first set the ID in the HTML:

...
<BODY bgcolor=cyan>
<OBJECT ID="TabStrip1" WIDTH=69 HEIGHT=107
 CLASSID="CLSID:EAE50EB0-4A62-11CE-BED6-00AA00611080">
...

in order to be able to reference it within a scripting language and therefore get the tab control to respond to each tab that is selected:

<SCRIPT LANGUAGE="VBScript">
Sub TabStrip1_Click(Index)
select case Index
  case 0  msgbox "one"
  case 1  msgbox "two"
  case 2  msgbox "three"
  case 3  msgbox "four"
end select
end sub
</SCRIPT>

The <OBJECT> tag can include a CODEBASE attribute to specify the location of the control and the required version. If the control has not been previously installed or it is out of date, the ICDS uses the CODEBASE's URL to retrieve and install the control. For example

CODEBASE="http://activex.microsoft.com/controls/iexplorer/timer.ocx#
      Version=4,70,0,1161">

Managing Downloaded ActiveX Controls

Until IE4 there was no smart way of managing the numerous ActiveX Controls that were downloaded and installed. It was possible that controls could be installed, used only once, and then would become redundant wasting disk space. With IE4, downloaded controls are installed in a folder called Downloaded Program Files within the Windows folder. When this folder is viewed via the Windows Explorer, a shell extension provides special functionality to manage the content. Each installed control is represented by an icon. By right clicking on an icon, a control can either be uninstalled or have its properties inspected.

You have three different choices of view, which supply general information such as the Class ID, Dependency Information and Version Information:

To create ActiveX Controls, the developer has several options, including the following:

  • Visual Basic – from version 5, VB can create ActiveX controls; furthermore, there is a free version, VB Control Creation Edition (VB CCE), that is limited to just the creation of ActiveX Controls and can be downloaded off the Microsoft Web site http://www.microsoft.com/vbasic/

  • Visual C++ – provides three frameworks for creating controls: Microsoft Foundation Classes (MFC), the Active Template Library (ATL) or from the ActiveX Controls Framework (BaseCtl - part of the Internet Client SDK). The latter is designed for building very light controls but requires expert knowledge of OLE. MFC simplifies the development of such controls but, compared to other two, generates bulky code and requires the MFC libraries to be installed.


Java

Java is another technology that aims to bring interactivity to the Web and networks by dynamically distributing executable content as required. It originated from Sun Microsystems http://java.sun.com/. They were working on a language that would be cross platform capable and, at the same time, help them to propel their products in the market. They recognized its suitability as a language for embedding executable logic within an HTML page for processing by a Java enabled Web Browser. While the term Java encompasses a number of Sun owned technologies, it is frequently used to refer to the Java programming language. This distinct from JavaScript which is very loosely based on Java and C++, and is used in scripting and should not be confused with Java.

Java is a simplified object-orientated programming language that was originally based on C++, and so looks familiar to C++ programmers. However, the Java designers removed some of the C++ features that can cause developers problems and stop code from being robust.

A Java program is compiled into Java byte code – while this is an optimized format it is not native CPU machine code. Java Byte code is executed by an interpreter called a Java Virtual Machine (JVM)which converts byte code, on the fly, into native machine code.Some JVMs include a just-in-time (JIT) compiler that speeds up the processing by keeping a copy and reusing translated codes. Alternatively, some vendors have produce native compilers that directly generate native machine code for the target platform. Like most operating system vendors, Microsoft developed a Java VM for their Windows operating systems and then licensed the name from Sun.

A Java program running in a Web Browser is called an Applet and is referenced in the HTML page with the <APPLET> tag. Java applets can be anything from games, business resources, and complex multimedia effects to simple animations and scrolling logos, or even dynamic menus, such as the one featured at http://java.sun.com:

A Java program running standalone is called an Application. A great advantage of a Java program is that it is portable – it can run on platforms that support the Java VM. Java is sometimes summarized as "Write Once – Run Anywhere".

A lot of effort has been made to ensure that Java Applets are safe and cannot cause malicious damage to a client machine. Typically the JVM uses the sandbox security model to ensure this doesn't happen. By sandbox we mean a model which restricts access to potentially dangerous parts of the operating system – i.e. you don't want an applet to be able to wipe your hard disk. The Microsoft JVM supports the Authenticode mechanism and allows trusted Applets the freedom to access certain system resources.

Many people question whether Java is better than ActiveX – in truth, both have their strengths and weaknesses. While many view the technologies as competing, Microsoft has embraced the idea that Java and ActiveX complement each other. The Microsoft JVM is actually an ActiveX control that can expose public variables and functions, as shown in the following diagram. This enables an Applet to co-operate with a Web Browser scripting language. Furthermore, the Microsoft JVM provides full support with COM/DCOM allowing the two different technologies to integrate seamlessly.

Microsoft's Java development tool is called Visual J++. In addition to the standard JAVA classes found in the AWT, IE4 comes with Microsoft's Application Foundation Classes (AFC) so saved the trouble of the classes having to be downloaded each time. The AFC is a set of powerful class libraries that expand upon the standard Java offerings to provide user interface capabilities that have the visual quality that Windows users are accustomed to. AFC is designed to run on all platforms supporting a JVM except for two classes that are Internet Explorer specific and are used for data binding.

It's not really appropriate to go much further into Java in this introduction as we won't be using it in the book. If you wish to know more then look out for Beginning Java, by Wrox Press ISBN 1-861000-27-8.


Summary

In this chapter we have taken a fast paced look at the new features that make up IE4. The six most significant features are the shell extensions, the active desktop, multimedia controls, webcasting technology, the communication tools suite and Dynamic HTML. As the way in which IE4 communicates with Web servers is crucial to its operation, we also took the time to look at the technology that Microsoft has now delivered to provide the Active Platform. We finally took a very brief overview of an alternative web integration technology, Java.

As this book is all about IE4 programming, we won't be really be considering the new extensions to the shell or the communication tools suite (except for NetShow) as they aren't really programmable. Instead we will be concentrating solely on the other four features, starting with Dynamic HTML. This is the language that enables many of the new features and so it's important to know it in some detail. This text will help you to wind your way through the complexities and subtleties of this emerging technology, and show you what can be done with the features found within Microsoft's Internet Explorer 4.0.


Chapter 2: Formatting with Style Sheets

The more popular HTML has become, the more page designers accustomed to a finer level of control over the appearance and location of each element have chafed at the limitations imposed by the language. The World Wide Web Consortium's (W3C) solution was to develop a specification for a new addition to HTML called style sheets. Over the last year both Netscape and Microsoft have adopted W3C consortium's standard (more or less), paving the way for web pages that can be laid out as precisely as magazinesand other printed materials. For those with programming experience, style sheets allow HTML elements to be placed with the same precision as elements on Visual Basic forms – an improvement, but they still don't offer the fine level of detail that might be required.

With the introduction of Dynamic HTML, the functionality of style sheets has been extended allowing developers to gain increased control over the positioning of elements, via the provision of x and y type coordinates which are calculated in pixels. Also, it is now possible to alter properties of elements on a page dynamically, and for the positions to be altered without a page refresh. Indeed style sheets now form a main method for connecting your scripts to the elements on the page. However, we won't be covering style sheets and scripting until Chapter 5. Although many of the more advanced Dynamic HTML effects require this, such as element movement, this chapter provides a quick overview on style sheets and demonstrates how useful style sheets can be on their own.

In this chapter we'll be talking about:

  • What exactly style sheets are and why we need to use them.

  • How they provide for formatting and positioning in Dynamic HTML.

  • How to add style information to HTML tags.

  • The new style object.


What is a Style Sheet?

A style sheet is a series of style rules that can be linked to elements in an HTML document. IE4 implements style sheets through the use of Cascading Style Sheets (CSS). Cascading Style Sheets were developed by W3C to provide a standard for effective markup, layered text and images, and exact positioning of items. IE4 and Dynamic HTML build on the W3C CSS specification to enable faster and more interactive Web pages. What exactly are the advantages of learning yet another addition to HTML?


Benefits of Style Sheets

HTML in its pre-web form was designed to enable only the meaning of text phrases to be defined – any mention of formatting was explicitly left out. The idea was that each viewer of an HTML page would be free to set the specific formatting they preferred for each functional element. Users with high resolution color monitors could view their pages one way, and those with monochrome text only displays could view them in another.

The web adopted HTML, which quickly evolved, and browser manufacturers soon began to add additional capabilities to existing tags and even new tags themselves to enable web authors to add formatting to their pages. Just as quickly as these tags proliferated they started to detract from the content/formatting division so important to HTML. But, while these tags gave some improved control over the appearance of HTML, they weren't enough to remedy the problem, since authors continued to replace lengthy blocks of text with graphics when they weren't able to create the effect they wanted with new tags.

Fortunately, style sheets overcome these problems while at the same time adding even more features: everything a layout artist could want. However, they still give the user some control over the browser presentation of different HTML.

General HTML Benefits

Style sheets enable the separation of content and formatting in HTML documents. In fact, styles can be so separate from the HTML pages that use them that we can even define them in another file. With styles characterized in a separate file, they can easily be applied to more than one document – a great advantage.

Wherever we define as our styles, we know that they won't alter the text in any way. This means that people with text-only browsers (and browsers that don't support style sheets) will still be able to see all of the content – it just might not look quite as good.

But these advantages wouldn't mean anything at all if the first purpose of style sheets, to give web developers greater control over the appearance of their pages, wasn't met. As we'll see, style sheets do indeed make pages with all kinds of objects possible in any location we desire.

New Uses for Dynamic HTML

All of this is great, but you need to consider how style sheets apply to the topic at hand. If text and other elements are statically placed when the page is loaded, can we really call it Dynamic HTML? We don't need to argue about this point, because our elements aren't statically arranged – they can be moved and their display properties can be changed. These features are all due to the object model, styles and script code. Before we get into a description of how style sheets work and how they relate to Dynamic HTML, let's get a small taste of what they can do for us.


A style sheet example

The screen shot of the browser below shows what we can do with a normal HTML page and a few style declarations.

Without style sheets this sort of page would have been impossible to create without using a graphic… not any more! Here's the HTML code for this page:

<HTML>
<HEAD>
<TITLE>CSS Style Example</TITLE>
<STYLE>
<!--
BODY   {font-family: Times;
   font-size: 125;
   font-weight: bold;
   color: red }

.layer   {font-family: Arial;
   font-size: 40;
   font-weight: bold;
   color: blue;
   margin-top: -75px }
-->
</STYLE>
</HEAD>
<BODY>
<CENTER>
Wrox Press
<DIV CLASS=layer>DHTML Programming</DIV>
</CENTER>
</BODY>
</HTML>

If you ignore the information between the <STYLE> and </STYLE> tags and <DIV> and </DIV> tags the page consists simply of two lines of text – Wrox Press and DHTML Programming. Obviously, the work must be done by the new HTML in the page. All body text has been defined as being in the Times font face, 125 points large, bold, and colored red. Our Wrox Press line of text is displayed in this base body style. We've also defined another style and called it layer (an additional style like this is also known as a 'class'). This extra style uses the Arial font and sets the top margin for any text displayed with this style as 75 pixels above the normal font baseline. DHTML Programming is displayed using the layer class.

The end result of this work is that the second line of text is displayed in a different color and size, and 75 pixels above where it would be placed normally – which puts it immediately in the middle of the first line of text we displayed.

In order for Internet Explorer 4.0 to use any style sheet information when rendering the page you must have style sheets turned on. This is the default setting of Internet Explorer 4.0, but in case you've turned style sheets off, you can turn them back on by opening the View menu, choosing Options…, and switching to the Advanced tab. To turn on style sheets make sure the checkbox next to the Use Stylesheets option (listed under Browsing) is checked.

We'll be covering all of this and more in detail over the rest of this chapter.


Style Sheet Capabilities

We've been alluding to the capabilities of style sheets for the last three pages, but short of the first example, we haven't talked exactly about how style sheets extend HTML. This section of the chapter will spell out the capabilities of style sheets.

While we call the different style sheet elements 'properties' we don't use them in exactly the same way we use properties of objects. In our example, we set the BODY text properties like this:

BODY   {font-family: Times;
   font-size: 125;
   font-weight: bold;
   color: red }

As we'll see later in this chapter when we talk about the style object, this is clearly a different way of doing things than we use for objects. With objects we refer to a property by appending a period and the property name to the name of the object instance, like this:

MyTextObject.fontFamily = "Times"
MyTextObject.fontSize = 48

Although it's a semantic difference and not important for understanding the capabilities of style sheets we should keep this in mind while we learn about the different style properties that we can use.


Formatting Properties

The appearance of elements is of course a primary concern of ours, and numerous properties of this type can be set through the use of style sheets. In this section we'll talk briefly about the different style properties that can be used to change the formatting of an element (be it a block of text, an image, or so on).

The style sheet reference in section E can provide you with more information about each style property, including the values they can assume and their applicability to different elements.

Font Properties

These properties are used primarily with groups of text. They control every possible element of the appearance of a block of text.

Property | Description | Possible Values
font-family | name of a specific or generic font family | specific (Arial, etc.) generic (serif, etc.)
font-style | changes in font style | normal, italic, oblique
font-variant | changes in font appearance | normal or small-caps
font-weight | the weight of the font | normal, bold, bolder, lighter or numeric values 100-900
font-size | size of the font | in absolute or relative units
font | allows setting of all font properties at one time | all of the above

As we'll discuss in more detail later in this chapter, one of the ways of adding style information to tags is simply to include a STYLE attribute in the HTML tag itself. We can specify multiple style sheet elements by delimiting them with a semi-colon. This is the format we'll generally use to for our examples. For example, to set the font of a single <H1> element to 48 point Times New Romanitalic, we could use this HTML:

<H1 STYLE="font-family:Times New Roman; font-size:48pt; font-style:italic">This is a stylish heading</H1>

This code produces a heading that looks like this (see for yourself – just add the above code to a page with an <HTML> and <BODY> block):

Multiple fonts can be specified for the font-family property. Browsers can then use the first font on the list that is available on the machine in question. Font names should separated with commas.

For example, suppose we would like to display our text in the Arial Black font, but aren't sure that all of our users will have this font installed on their machines. We could specify Arial Black first in the font-family string, followed by another serif font that is more common, like Arial. With this arrangement we'll get Arial Black if it's installed, but our text will still look good if Arial Black isn't on the machine. This is how we would do it:

<H1 STYLE="font-family:Arial Black,Arial; font-size:24pt">This heading uses Arial Black</H1>
<H1 STYLE="font-family:Arial; font-size:24pt">This heading uses Arial</H1>

The code is rendered by the browser like this (on a machine that has both Arial Black and Arial installed):

Text Properties

While you're setting the traits of your font, you might also want to set the properties of the text you'll display with this font using the many different text properties that style sheets provide.

Property | Description | Possible Values
word-spacing | distance between words | numeric value
letter-spacing | distance between letters | numeric value
text-decoration | special appearance of the text | underline, overline, strikethrough
vertical-align | vertical positioning of an element in the line | baseline, sub, super, top, text-top, middle, bottom, text-bottom, or percentage
text-transform | capitalization of the text | capitalize, uppercase, lowercase, none
text-align | alignment of the text | left, center, right, justify
text-indent | indentation of the text | numeric or percentage
line-height | height of the line | numeric of percentage

These properties are useful if you want to control your text like you can in a desktop publishing program. As the power of these properties demonstrate, the change from the old HTML to style-sheet enhanced HTML is nothing short of amazing.

We've always been able to center HTML using the <CENTER> tag. Style sheets allow us to control the justification of our text more robustly, with the text-align property. As an example of some of the things that the text properties can do, take a look at the browser in this screen shot:

This page was created with just these three lines of style sheet HTML:

<P STYLE="text-align:left">Normal text.</P>
<P STYLE="text-align:center; text-decoration:underline">Underlined text.</P>
<P STYLE="text-align:right; text-transform:uppercase">Uppercase text.</P>

With this we can begin to see how style sheets now give users of HTML the same power that used to only exist in word processors and page layout programs.

Color and Background Properties

Property | Description | Possible Values
color | color of the element | color name or RGB value "#RRGGBB"
background | background of the element | color value, URL location, repeat value, scroll value, position value

The color property is simple but useful – just specify a color and the element is displayed in this color. Background is equally useful, but more complicated because of its wide variety of options. The background of an element can be set to a solid color, an image, or a combination of both. Images can be set to repeat and / or scroll with the document. So if we wanted the code to set the background of the <BODY> tag to have a default color of red, but to also have the Wrox logo repeating down the y-axis of the page, underneath some text, it would be fairly simple to achieve:

<BODY STYLE="background: url(wrox.gif) red repeat-y">
This is some example text
</BODY>

The resulting screen would look like this:

Supplementary Background Properties

In addition to the basic background property, style sheets in Internet Explorer 4.0 support a number of properties that address specific elements of the background, like background-image, background-color, and so on.

background-attachment | background-color
background-image | background-position
background-repeat |

While the background property can be used to set all of these attributes at one time, with these properties you can modify a part of the background without having to set each element. So you could you create the last example by setting each supplementary background property as follows:

<BODY STYLE="background-image: url(wrox.gif); background-color: red; background-repeat: repeat-y">
This is some example text
</BODY>

Border Properties

Using these properties, each side of the border around the element can be set with a style and width value.

Property | Description | Possible Values
border-top |  |
border-right |  |
border-left | Sets the width, color, and style |
border-bottom | properties of the border around |
border | the element |
color |  | any color value
width |  | numeric
style |  | thin, medium, thick

Supplementary Border Properties

The border property has additional properties in the same way that background does.

border-color | border-style
border-right-width | border-left-width
border-top-width | border-bottom-width


Using Style Sheets

So we know what style sheets can do for us, but we haven't talked about how we can actually add them to our pages. There are several different ways to provide style information and an equal amount of latitude when it comes to reapplying style information to different but similar tags.


Specifying Properties and Values

To indicate to the HTML engine our setting for a property we simply use this syntax:

TAG {property:value}

We'll see an actual example of this in the next paragraph. We can specify additional property values by adding new text and separating them with a semi-colon. We can also set the property values for more than one tag at time; for this purpose we use a comma as a delimiter.

For example, first, set the body text to the Arial font:

BODY {font-family:Arial}

Then set all of the headings to be green:

H1, H2, H3, H4, H5, H6 {color:green}

Finally, define emphasis text as bold 24-point Times:

EM {font-family:Times; font-size:24pt; font-weight:bold}

And that's all there is to setting style sheet properties – we just put whatever property we'd like to set together with the value and associate this combo with a tag.


The <DIV> Tag

We looked at this briefly earlier in the chapter, but it's now time to consider it in more detail. We use <DIV>…</DIV> blocks to create what we'll call a document division. We can think of a division as a separate part of the HTML page – in effect an HTML page in itself, inside a top level page and displayed in the same browser window.

We can have any number of divisions in a page, and each division has its own set of style properties that can be used to position the division anywhere on the page, down to the pixel. Divisions have a z-index property and can overlap each other. Text or other elements inside a division automatically take the font, color, and other settings from their parent division, allowing us to set the formatting properties of objects (like small blocks of text) that might not have their very own set of style properties.

Finally, when using <DIV> with absolute position, it's important to remember that the <DIV> tag starts a new coordinate system that is relative only to the HTML placed within the <DIV> … </DIV> block.

The <DIV> tag is one of the most useful single tags in Dynamic HTML programming and you'll get very familiar with it as you read and use the rest of this book.


Incorporating Styles into a Page

Once we've decided how we want our page to look and know what our style tags will look like, we need to take the final step and apply this information to the page. Just like we can't add script code to the page without enclosing it in a variety of <SCRIPT> tags, we can't add style sheet information without some sort of structure. There are a few different ways to add styles to a page, and they each have their own set of advantages and disadvantages.

The Single <STYLE> Tag

Our first example of style sheets uses probably the most intuitive method of adding style information. With this method, we open a <STYLE> tag, provide all of the formatting information for our page (according to the syntax we just laid out), and then close the tag. Usually, we'll place this <STYLE> block in the header of the HTML document, like we did in the first example.

<STYLE>
<!--
BODY   {font-family: Times;
   font-size: 125;
   font-weight: bold;
   color: red }

.layer   {font-family: Arial;
   font-size: 40;
   font-weight: bold;
   color: blue;
   margin-top: -75px }
-->
</STYLE>

This makes sense, but it does have a few limitations. Most importantly, this technique requires that we hardcode the style information into every single HTML file that needs it. If we need to change one small bit of the formatting for our pages, then we have to manually open, edit, and save each file. Also, in addition to being difficult to change, information repeated in many documents needs to be downloaded repeatedly, and this consumes time and server/network resources.

We also need to remember to enclose our style information inside the HTML comment tags <!-- and --> so that browsers that don't understand style sheets won't attempt to display the information inside of our <STYLE> tag as text.

Using <STYLE> with Individual Tags

For quick and easy addition of style information we can add styles directly to almost all elements on the page.

<H1 STYLE="color:purple; font-size:100">A Big Ugly Heading</H1>

This biggest advantage to this method is its flexibility. Style information can be added to individual elements, and it will only affect the specific instance of the element it's added to. In this example, we'll only change one <H1> tag, not all of the first-level headers. This ability makes it even more useful when we're using just a bit of style information for the sake of scripting.

However, we still haven't done anything about the problem of repeating identical style information across pages. To fix that we'll need to use one of the next two methods.

The <LINK> Tag with Style Sheets

If we have a set of style information that we'd like to apply to multiple pages (or even think we might want to, someday) then we can save this information in a separate file. Adding these styles to a new page is now no more complicated than adding a single <LINK> tag to each page that should incorporate the style information:

<LINK REL=STYLESHEET TYPE="text/css" HREF="http://yourserver/yourpath/style.css">

This tag can be placed in the <HEAD> of the document, in the same location where we've been placing all of the in-line style information.

The HREF attribute should be changed to point at the appropriate style sheet, which should incorporate style information in exactly the same format as we saw when we used the <STYLE> tag – just omit <STYLE> and </STYLE>:

BODY   {font-family: Times;
   font-size: 125;
   font-weight: bold;
   color: red; }

.layer   {font-family: Arial;
   font-size: 40;
   font-weight: bold;
   color: blue;
   margin-top: -75px }

Using @import

There is another way to automatically apply style sheets and still keep the file sizes down, the @import notation. This functions in a very similar way to using LINK and HREF, except that the URL of the style sheet is specified within an @import line.

<HTML>
<HEAD>
<TITLE>CSS Style Example</TITLE>
<STYLE>
@import URL("http://rapid.wrox.co.uk/books/0707/style.css");
</STYLE>
</HEAD>
<BODY>
<CENTER>
Wrox Press
<DIV CLASS=layer>DHTML Programming</DIV>
</CENTER>
</BODY>
</HTML>

This notation tells the browser to get the style sheet style.css from the server at rapid.wrox.co.uk. If we place the @import line between <STYLE> tags in the <HEAD> of our document, the style will be automatically retrieved and applied before our document is displayed. These are the preferred methods of incorporating style sheets using Internet Explorer 4.0: it's easy, and they maintain all of the advantages we gain from using style sheets.


More Style Sheet Features

We've already established how useful style information is to designing HTML pages, and we've talked about a multiplicity of different ways to add style information to a page. We're now going to cover two additional and very useful features of specifying style information.

Simple Inheritance

Tags enclosed in other tags inherit the settings of the outer tags. You might have seen this phenomenon if you've experimented much with style sheets, although you might not have recognized it as such.

For example, suppose we've defined the <H1> style like this:

H1 {font-size:48; font-family:Arial}

In the course of writing the HTML for the page that uses this style we use an <EM> tag inside a <H1> block:

<H1>Here's my <EM>important</EM> heading</H1>

We wouldn't be very happy if the word important reverted to some other style, such as the body text style. However, because embedded tags inherit the style properties of the tags enclosing them, the text inside the <EM> tag is rendered as bold in 48 point Arial – because it's enclosed in an <H1> tag.

It is important to know that the <EM> tag will only inherit the properties of the <H1> tag if it hasn't already had its own style defined. If you think about this approach you'll see that it makes a lot of sense. In effect, the style sheet engine standardizes everything that you do not specifically define. This works well in practice – as you'll see when you start writing HTML pages that use style sheets heavily.

Contextual Selectors

But this isn't all there is to inheritance! Style sheets even give us the ability to specify that certain instances of certain tags should look a specific way. If we've set all <H1> tags to the color green and all <EM> tags to yellow, we can use what is known as a contextual selector (because the selector depends on the context it's used in) to stipulate additional behavior for a given tag.

With the two declarations above, an <EM> tag inside of a <H1> tag would be rendered as yellow. However, with the following simple declaration we can require that all <EM> tags inside <H1> tags be colored purple instead:

H1 EM {color:purple}

This declaration only affects <EM> tags inside <H1> - it leaves every other combination alone.

Classes

Another interesting kind of syntax we're going to cover here is known as a style sheet class. This technique allows us to assign names to modifications of basic styles and then use them in much the same way we would use a normal style.

Suppose we usually would like our first level headings to be displayed in black 24 point Arial type. From our previous discussions we know that we can accomplish this with the style sheet declaration of:

H1 {font-family:Arial; font-size:24pt; color:black}

Now further suppose that occasionally we'd like the <H1> tag to take on a slightly different appearance – maybe we want it to be a larger point size, but in all other respects we'd like it to remain the same. Using a class in our style sheet declarations, we can accomplish this easily:

H1.second {font-size:48pt}:

Now we can use both the original declaration and the subclass in our HTML:

<H1>First heading</H1>
<H1 CLASS="second">First large point heading</H1>

Another way to accomplish this task would be to use an in-line <STYLE> tag for each heading that we wanted to appear red:

<H1 STYLE="font-size:48pt">Another large point heading</H1>

As we'll see shortly when we talk about how properties take precedence over each other, the large point size specified in the heading tag itself will override the other font-size specified in the top-level set of styles.


Conflict Resolution (What does 'Cascading' mean anyway?)

With all of these different ways to specify what elements should appear like, how does the style sheet engine determine which declaration wins if more than one declaration could apply to a given element?

The answer is in the title of the specification we've spent so much time discussing in this chapter: Cascading Style Sheets. In a nutshell, the title indicates an important and special property about style sheets, which is that if more than one style attempts to format an element at the same time the style that actually formats the element will be determined based on a concrete set of rules.

Specifying Your Own Style Sheet

There's one other source of style sheet information that we haven't considered yet. In addition to the different styles we can specify in the source of our page or in a linked style sheet, the browser user can also define their own style sheet. In general the values in this sheet will be overridden by any style values in a page. The rationale behind this practice is that any style information in a page is important enough to display, but pages without style sheets can still be prettied up by following user preferences.

Setting a default style sheet is easy to do with Internet Explorer 4.0. Select the Internet Options menu item from the View menu, and then click on the Accessibility… button. In the Accessibility dialog, check the Format documents using my style sheet checkbox and fill in the path or use the Browse… button to specify the location of your own personal style sheet.

Rendering the Page: Resolving Style Conflicts

When a style sheet enabled browser downloads a page it runs through the following steps to determine which (if any) style information should be used to format each element:

  • Parse the page and consider all elements. If an element (including its parents) contains no style information, then use the user defaults that were set via style sheet or browser options. If parent elements contain style information use this data instead of the default values.

  • If a conflict exists, the values are sorted again so that values set by the author of the page rank higher than default values set by the browser user.

  • If we still have not resolved all conflicts, sort the values by their specificity to the situation. A value that applies only to the situation at hand will override a value that applies in a more general case.

With the information from this section (and the style sheet reference, section F, in the back of the book) you can write HTML pages that look great because they contain specific formatting and layout information.


The Style Object

Before we finish up with the chapter, we're going to talk for just a moment about an element that isn't officially a part of the style sheet specification, but is very important to our use of style sheets in Dynamic HTML.


The Style Object

First, we should explain why you'd use the style object when writing Dynamic HTML script code…

Any HTML page element that can take a style property has a corresponding style object. This object is of no importance while we're writing the page layout and text itself, but plays a vital role when we come to write code. In short, if we want to do anything to a page element dynamically, we'll use the underlying style object to do it. Want to change the color of a list item as the mouse moves over it? Write to the color property of the list item's style object. Curious about the size of the heading elements on the page? Read from each heading's style.fontsize property. Need to move an element after the page has been loaded and displayed? Use the top and left properties of that element's style object. You get the idea, and you'll surely be seeing many examples using the style object throughout the rest of this book.


The Style Object's Interface

The style object has a very long list of properties. Fortunately, they're all easy to categorize as they mimic the different style properties we've discussed in this chapter.

background | backgroundAttachment
backgroundColor | backgroundImage
backgroundRepeat | border
borderBottomColor | borderBottomStyle
borderBottomWidth | borderLeftColor
borderLeftStyle | borderLeftWidth
borderRightColor | borderRightStyle
borderRightWidth | borderTopColor
borderTopStyle | borderTopWidth
clear | color
cssText | display
font | fontFamily
fontSize | fontStyle
fontVariant | fontWeight
height | left
letterSpacing | lineHeight
margin | marginBottom
marginLeft | marginRight
marginTop | overflow
posHeight | posLeft
posTop | posWidth
textAlign | textDecoration
textDecorationLinethrough | textDecorationOverline
textDecorationUnderline | textIndent
textTransform | top
verticalAlign | visibility
width | zIndex

From time to time we see a very slight change as hyphenated properties lose their hyphens when translated into a property of the style object. For example, the style sheet font-size property corresponds to the style object's fontsize property. Other than this difference, the list of properties is identical.

With this discussion of the style property we can revisit the code we wrote earlier in the chapter to make a header disappear and reappear:

<H1 ID="MyHeader" STYLE="visibility:visible; text-align:center">Phantom Header</P>

<SCRIPT LANGUAGE="VBScript">
Sub Document_onKeyPress
   If MyHeader.style.visibility = "hidden" Then
      MyHeader.style.visibility = "visible"
   Else
      MyHeader.style.visibility = "hidden"
   End If
End Sub
</SCRIPT>

You can understand now exactly what we're doing in this code. The header object we created with a normal <H1> tag and gave the ID of MyHeader exposes a style object. Our code uses the visibility property of this style object to hide and show the header.


Summary

In this chapter we talked about one of the most important additions to Internet Explorer which is utilized by Dynamic HTML. While style sheets were present in Internet Explorer 3.0, it's only in IE4 that virtually of the CSS1 standard has been implemented. Style sheets give us the ability to precisely display and position elements on our pages, and are a key to creating dynamic pages with scripting.

Specifically we learned in this chapter that:

  • Style sheets have tremendous importance for Dynamic HTML: they're the mechanism we use to change the appearance of HTML elements dynamically.

  • Several kinds of style information- such as positioning or fonts- can be added to HTML pages in several different ways.

  • The style object makes the different style properties of elements available to script code.

Over the rest of the book you'll be seeing the objects and concepts we talked about in this chapter again and again. As we cover the rest of the fundamentals of dynamic HTML, we'll see how style sheets can be used to connect script to the elements in this page. When tied together with script code and the browser object model, the full functionality of style sheets are revealed. We'll cover this over the next few chapters, but first we need to look at the browser object model.


Chapter 3: HTML Formatting and the Browser Object Model

After our brief introduction to style sheets in the last chapter, we're now ready to dig into one of the main foundations of Dynamic HTML: the browser object model. Netscape 2 was actually the first browser that provided a documented object model, which allowed users to access both the browser environment and page contents with scripting code. However certain contents of the page such as text and images weren't accessible, until the advent of Dynamic HTML. If we are going to be able to program the object model using Dynamic HTML, then we need to understand the whole hierarchy of objects inside the browser first and what they all do.

So we're going break our discussion of the object model into two chapters. In this, the first, we'll discuss exactly what the object model is and why it's so important to Dynamic HTML programmers. From this introduction, we'll move on to the programmatic representation of the browser itself. The actual discussion of how we can manipulate elements on the web page, via the object model, won't be dealt with until the next chapter.

So, we're going to cover these topics, in this chapter:

  • The purpose of the browser object model and its overall organization.

  • Properties, methods, and events of the top-level window object.

  • Window's subsidiary objects: history, location, navigator, screen, and event.

Since we haven't had an opportunity to talk in-depth about writing script code inside HTML pages, we'll refrain from giving detailed code examples in these chapters. The foundation of everything we do with DHTML is the object model; these two chapters provide a basis for everything we'll write about in this book. The fact that we're talking about the object model before we talk about scripting is logical too: it's important to talk about the objects that we can manipulate through code before we talk too much about the script code itself. While we won't explicate page after page of code in these chapters, we'll spend plenty of time talking about what the objects can do.


The Organization of the Browser Object Model

While it is possible to write good Dynamic HTML code without a basic understanding of how the objects provided by the browser interact, knowledge of the object model makes this a lot easier.


What is the Browser Object Model?

In short, the browser object model is the interface between the code we write to control or react to the browser and the internal workings of the browser itself. This level of abstraction allows us to manipulate the browser without worrying about how the instructions we give are carried out. In effect, the guts of the browser become a 'black box' – we perform some action and get a result, but we don't need to know how our input is translated into output by the browser.

However, in many cases we do need to know information about the task itself or, we might need to instruct the browser to perform a task or know when a task has been undertaken or completed. For example, the specific code that the browser uses to decide exactly which pixels are turned on and off is of little concern to us. However, we'll often find other information extremely useful. For instance, we might want to instruct the browser to navigate to a list of different page based on an entry in a form on the current page. We might also want to respond to the loading of a new page, for example, perhaps we'd like to display a dialog that grabs the user's attention the first time they've seen our page. The browser object model gives us the ability to write code that will accomplish all of these tasks.


Objects

The object model provides us with an interface to the browser that masks us from what actually goes on beneath the hood of the browser, such as how browser operations are accomplished. This interface is embodied in the set of objects that make up the object model. But what actually is an object in computer terms?

While an object can be anything from an apple to a television in the real world, to keep things simple, in computer terms, we shall just consider an object to be a combination of data and code. Objects are self-contained parts of a program, which carry out specific functions. The data and code associated to the object define everything that the object represents (state) and what it can do (behavior). The characteristics or blueprint of an object is specified by a class definition. An object, of which there may be many, is a created instance of a class, just as an instance of a cake can be created from a recipe. IE4 is made up from many interrelated objects and indeed even the whole browser itself is considered an object.

An Object's Interface

All object models, including the browser object model, are made up of a set of interconnected and interrelated objects. Fortunately for us, while these objects might represent anything, they all operate in the same way to a script developer. It's this similarity that makes objects so powerful. Our script code can access every object with similar syntax, without regard for what the object represents. Furthermore, code can only interact with objects in predefined ways – through properties, methods, and events. A standard way of communicating is a key concept in any set of objects.

Properties

A property of an object is an attribute that somehow reflects the current state of the object. As an example, suppose we were working with a Car object. This object might have properties with names like NumberOfDoors, BodyStyle, CurrentSpeed, and LightsOn. Some propertiescan only be read – they can't be changed. Others can be read and written. Many of the Car object properties are read-only; that is, they were set when the car was built and can't be changed (at least without a lot of work!). Other properties, like CurrentSpeed and LightsOn, can be changed or retrieved; these properties are read/write properties.

While we've said many times that we won't provide any more than the most rudimentary code in these chapters, it's important to understand on the simplest scale how properties are called. Assuming we had an instance of our object called MyCarObject, we could display the read-only properties we invented in the previous paragraph in VBScript like this:

MsgBox MyCarObject.NumberOfDoors
MsgBox MyCarObject.BodyStyle

Setting read/write properties is equally as simple:

MyCarObject.CurrentSpeed = 55
MyCarObject.LightsOn = True

We can read these variables in the same manner that we access read-only variables:

MsgBox MyCarObject.CurrentSpeed
MsgBox MyCarObject.LightsOn

Methods

Properties are great if we're only interested in finding out what state an object is in or in changing a discrete characteristic of an object.. Fortunately we have methods to help us out of this situation. In short, objects provide methods so that they can be instructed to carry out certain predefined operations. Our Car object could have methods like SpeedUp, SlowDown, TurnRight, TurnLeft, and Stop. These methods could take any number of parameters (perhaps the miles per hour to change speed or the degrees to turn). We use methods to influence the current state of the object.

Calling methods with VBScript is simple:

MyCarObject.SpeedUp 20  'speed up 20mph
MyCarObject.TurnLeft 90 'turn left 90 degrees

Events

Both properties and methods are things that we, as an outside force, do to an object. Events, in contrast, are things that an object does to its container or host. More specifically, they're entities that allow us to respond to some condition or state inside an object. A Car object might have, among other things, a FuelLow event that would be raised when the fuel level in the car fell below some level, a FuelEmpty event raised when the fuel tank was empty, and a Collision event – which we hope we never see!

There are a few different ways to connect code to events with VBScript. In our simple examples we'll use the same style that Visual Basic uses:

Sub MyCarObject_FuelLow
   ...      'switch the fuel-low light on
End Sub

Sub MyCarObject_Collision
   ...      'call the insurance company
End Sub

Collections

The final object concept we need to understand before digging into the object model is the idea of a collection – an easy way to group related objects together so they can be on as a unit. A car rental agency might keep a few collections of Car objects around. These collections might be called names like AvailableCars, RentedCars, and WreckedCars (these cars would probably have seen at least one Collision event). Collections are objects in their own right, and they have their own properties to show the status of the collection (like Length – the number of elements in the collection). They can also have their own methods and events, which will operate on the collection as a whole.


The Object Model in Action

Having looked at the basic objects, we need to know more about what exactly the browser object model enables us, as developers, to accomplish. What kind of access does it provide to the operation of the browser or to the HTML pages currently or previously displayed? What can you do with the object model to create new and exciting web pages and web applications? To discover the answer to these questions, we need to talk about the objects in the object model.

The Object Model Diagram

This diagram is intended to give you a feel for each object's place in the object model. There are a lot of different objects in the diagram, but don't worry as we'll be spending most of this and the next chapter going over them all so you can understand their use throughout the rest of the book and apply them in your own code.

The object model shows the 'hierarchical' relationships between different objects. The connections between the objects in model are arranged in a hierarchy because one object can often contain one (or more) of another object, rather like a parent having one or more children. There are also a number of important objects (like the style object that we talked about in the last chapter) that aren't an intrinsic part of the model at a high level, but are very important when we get into actually working with objects that the model provides.

As you can see, the top-level object is the window object. This is the parent object of the entire model – we can think of all the other objects as being children of window. This object contains the focus of the next chapter, the document object – think of it in the same way as the browser window contains the HTML document.

The window object also contains a collection called frames. Each object in the frames collections represents a frame currently displayed in the browser. In addition to document and frames, the window object also exposes (makes available to the programmer) five separate objects: history, navigator, location, event, and screen. We'll talk more about these objects in this chapter and leave the rest of the document object's descendants (like the numerous collections) until the next chapter when we talk more about the document object.

While we've made the point (over and over again!) that the window object is the top-level object in the hierarchy, we need to modify our thinking just a bit when we work with frames. As we'll see in more detail a bit later in the chapter, each frame contains its own window object, with its own set of objects, properties, methods, and events.

Changes from the Internet Explorer 3.0 Object Model

If you have some experience with the Internet Explorer 3.0 object model you'll see many similarities between this diagram and the one you might be familiar with for Internet Explorer 3.0. You'll also notice a few differences. The basic layout of the object model hasn't changed, however, you'll see a lot of new objects, like event and screen, as well as a large number of new document collections. We'll cover each of these objects in turn in this and the next chapter.


The Wrox Dynamic HTML Reference Database

As you can start to appreciate when you begin to digest the diagram on the previous page, the Dynamic HTML object model encompasses nearly every part of the browser and displayed HTML document. This makes logical sense: we know that we can use Dynamic HTML to modify almost every part of the browser and the page and that the object model is the sole conduit between our code and the browser. From this follows the reasonable conclusion that the object model must expose all of this extensive functionality through its objects and collections.

The pages that follow explain what each object can accomplish and how the objects relate to each other. We talk about the most important properties, methods, and events. However, to save you from paging through endless amounts of material while you're trying to read and understand, we've placed the extensive reference section with descriptions of every interface element at the end of this book in Appendix D.

The same information, including any updates are available in a snazzy and easy to use Access 97 format database on the Wrox web site at http://rapid.wrox.co.uk.


The Window Object

The window object represents the browser itself. Since we start IE4 to view HTML documents and perform any other task using the browser it makes sense that the window object's properties, methods, and events concern themselves with the state and operations of the browser.

The window object has properties, methods, and events of its own as well as subsidiary objects. We'll cover the intrinsic interface elements below before moving onto the objects.


Properties

The following properties are available for the window object:

Property | Description
clientInformation | A reference that returns the navigator object for the browser
closed | A true or false value that indicates whether a window is closed
defaultStatus | The text that is displayed in the browser's status bar by default
dialogArguments | Returns arguments that were passed into a dialog window, as an array
dialogHeight | Sets or returns the height of a dialog window
dialogLeft | Sets or returns the x coordinate of a dialog window
dialogTop | Sets or returns the y coordinate of a dialog window
dialogWidth | Sets or returns the width of a dialog window
document | Read-only reference to the window.document object
event | Read-only reference to the global event object
history | Read-only reference to the window.history object
length | Returns the number of elements in a collection
name | The name of the window object
navigator | Read-only reference to the window.navigator object
offScreenBuffering | Specifies whether or not to use off-screen buffering for the document
opener | A reference to the window object that opened this window object
parent | A reference to the parent of the current window object (for use with frames)
returnValue | Allows a return value to be specified for the event or dialog window
screen | Read-only reference to the global screen object
self | A reference to the current window
status | Text displayed in the browser's status bar
top | A reference to the top-most window object (for use with frames)

When using property, method or event names in JavaScript, you must be very careful, as JavaScript is case sensitive, so while defaultStatus would be a correct reference, defaultstatus would return an error. In VBScript however, either version would be acceptable!

The opener, parent, self, and top properties are useful when we write code that deals with more than one window object. As we'll discuss when we talk about the frames collection below, multiple window objects can come about when the page we're displaying has multiple frames and when we've explicitly created new windows with the open method (which we'll also discuss shortly).

The dialogArguments, dialogHeight, dialogLeft, dialogTop and dialogWidth properties can all be used to set the various properties of a dialog window. We take a look at dialogs later in this chapter.

The other two useful properties are defaultStatus and status. Both of these properties control what is displayed in the status bar of the browser, when the page is first displayed and afterwards respectively.


Methods

While the properties of the window object are useful, in contrast to what you might see for many other objects, they aren't as numerous or wide-ranging as the window's methods:

Method | Description
alert | Displays an Alert-style dialog with a message and an OK button
blur | Causes the browser window to lose the focus and be placed at the back of the list of all non-minimized windows
close | Closes the current browser window
confirm | Displays a Confirm-style dialog with a message and OK and Cancel buttons
focus | Causes the window to gain focus (useful when used with references to other browser windows that don't have the focus)
navigate | Causes the browser window to navigate to the URL specified after the method name
open | Opens a new browser window using the options specified
prompt | Displays a prompt-style dialog box with an input text box and OK and Cancel buttons
scroll | Scrolls the window to the x and y position specified after the method name
setTimeout | Sets a timeout to execute a specific piece of code after a specified time interval
clearTimeout | Turns off a timeout that was set with the setTimeout method
setInterval | Sets an block of code that will execute repeatedly every x milliseconds
clearInterval | Turns off an interval that was set with the setInterval method
execScript | Executes a script (default language is JavaScript, not VBScript)
showHelp | Displays an HTML help window as a dialog
showModalDialog | Displays a new HTML window modally

Dialogs

The window object provides three methods that can be used to display dialogs commonly used for simple messages, confirmations, or prompts. The alert method displays a dialog with a text message and a single OK button; use it to give the user a message. Confirm is identical to alert but the dialog includes an additional Cancel button. Finally, the prompt method displays a dialog with a single line text box that can be used for input. These events are most useful when used with a scripting language (like JavaScript) that doesn't include basic dialogs in the language. VBScript provides more powerful equivalent statements that can be used instead of these methods.

The code next to each dialog below is all that is necessary to generate the displayed dialog.

Note that while alert, confirm, and prompt are all methods of the window object, we haven't prefaced our calls with a 'window.' We can do this because window is the default object for any script code. If we don't give the name of an object, the script interpreter will assume that we're calling a method or accessing a property of the window object.

Please place the screen shots to the left or right of the code that generates them

alert "This is an alert dialog"

confirm "This is a confirm dialog"

{bmc07070305.bmp}

strReturnValue = prompt "This is a prompt dialog", "Default Input"

We'll usually use the prompt method when we care about what the user chooses. In our example above we've saved the text entered by the user in the string variable called strReturnValue. We can then use this variable to change the way the rest of our script code executes.

New Windows (Open, ShowModalDialog, and ShowHelp)

One of the most powerful methods in the Internet Explorer 3.0 object model was open, allowing new instances of Internet Explorer to be opened at will with a highly customizable list of features. Fortunately for us as developers, IE4 expands even further on this ability with more configuration options and the new showModalDialog method.

Open

The open method is called with this syntax:

Set objMyWindowRef = window.open (URL, name, features)

In this line of code, all three parameters are strings. URL is the only required argument – it specifies the URL of the document to be displayed in the new window. Name can be used to name the window, although this isn't as much use to us as the reference to the newly created window object that the open method returns to us. If we specify the name of a window that already exists, open will bring this window to the foreground instead of creating a new window. In our example line of code we're saving this reference in a variable called objMyWindowRef. Since this variable is a reference to another window object, any of the properties or methods we've talked about in this chapter can be used with this reference in the same way that we've used them with the window object where our code is executed.

The last parameter, features, is what gives us the fine level of control over the appearance of the new window. The following table shows the attributes that can be set with the features parameter.

Attributes | Values | Description
channelmode | yes | no | 1 | 0 | Show channel controls
directories | yes | no | 1 | 0 | Show directory buttons
fullscreen | yes | no | 1 | 0 | Maximize new window or leave default size
height | number | Height of new window in pixels
left | number | Left position on desktop (in pixels)
location | yes | no | 1 | 0 | Show the URL address text box
menubar | yes | no | 1 | 0 | Show the default browser menus
resizeable | yes | no | 1 | 0 | Window can or cannot be resized by user
scrollbars | yes | no | 1 | 0 | Show vertical and horizontal scrollbars
status | yes | no | 1 | 0 | Show the status bar at the bottom of the window
toolbar | yes | no | 1 | 0 | Show the default browser toolbars
top | number | Desktop position (in pixels) of the new window
width | number | Width of the new window in pixels

The features string is formed by concatenating the values we would like to set with delimiting commas. Here's an example that opens the Wrox site and saves the window reference in a variable called objWinRef:

set objWinRef = window.open ("http://www.wrox.com", "MyWindow", "toolbar=no, left=150, top=200, menubar=no, systemMenu=no")

The open method returns a reference to the window that it opens. What does this mean? If we save the reference in a variable, like we do in the above example where it's saved in the variable called objWinRef, then we can control the window after it has been opened. To do this we use the same window object methods we've been talking about in this chapter. For example, to close the window in the same block of code that opened it, we would simply use this line of code:

objWinRef.close

Any of the other window methods and properties can also be used.

ShowModalDialog

The showModalDialog method is similar to open but with a few differences.It takes a feature string built the same way we build an open feature string, except that some of the options are different with a modal dialog.

Attributes | Values | Description
border | thick | thin | Size of border around window
center | yes | no | 1 | 0 | Center the dialog window with respect to the browser window that opens it
dialogHeight | number & units | Height of the dialog window in style sheet units specified
dialogLeft | number & units | Left position of the dialog window with respect to the desktop (in style sheet units specified)
dialogTop | number & units | Top position of the dialog window with respect to the desktop (in style sheet units specified)
dialogWidth | number & units | Width of the dialog window in style sheet units specified
font | CSS string | Default font and style for the dialog
font-family | CSS string | Default font for the dialog
font-size | CSS string | Default font size for the dialog
font-style | CSS string | Default font style for the dialog
font-variant | CSS string | Default font variant for the dialog
font-weight | CSS string | Default font weight for the dialog
help | yes | no | 1 | 0 | Display the help button in the dialog title bar
maximize | yes | no | 1 | 0 | Display the Maximize button in the title bar
minimize | yes | no | 1 | 0 | Display the Minimize button in the title bar

Since we can now use CSS font attributes in our feature string, we have to format the string a bit differently. Instead of using equal signs and commas like we did with the open method, we'll create our features string in the same style as we create CSS formatting, using colons and semicolons.

Compare these strings:

strOpenFeatures = "toolbar=no, left=150, top=200, menubar=no, systemMenu=no"

strModalFeatures = "font-size:10;font-family:Times;dialogHeight:200px"

Since we're using CSS formatting, we need to remember to specify the unit for the size and position attributes. This is why we said dialogHeight:200px in the above example.

The method displays a new browser window, but makes this window modal to the browser it is shown from. This means that the user won't be able to switch back to work in the original browser until the modal browser instance is dismissed. It also means that any script code in the original browser stops execution while a modal dialog is displayed.

Modal dialogs look different to windows created with open. As we can see in the screen shot below, there is a lack of a system menu icon and minimize/maximize buttons of the type we'd see with an open-created window. How do we show a modal dialog? The next line of code creates the dialog shown in the screen shot. The first argument is simply the filename of the page to display. We've seen the third argument before also, when we worked with the open method (but remember that the table above shows the differences in what we can use with non-modal and modal windows). We'll talk about the second parameter in this call in just a few seconds.

DialogRef = showModalDialog ("blank.htm", 0, "toolbar=no")

This method can be very useful. Suppose we'd like to prompt our users for some information, but find the basic dialog we get with the prompt method too limiting. To display a custom prompt from our script and not take any action until the viewer of our page has responded to the prompt, we can create an HTML page that implements the prompt and then display the page using the showModalDialog method. For example, suppose we'd like to ask the user to choose between three different options. We would first create an HTML page that showed these options, perhaps using a set of radio buttons, and then we would show this page with the showModalDialog method. Our code would stop while the dialog was displayed, and when our code continued we could use the value that the user selected to determine how to proceed.

Since we now have the same ability to choose specific positions (using style sheets) as we do with environments like Visual Basic, we can use prompts and other specific forms in the same way that we use forms with Visual Basic and other rapid-application development tools.

As we saw above, the showModalDialog method takes a second parameter called arguments instead of name. We'll often want a way to pass information to the dialog, and to do this we can use the arguments string. Inside the code that may reside in the dialog's HTML page (remember, the page is created with HTML like any other we're using) we can access the arguments string and take appropriate action. For example, suppose we create an HTML page that contains a set of radio buttons, but we want to display different information in the text of the radio buttons each time the dialog is shown. If we wrote the code for our dialog so that it took the text from the arguments string, then changing the radio buttons would be a simple matter of changing the call to showModalDialog.

Finally, we've said that showModalDialog doesn't return a reference to a window like open does. Why? First of all, a reference wouldn't be much use to us, since, by definition, the code that creates a modal dialog stops until the dialog is dismissed. A reference to a non-existing window wouldn't do us any good! Secondly, since modal dialogs will often be used to create custom prompts, we want a way to return information from the dialog to the calling code, like the prompt dialog does.

For these reasons showModalDialog returns a string that can be set in the code that resides on the calling page.

ShowHelp

Online help is a necessity in today's computer applications. Since DHTML gives web authors the foundation they need to write full-featured applications with all the features and perks a native Windows application can provide, it's not surprising to see efforts to make it possible to provide online help in a web environment also. Microsoft has created a standard called HTML Help that allows help information to be displayed in a browser window that looks similar to the Help Topics window that Windows applications like Word and Excel display today.

IE4 is, in fact, one of the first HTML Help hosts. To see it, load up IE and click on the Content and Index menu item in the Help menu.

If you right click on the right-hand pane of the new dialog you'll see the familiar View Source option in the menu that proves that what you're seeing is indeed an HTML page.

So how does the showHelp method work anyway? It takes the URL of an HTML Help page (see the last paragraph in this section for more information on how to do this), as well as a second optional parameter where arguments that control how the page is displayed can be specified.

For example, if we have an HTML Help page called MyHelp.htm, then we'd show it with this simple line of code:

showHelp "MyHelp.htm"

Timers

Using the setTimeout, clearTimeout, setInterval, and clearInterval methods of the window object we can automatically execute any code we've written after some time interval (that we set) has elapsed.

SetTimeout and ClearTimeout

setTimeout takes the name of a function, and a time value in milliseconds. After the time value has passed, the function is called automatically.

For example, the following code calls a routine named TimerFunc after 5000 milliseconds (5 seconds):

TimeoutID = Window.setTimeout ("TimerFunc",5000)

Once you've started a timer with setTimout, you may find that you want to cancel it so the function specified in the setTimeout call is not executed. This is where the clearTimeout function comes into play, assuming you've saved the return value of the setTimeout function, a number. In the line of code above we've saved our return value in a variable called ID, and it's this variable that we'll use in our call to clear the timer:

Window.clearTimeout TimeoutID

If you call clearTimeout with an ID value that doesn't exist, nothing will happen and any timers you have active will continue to work.

SetInterval and ClearInterval

The setInterval and clearInterval methods are new to IE4. They're called just like setTimeout and clearTimeout; the only difference is that the function specified in the call executes repeatedly, instead of just one time, until the page is unloaded or the clearInterval method is called.

The following line of code starts the process of calling TimerFunc every 1 second.

IntervalID = setInterval("TimerFunc", 1000, VBScript)

To stop the repeated calling of this function, we'd just use this line of code:

clearInterval IntervalID

Window Control (focus, blur, scroll)

If we have a reference to a window, we can use the focus, blur, and scroll methods to control the instance of the browser. Firing the blur method causes the browser in question to lose the focus and be placed at the back of any non-minimized windows; the window that was immediately behind the browser before it lost focus is made the active window. After we've blurred a window, we can bring it back to the forefront by using the focus method.

The scroll method should be called like this:

windowref.scroll x, y

where x and y are pixel values that the top left corner of the document should be scrolled to.

The top left hand of the document is defined to be point (0, 0), so:

scroll 0, 0

tells the browser to scroll the current document to the position it was displayed at originally. This line of code:

scroll 50, 50

scrolls the document to a position 50 pixels down the page and 50 pixels to the right. We'll use this method to do things like ensuring that a certain portion of the page is displayed in the viewer's copy of IE4.

Navigate

We've covered more than a several methods over the last couple of pages. Fortunately, the final window method we'll cover is much simpler. Navigate is powerful – it's used to load a different page into the current browser window. Calling it is simply a matter of specifying the method name and a URL string as the only parameter, like this:

navigate "http://www.wrox.com"

Using just a few lines of code with the prompt and navigate methods we can point a user's browser to a completely different site based their response to a simple question:

W3Cdhtml = prompt("Do you like IE4 - enter yes or no", "yes")
If W3Cdhtml = "yes" then
   navigate "http://www.microsoft.com"
Else
   navigate "http://www.netscape.com"
End If

Using the navigate method is the same thing as setting the href property of the location object, as we'll see in a bit when we cover window's child objects.


Events

The window object contains the HTML document and has top-level events that make sense for this purpose:

Event | Description
onblur | Fired when the window object loses the focus
onfocus | Fired when the window object gains the focus
onhelp | Fired when the user presses the F1 or Help key
onbeforeunload | Fired before the browser window is closed
onresize | Fired when the browser window is resized
onload | Fired when the HTML text for the page has finished loading
onunload | Fired just before the page is unloaded
onscroll | Fired when the user scrolls a page or element
onerror | Fired when an error occurs loading a document or image

OnLoad, OnUnload, onBeforeUnload

These events are very useful for executing any code that needs to coincide with the loading or unloading of the browser. onLoad is fired when the HTML for the current page has been downloaded, but may fire before all images, controls, or applets have finished loading.

The onUnload event is not only fired before the browser navigates to a new page, but also immediately before the browser is shut down (which is understandable, because the page is indeed being unloaded in preparation for application shutdown).

Finally, onBeforeUnload, fires before the page is unloaded. This event is only used during data binding and it allows you to validate entries in a field, before they're sent to a server, or pop a message box up. Its uses are discussed in the data binding section of the book in chapter 8.

OnFocus and OnBlur

When the user of the browser switches to another application or instance of IE4, onBlur is fired. Any code placed in this event will be executed before the switch to the new window is made. The converse of onBlur is onFocus. This event is fired in the current browser when the user switches back to the browser window from another application. While onFocus and onBlur existed in the Internet Explorer 3.0 object model for some form elements, their appearance in the object definition for the window object didn't happen until IE4.

OnResize

This event is rather simple compared to some of the other events we've just finished considered. When the browser window is resized, this event is fired.

OnHelp

If a user presses F1 (or another key that happens to be mapped to online help), the window's onHelp event is fired. This gives the developer control over what will happen if a person browsing their page requests help. The code for this event could redirect the user to one or more help pages on the same or another server on the internet/intranet. The new page can be displayed in a new window, in a modal dialog, or in the same browser window by using the open, showModalWindow, or navigate methods of the window object, respectively.

OnScroll

If not all of the display is visible to the user, and the user has to scroll the display to view it, in its entirety then an onscroll event is generated.

OnError

This is perhaps one of the most useful events. It can be used if an error occurs during the download of a page or image. It can be used to determine whether an error occurred to the connection or whether the download was actually aborted. The user can allow for this eventuality within script and decide whether to get the script to try and download the information again, or display an appropriate message and take corrective action.


The frames Collection

The last major element of the window object, aside from its descendant objects, is the frames collection. This collection exists to give a developer access to all of the frames in any frameset currently displayed in the browser.

The frames collection is indexed from 0 so that a three-frame frameset, like the one that follows, has three elements in the frames collection, numbered from 0 to 2. It's important to note that this three-element collection is only seen from the top-level window – more about this in a second.

Each frame is a window object itself, and each frame reference returns a reference to a window object. All we need is a reference to a window object to enable our use of the properties and methods we've spent the last few pages discussing. It shouldn't be surprising then that the frames collection allows us to control each frame in a frameset as if we're controlling a separate window. In fact, this is exactly what we're doing. Each element in the frames collection is a reference to a new window object.

Accessing Collection Members by Index and Name

For example, this code changes the URL of the bottom left-hand frame in the frameset above:

window.frames(1).navigate "http://bossman"

In the above example we're referencing the frame object we want 'by index' which means we're using a number that refers to the frame. We can also access members of the frames collection (and other collections) 'by name' if we've specified an ID or name attribute when we created the element of the collection. For example, if the frameset with an index of 1 was named leftHandFrame, we could also access it with this code:

window.frames("leftHandFrame").navigate "http://bossman.com"

These naming methods work identically – they both return a reference to the same frame.

As we'll see when we talk about scripting in chapter six, we also have the For Each construct available to us for use with collections. We use For Each to iterate through each element in a collection.

The Frame as a Window Object

Now back to that cryptic statement about the top-level window that I made a second ago… The only other confusing aspect about the frames collection also arises from the fact that each frame is a window object in its own right. Think about the world from the point of view of the frame in our example indexed with the number 1. It doesn't contain any additional frames (although it could – its page would need to contain a <FRAMESET> tag with one or more <FRAME> tags). No contained frames means that the frames collection of the frame(1) should be empty, and this is exactly what we see if we query the length property of the collection in code. Its own frames collection has zero elements, but the page containing it owns three frames.

So how do we access another frame from our code in frame(1)? Remember way back to the top and parent properties of the window object? These properties are the key to working with multiple frames in the browser object model. Before we can reference frame(0) or frame(2) from frame(1) we need to get a reference to the top-level window object so we can use its frames collection. This task is just as simple as using the code from frame(1):

parent.frames(0).method
parent.frames(2).method

In this case, since we're only moving up a single level, parent and top return a reference to the same window object.If we were to work with a set of nested frames, we'd see that top always returns a reference to the top-most window object, while parent just returns a reference to the window object that is immediately above itself.

Generally we'll stick to using parent instead of top to avoid problems when our pages are viewed from another site. If a user browses to our framed pages from another site that uses frames then top will return a reference to the top-level frame from the first site. In this instance, assuming that that top and parent are identical breaks our code.


Children of the Window Object

Despite the fact that we've spent some time on the intrinsic window elements, we still have a lot of browser functionality that we haven't covered yet. Instead of adding numerous new methods, properties, and events to the window object, the designers of the object model decided to group pieces of related functionality together into new objects and to make these objects children of the window object. As we'll see in a moment, these objects represent things like history list of the browser, the location currently shown, and information specific to the browser like its version and display properties. We'll cover each of these subsidiary, but still important, objects over the next few pages.


The Location Object

When we learned that the navigate method of the window object could be used to display a new URL, we alluded to a child object, called location.

The location object represents the current URL displayed by the browser. As you might expect, location's properties give information about the URL and its methods allow us to modify the browser's current location, but in a bit different way than we do with navigate.

Properties

The properties of location reflect every last thing we could ever hope to know about the link the browser is currently displaying. The following table describes each property and gives the value that the property would return for a URL of http://www.wrox.com:80/dhtml/book.htm?abc.

Property | Value | Description
href | http://www.wrox.com:80/dhtml/book.htm?abc | Complete URL
protocol | http: | The protocol of the URL
host | www.wrox.com:80 | Hostname and port number
hostname | www.wrox.com | Name of host
port | 80 | Port number (default is 80)
pathname | /dhtml/book.htm | Path after the host property
search | ?abc | Any value after a '?' in the URL
hash | (nothing in this example – would contain any anchor specified with #) | Any value after a '#' in the URL

In our example we haven't specified that we want to jump to any bookmark that might exist in the document. To jump to the anchor named two, we'd use an address similar to this:

http://myserver/location.htm#two

In this case the hash property of the location object would contain the string '#two'.

If you look at the values for these properties a little closer, you'll see that the href property includes the information in all of the other properties. The browser parses the full href string and fills in the appropriate information for each property, saving us the trouble of interpreting the full string ourselves and making it easier to find any specific piece of information we might need without having to write explicit script code.

The other cool thing about the href property is how it's dynamically tied to the browser. Read the href property and nothing special will happen; change the href property and the browser will display the new page. Changing the browser's current URL in this manner is exactly the same as using the window.navigate method.

Methods

In the old Internet Explorer 3.0 object model, the location object had no methods.This has changed with IE4. We now have three methods that give us a bit more control over the browser's current location.

Method | Description
assign | Loads the page specified, equivalent to changing the window.location.href property
reload | Reloads the current page and displays any changes to the HTML source
replace | Navigates to the URL passed as a parameter and replaces the current page in the history list with this new page

With assign we can navigate to another page by passing the URL of the page when we call the method, like this:

location.assign("http://www.microsoft.com")

Using the reload method we can refresh the contents of a page we're viewing. While Internet Explorer 3.0 didn't have this method, we could achieve the same affect using the go method of the history object with a parameter of 0. This was an often-requested feature, and so it was given its own method in IE4.

Replace, on the other hand, is a completely new method that enables functionality we didn't have at all with Internet Explorer 3.0. The immediate effect is no different to the user than changing the href property or using the navigate method of the window object: the page the browser is displaying changes. The difference lies in what is added (or in this case not added) to the history list of the browser. When we change pages in a conventional manner, each new page is added to the end of Internet Explorer's history list, forming a queue of visited pages. In contrast, if we use replace to change our location, the current URL is overwritten by the new URL and won't remain in the history list.


The History Object

The history object contains information about all of the different URLs that a client has visited, stored in the history list of the browser.

Method | Description
back | Takes an integer and moves back this many places in the history list
forward | Takes an integer and moves forward this many places in the history list
go | Takes a string that represents all or part of a URL and attempts to move to this URL in the history list (also takes a positive or negative integer)

The forward and back methods work identically. Each method takes a positive integer and moves the specified number of places forward or back in the history list. The go method works similarly if the argument you pass is an integer. In this case a negative integer will move backwards and a positive integer forwards. With IE4 you can also pass the go method a string representing all or part of a URL. If this URL exists in the history list, the browser will display the page.

History's only property, length, shows the number of elements in a collection.

If you attempt to move past either the beginning or end of the history list with any of these methods, the browser won't raise an error. Instead you'll just be left at the page at the beginning or end of the history list

The history object can be used to create a very useful part of most web pages: a back button or link that doesn't require a hard-coded URL. Instead, we can just execute either of the following lines of code whenever the link or button is pressed:

   history.back 1

or:

   history.go –1

Both lines of code use the history object to move to the previous page in the history list.


The Navigator Object

Using the navigator object we can retrieve information about the capabilities and nature of the browser that is executing our code. If we'd like to execute different code for different browsers, this is the object for us.

Properties

Instead of describing what each of the properties do, it's easier to use some and demonstrate them by querying some of the recent release of Microsoft and Netscape browsers using this JavaScript code:

alert(navigator.appName);
alert(navigator.appVersion);
alert(navigator.appCodeName);
alert(navigator.userAgent);
alert(navigator.cookieEnabled);

The following table shows the values that IE4 returned:

Property | Value
appName | Microsoft Internet Explorer
appVersion | 4.0 (compatible; MSIE 4.0; Windows NT)
appCodeName | Mozilla
userAgent | Mozilla/4.0 (compatible; MSIE 4.0; Windows NT)
cookieEnabled | True

For comparison here's the Navigator 4.02 values:

Property | Value
appName | Netscape
appVersion | 4.02 [en] (WinNT; I; Nav)
appCodeName | Mozilla
userAgent | Mozilla/4.02 [en] (WinNT; I; Nav)
cookieEnabled | undefined

The values for Navigator 3.01:

Property | Value
appName | Netscape
appVersion | 3.01 (WinNT; I)
appCodeName | Mozilla
userAgent | Mozilla/3.01 (WinNT; I)
cookieEnabled | True

And also the values for Internet Explorer 3.01:

Property | Value
appName | Microsoft Internet Explorer
appVersion | 2.0 (compatible; MSIE 3.01; Windows NT)
appCodeName | Mozilla
userAgent | Mozilla/2.0 (compatible; MSIE 3.01; Windows NT)
cookieEnabled | True

The appName and userAgent properties (and their derivatives appVersion and appCodeName) reflect the browser that is executing the script code.

In case you haven't heard the explanation behind the Mozilla code name yet… it goes like this: Marc Andreesen who helped to develop the Mosaic browser while in college, once he left, founded Netscape, and started work on a browser that should 'beat its chest and smash Mosaic.' Mosaic + Godzilla = Mozilla. Get it?

Methods

There are two methods of the navigator object.

Methods | Description
javaEnabled | Returns true or false depending on whether a Java VM is installed or not
taintEnabled | Returns false, included for compatibility with Netscape Navigator – the title seems to be Microsoft's little joke!

Collections

There are two collections of the navigator object as well.

Collections | Description
plugins | An alias for the collection of all the <EMBED>ded objects in the page
mimeTypes | Collection of all the document and file types supported by the browser


The Screen Object

If you're curious about the capabilities of the display your code is executing on or if you want to change the display of your page depending on the pixel and color resolution the browser is running at then the screen object provides you with the capability to accomplish these tasks.

Property | Value
width | returns the width of the user's display, in pixels
height | returns the height of the user's display, in pixels
updateInterval | sets or returns the interval between screen updates
bufferDepth | returns or changes the color depth of the off-screen bitmap buffer
colorDepth | returns the currently supported color depth for the browser

height and width simply return the height and width of the screen. These are useful when creating new browser windows as they can be used to help decide where the new window should go. You can also use them to change the size of the existing display.

The colorDepth property tells us how many bits per pixel the browser currently has available for displaying different colors. With one bit per pixel we can display 21 = 2 different colors, with 4 bits per pixel we can display 24 = 16 different colors, and so on. Most displays today have at least 256 colors per pixel available and some may have over 16 million different colors. One immediate use for this property is in determining how many different colors to use on our page or in graphics that we might display. If a display can only handle 16 colors, and we have a 16 color and 16 million color JPEG to choose from the colorDepth property can allow us to give the high resolution image only to the person with the high resolution monitor. To do this in code we'd just need to change the URL of the <IMG> tag to point to the correct graphic.

In this case, our code would look something like this:

<SCRIPT LANGUAGE="VBScript">
If screen.colorDepth > 4 Then
   document.writeln "<IMG SRC=""manycolor.jpg"">"
Else
   document.writeln "<IMG SRC=""fewcolor.jpg"">"
End If
</SCRIPT>

This code block tests the value of the colorDepth property and then outputs a string to the browser that either references the manycolor.jpg file if the display is capable of more than 24 or 16 colors or the fewcolor.jpg file otherwise. The next chapter talks more about the document.write method that we're using in this example to create HTML as the page is loaded.

Using the read/write bufferDepth property, we can instruct the browser to buffer all input off-screen before display with whatever color depth we desire – it's as simple as setting screen.bufferDepth to the number of pixels you'd like. bufferDepth can handle values of 0 (the default), 1, 4, 8, 15, 16, 24, and 32, as well as -1 (indicating that buffering should always be performed at the screen depth). Yes, this means that if you've always wanted to know what your pages looked like with two colors you now have your chance! When the bufferDepth property is set to the default of 0, the browser may buffer the input, but it isn't required to do so.


The Event Object

The event object is a subsidiary object of window, but we won't spend too much time covering it here. Instead, we'll wait for chapter 5 when we talk more about scripting, as this is when the properties will come in to use – in conjunction with the events that have been triggered by the browser and identified in the scripting code.

The objects we've talked about so far generally have reflected concrete parts of the window or browser. Many of them can't even be changed, and they're all around at all times for querying if not modifying. The event object, in contrast, is a much more ephemeral beast. It furnishes specific information about the event last fired by the browser and can and will be very different from one instant to the next. The different properties of event tell things like the state of the Ctrl, Alt, and Shift keys when the event was first fired, whether or not any mouse buttons were pressed, and so on.

Property | Value
altKey | The state of the Alt key
button | The mouse button(s), if any, that was (were) pressed to fire the event
cancelBubble | Stops the current event from bubbling up the hierarchy
clientX | The x coordinate of an element, excluding borders, margins, scrollbars etc
clientY | The y coordinate of an element, excluding borders, margins, scrollbars etc
ctrlKey | The state of the Ctrl key
fromElement | Specifies the element being moved from (for onmouseover and onmouseout events)
keyCode | ASCII code of the key being pressed (if a key has been pressed)
offsetX | the x coordinate of the mouse pointer when an event occurs, relative to the containing element
offsetY | The y coordinate of the mouse pointer when an event occurs, relative to the containing element
returnValue | Sets the return value for the event
screenX | The x coordinate of the mouse pointer when an event occurs, relative to the screen
screenY | The y coordinate of the mouse pointer when an event occurs, relative to the screen
shiftKey | The state of the Shift key
srcElement | The deepest element that the event occurred over
srcFilter | Specifies the filter that caused the element to produce an onfilterchange event
toElement | Specifies the element being moved to (for onmouseover and onmouseout events)
type | Returns the name of the event as a string, without the on prefix, such as mouseover instead of onmouseover
x | The horizontal position of the mouse in window coordinates (pixels)
y | The vertical position of the mouse in window coordinates (pixels)

This object is extremely useful when we write script code and perform exotic sounding things like 'event-bubbling', however we'll avoid discussing any of the properties until chapter 5 of this book, as you'll need to understand how events can be caught by your scripting code first.


Summary

In this chapter we've gained a thorough introduction to three of the most important parts of Dynamic HTML: the browser object model, the window object, and most of window's child objects. We looked at the reasons why the browser object model is important to Dynamic HTML programmers although held back from giving complex examples of how it can be manipulated via script code. We looked at how the object model is use to hide us from the internal operations of the browser itself. We considered the top level object, window and looked at its most significant properties, methods and events. We finally looked at all of the child objects of window to round off this chapter.

Specifically, we learned that:

  • The browser object model is the interface between our script code and the browser itself.

  • Through the properties, methods, and events of the different objects in the object model, we can find out about objects, tell them to perform tasks, and be notified when something happens to the object.

  • The window object is the top-level object in the object model, and, while it has a lot of significant elements, this doesn't necessarily make it the most important object we'll use in DHTML programming.

  • A number of child window objects allow us to do things like access the browser's history list, find out information about the browser's version and display properties, and more

With this foundation in the object model, we're ready to move on to discuss the most important child of the window object, the document object. In the next chapter we'll be able to find out why this provides Dynamic HTML programmers with the functionality needed to manipulate individual elements on the web pages, such as text and graphics, something that was never possible in previous Internet Explorer releases.


Chapter 14: Dynamic HTML Scriptlets

Scriptlets are one of the latest innovations in IE 4. They are the missing link that further enriches the Dynamic HTML object model, and makes it interesting not only to Web developers, but also to VB and VC++ developers. The integration between IE4 and the upcoming Windows 98 shell pushes Dynamic HTML as a real development platform. However, any real-world application development platform must provide a way (and if possible an efficient and safe way) to reuse code. Scriptlets are just this: self-contained and programmable Dynamic HTML pages that can be hosted inside other HTML pages as components.

In this chapter, we'll introduce the concept of Scriptlets, and show you how to get started using them in your own pages. While the underlying technology that makes them work is quite complex, as a Web author we can ignore most of this complexity and let Internet Explorer handle it in the background for us automatically. As you'll see, Scriptlets offer an exciting mix of easy construction, with quite amazing abilities. In effect, they allow us to create our own ActiveX controls, without resorting to a 'real' programming language like Visual Basic, C++, Delphi, etc.

So, we'll be covering:

  • What Scriptlets are, and what they can do

  • When you should and shouldn't use them in your pages

  • How we define the various parts of a Scriptlet, including the interface code, the visible and the script sections

  • Some examples of Scriptlets in use, showing you how they work

  • How Scriptlets can be used in other applications, such as Visual Basic or Office 97

To start with, we'll take an overview of what Scriptlets are, and see a couple of simple examples.


What are Scriptlets?

Until the release of IE4, the only way to create a custom ActiveX control was to use a 'real' programming language like C++. While this is still the case for large and complex controls, often we (as Web developers) would like to create smaller, simple controls that encapsulate code we reuse many times on our site. Since the development time to create real ActiveX controls is greater than the time it takes to copy and paste that reusable code onto every page, or into a linked client-side code file, we are often tempted to take the easy way out.

With the availability of Scriptlet technology, we have an easy way to encapsulate client-side code as an ActiveX control component. This allows us to quickly create platform independent (with the upcoming release of IE4 for the UNIX and Macintosh platforms) components that we can deploy onto the Web.


Scriptlet Architecture

Scriptlets are simply HTML pages that contain client-side script, which conforms to a standard scripting architecture. This architecture, similar to VC++ and VB, defines what routines and variables are public (that is exposed to the control's container on the page) and private (hidden from the control's container).

When a scriptlet is loaded into memory, it is loaded into an instance of the HTML parsing engine that is itself wrapped inside a COM container. This container is placed inside the HTML parsing engine of the original HTML page that contains the reference to the Scriptlet. This implementation means that the Scriptlet can let Internet Explorer handle all of the housekeeping tasks of the component, so that all we have to worry about is creating the HTML page that contains the internal logic of our Scriptlet. Once it is inserted into the original HTML page, it effectively becomes an ActiveX control.

It's important to realize that our Scriptlet control has the option of providing a user interface. Since it is parsed by the HTML parsing engine, we can use standard HTML and Dynamic HTML within our scriptlet to provide information to the user. In the remainder of this first section of the chapter, we'll show you very briefly two simple examples of Scriptlets in use, and see the basics of their implementation.


The Simple 'Typing' Scriptlet

Our first example is a Scriptlet with a user interface. All it does is appear to 'type' a message onto the screen. The main page contains a text box and buttons that can be used to control the Scriptlet:

You can run or download this page, typetext.htm, from our Web site at http://rapid.wrox.co.uk/books/0707.

The Scriptlet itself is a Dynamic HTML page, which produces white mono-spaced text on a black background:

<html>
<head>
<script language=jscript>
...
... script goes here
...
</script>
<style>
  BODY {background-color:black; color:white; font-family:"Courier New",Monospace;
        font-size:12pt; font-weight:bold}
</style>
</head>
<body>
  <div id=objDiv style="position:absolute; top:5; left:5; width:100; height:100">
    <p id=objPara></p>
  </div>
</body>
</html>

The main page, typecomp.htm, that uses this Scriptlet contains all the descriptive text, instructions, text box, and buttons. It also contains a <OBJECT> tag that inserts the Scriptlet page typetext.htm:

<html>
<head>
  <title>Using Visible Scriptlet Components</title>
</head>
<body onload="startCode()">
<h3>Using Visible Scriptlet Components</h3>
<object id=myTypist width=400 height=100 type="text/x-scriptlet"
        data="typetext.htm" style="position:relative;">
</object>
...
... rest of page goes here
...
</body>
</html>

As you can see, the Scriptlet simply contains standard HTML that the IE4 parsing engine can display within the window defined by the <OBJECT> tag in the main (original) page.

Defining The Scriptlet's Interface

Of course, to be able to manipulate the Scriptlet, the original page has to have a way of accessing it. Scriptlets define an interface, which the page that contains them can use to reference the public properties, methods and events supported by the Scriptlet – in the same way as it would with any other ActiveX control.

Defining Properties

A property is simply a variable within the control. Any variable that has public_ prefixing the variable name will be exposed as a property that can be accessed by the scriptlet's container. This is the easiest way to declare a property and set the default value of it:

<script language=jscript>
//public property variables
public_property1 = "Hello"

If you have any experience programming controls for Windows, then you know that COM also allows us to define a function that behaves like a property. This is especially useful if we need to perform input validation or other calculations before we set the value inside the control. The Scriptlet architecture allows us to declare a function for the put and get operations of a property. When the code in the container tries to place a value into a property, it calls the function that has public_put_ prefixing the property name – for example, here is the definition for a property named roomSize:

//private internal 'member' variable
var m_roomSize = 0

function public_put_roomSize(newvalue)
  {
   if ((newvalue > 0) & (newvalue < 99)) m_roomSize = newvalue
  }

The variable m_roomSize is a private internal variable that will be used inside the Scriptlet's code sections. It cannot be accessed from outside the Scriptlet, and so can only be set using the public_put_roomSize function (i.e. the roomSize property). This allows us to validate the new value proposed by the user and reject any unsuitable ones, as we've done in the code above.

Likewise, when the code in the container tries to get the value of a property, it calls the function that has public_get_ prefixing the property name. In this case, we can just return the value of the internal variable m_roomSize:

function public_get_roomSize()
  {
   return m_roomSize
  }

By providing both the put and get functions, we produce a property that is read/write to the outside world. If we want our property to be read-only or write-only, we define just the put or the get function, as appropriate.

Keep in mind also that we can only define a property using one of the two outline methods we've discussed here. If we use the put and get functions method then we cannot have a variable in our Scriptlet which has public_ prefixing a property name that we are also using for the put and get functions.

Defining Methods

A method is declared simply by adding the prefix public_ to the name of the function or subroutine that carries out the method:

function public_rollsOfWallpaper(intLengthEach)
  {
    ... some code to calculate the result ...
    intNumberOfRolls = ...
    return intNumberOfRolls
  }

Any functions or variables within our Scriptlet not named with the public_ prefix will only be accessible to code within the Scriptlet, and not from the containing page. Like all routines, we can pass values into the routine as parameters (such as intLenghtEach above), and we return the value, if there is one, just like we would from any other function. Of course, the function does not have to return a value if it's only required to carry out some direct action within the Scriptlet.

The interface of a Scriptlet can also be defined in another way, using the JavaScript public_declaration object. We'll see how this is done later in the chapter. We'll also see how events are declared and implemented.

Setting Properties and Calling Methods

In the main (container) page, we can use the properties and methods of the Scriptlet just like we would with any other ActiveX control or integral HTML object:

MyScriptlet.roomSize = 42;                  'set the roomSize property
alert(MyScriptlet.roomSize);                'retrieve the roomSize property
alert(MyScriptlet.rollsOfWallpaper(33));    'call the rollsOfWallpaper method

The 'Typing' Scriptlet Code

Our Typing Scriptlet, typetext.htm, provides four properties and one method:

  • The property messageText (write only) is used to set the text of the message to be 'typed'.

  • The property typeStatus (read only) returns the current status of the 'typing' process.

  • The properties height and width (write only) are used to set the size of the typing area.

  • The method stopTyping() stops the current 'typing' process.

You can see how these properties and methods are defined by the interface in the code section below. This is the entire script section of the typetext.htm Scriptlet:

<script language=jscript>
//private internal 'member' variables
var m_messageText = " ";
var m_typeStatus = "Idle";

//other internal variables
var timer = null;
var nPosition = 0;
var nLength = 0;

//--------------------------------------------
//put routine for text (write only)

function public_put_messageText(newString)
{
  nPosition = 0;
  if (newString.length > 0)
  {
    m_messageText = newString;
    nLength = m_messageText.length;
    timer = setInterval("typeText()", 200);
    m_typeStatus = "Typing";
  }
  else
  {
    nLength = 0;
    m_messageText = "";
    clearInterval(timer);
    document.all("objPara").innerHTML = "";
    m_typeStatus = "Idle";
  }
}

//-------------------------------------------
//get routine for typeStatus (read only)

function public_get_typeStatus()
{ return m_typeStatus }

//--------------------------------------------
//put routine for height (write only)

function public_put_messageHeight(newValue)
{
  if ((newValue > 9) & (newValue < 1000))
    document.all("objDiv").style.pixelHeight = newValue
  else
    alert("messageHeight must be between 10 and 999");
}

//--------------------------------------------
//put routine for width (write only)

function public_put_messageWidth(newValue)
{
  if ((newValue > 9) & (newValue < 1000))
    document.all("objDiv").style.pixelWidth = newValue
  else
    alert("messageWidth must be between 10 and 999");
}

//-------------------------------------------
// public method for the component

function public_stopTyping()
{
  clearInterval(timer);
  m_typeStatus = "Stopped";
}

//-------------------------------------------
//private internal routines

function typeText()
{
  if (nPosition < nLength)
  {
    nPosition++;
    document.all("objPara").innerHTML = m_messageText.substr(0, nPosition);
  }
  else
  {
    clearInterval(timer);
    m_typeStatus = "Complete";
  }
}

</script>

As you will be able to see from the code, the Scriptlet works by using the setInterval method of its own window object, and simply changing the text that is displayed in the <DIV> tag objDiv each time the interval occurs. At the same time, it sets appropriate values for the internal m_typeStatus variable, which provides the read-only typeStatus property. This simple example should prove that the architecture of scriptlets is relatively simple, and indicate how they allow us to create custom client-side controls quickly and easily.

It's As Easy As That!

So, using scriptlets is as simple as using an ActiveX control. All we have to do is place an <OBJECT> tag within our HTML page that will contain the Scriptlet, and then we can use the scriptlet just as we would use any other ActiveX control:

<object id=myTypist width=400 height=100 type="text/x-scriptlet"
        data="typetext.htm" style="position:relative;">
</object>

The only differences between a scriptlet <OBJECT> tag, and an <OBJECT> tag that defines an ActiveX control, are the values of the type and data attributes, and the lack of a ClassID (or GUID). The type attribute defines the MIME type that Internet Explorer should use when parsing the Scriptlet code; the MIME type for scriptlets is "text/x-scriptlet". The data attribute defines the location of the scriptlet code, as a standard URL. This is similar to the codebase attribute used for ActiveX controls.

Notice that there are no parameters declared using <PARAM> tags. This is because scriptlets do not currently support parameters, however Microsoft is working on adding support for parameter tags for a future release of Internet Explorer.


The Simple 'Decorator' Scriptlet

Our second simple example shows how we can create a Scriptlet that does not have a user-interface. This page contains a Scriptlet that calculates the amount and cost for paint, wallpaper, and carpet to completely decorate a room. The Decorator Scriptlet simply encapsulates a series of functions that could easily be re-used several times in our pages. As you can see, all we have is a simple page that contains a few buttons to access the various properties and methods of the Scriptlet:

You can run or download this page, decorate.htm, from our Web site at http://rapid.wrox.co.uk/books/0707.

In order to use the Decorator scriptlet, we place the appropriate <OBJECT> tag in the main page. In this case since we do not want the control to be visible, we've set the CSS visibility property to hidden:

<object id=myDecObj width=1 height=1 type="text/x-scriptlet"
        data="decocalc.htm" style="visibility:hidden">
</object>

As you can see from the screenshot, the Rolls of Wallpaper button simply calls the rollsofWallpaper method of the myDecObj control (our scriptlet), passing it a value which is the price of each roll:

<BUTTON ONCLICK="myDecObj.rollsOfWallpaper(6.49)" STYLE="width:500">
  Click here to run the code: <CODE>myDecObj.rollsOfWallpaper(6.49)</CODE>
</BUTTON>

Inside the Scriptlet, the rollsOfWallpaper function calls another internal sub-routine (named getRollsWallpaper) which does the calculation and returns the result.

function public_rollsOfWallpaper(cPriceEach)
{
  alert("You'll need " + getRollsWallpaper(cPriceEach))
}

The rollsOfWalpaper function then displays this as an alert dialog, as you can see here:

This is just one example – the Decorator Scriptlet contains several other properties and methods, and you can experiment with it yourself using the sample page available from our Web site at http://rapid.wrox.co.uk/books/0707. The code within the Scriptlet is fully commented so that you can see what's going on. For example, this Scriptlet also provides a shoppingList method which returns a string value, instead of displaying the result in an alert dialog:

function public_shoppingList(sBreak, cWallpaper, cPaint, cCarpet)
{
  if (sBreak == null) sBreak = "";
  sList = getRollsWallpaper(cWallpaper) + sBreak
        + getPintsPaint(cPaint) + sBreak
        + getYardsCarpet(cCarpet);
  return sList
}

In the main page, we can use the method to create a shopping list of materials in a new browser window. The sBreak parameter of the method is the string or character used to delimit each item in the list, and the other three parameters are the individual prices of each item:

function RunShoppingListCode()
{
  sLF = "<BR>";
  cPaperEach = 6.49;
  cPaintEach = 3.99;
  cCarpetYd = 29.50;
  sMyList = myDecObj.shoppingList(sLF, cPaperEach, cPaintEach, cCarpetYd);
  objNewWin = window.open("","","toolbar=no;statusbar=no;directories=no");
  objNewWin.document.write("<H3>My Shopping List</H3>");
  objNewWin.document.write("<B>" + sMyList + "</B><HR>");
  objNewWin.document.close();
}

And here's the result:


Using Scriptlets in HTML Pages

Having seen an overview of some simple uses of Scriptlets, we'll consider in more detail where and why we might use them. It is also important to realize that, because they behave in many ways like ActiveX controls, our viewers can control whether they actually appear in our pages through the options available in their browser. Firstly, however, let's look at when we should and shouldn't consider using Scriptlets.


Why Should You Use Scriptlets?

Scriptlets are simple and powerful, provided that you really need them. If you are a C/C++ developer writing image-processing or data-warehousing applications, it's unlikely that you'll need to learn about Scriptlets. But if the Web and HTML are part of your everyday work, Scriptlets can be incredibly useful. Here's a brief top-ten list of reasons why you – as a Web developer –should consider using Scriptlets:

  • You can write them in any (repeat, any) scripting language. You don't need to know C/C++ or any other programming language.

  • You can prototype them in a matter of minutes, and things will certainly be even easier once updated development tools become available.

  • You can create your own HTML objects, and describe them in terms of properties, methods and events.

  • Your entire edit-compile-debug cycle is reduced to hitting Save and F5 in the IE4 main window.

  • For the very first time you have the opportunity of writing reusable HTML code, which is a primary requirement in any large-scale Web-oriented project.

  • You can exploit all the power of the Dynamic HTML object model and implement cool effects such as text or image drag-and-drop, mouse over and out object manipulation, and style changes.

  • You can also use your Scriptlets in desktop applications, through a dedicated ActiveX control.

  • You can use them to customize and enhance your Windows Active Desktop and your disk folders.

  • You can always convert your samples to ordinary ActiveX controls once you've fully tested their public interface.


When Shouldn't You Use Scriptlets?

Scriptlets contain ordinary script code, which can be a disadvantage since the browser must interpret it before producing output. This is not a problem for small and simple components, but might be an issue when the size of the Scriptlet increases. A complex component such as a calendar, composed of a table and some ActiveX Form controls (i.e. combo boxes and text boxes) may take a while to load and respond. The corresponding Office 97 Calendar ActiveX control appears to load and run much faster. Here are some possible snags you should consider carefully before writing and distributing Scriptlets:

  • A Scriptlet may run slower than a corresponding ActiveX control.

  • Scriptlets don't support any kind of code protection, and are open to inspection and alteration by others.

  • A user can modify Scriptlets, inadvertently or not, and this might result in run-time errors.

  • Scriptlets aren't subject to licensing, and may be copied without permission.

  • Scriptlets don't get downloaded if your security level is set to High for the Internet zone they come from.

  • At present, there are no authoring tools that fully support for Scriptlets, though these will surely appear in the near future.

  • A Scriptlet may include a collection of files (video, images, sounds, controls, other Scriptlets or HTML pages), but no utility for gathering all these together in a CAB or ZIP file is available.

  • Scriptlets are supported only by IE4. Netscape Navigator 4 and other browsers simply ignore them.

  • To host Scriptlets in desktop applications you need the Microsoft Scriptlet Control.

  • When using Scriptlets with the Microsoft Scriptlet Control, you need to specify an absolute path to the source code. This works well if the path is a real URL, but forces you to indicate a specific installation directory if you are using it locally.

The lack of dedicated authoring tools is a drawback that should disappear over time. Topics such as licensing and protection might become a central issue if Scriptlets succeed in populating the Windows 98 desktop and folders. Reusable components must be safely and easily distributable, and this issue will need careful consideration when you choose your development strategy.


Internet Zones and Scriptlet Security

IE4 introduces security zones, and allows users to associate them with any files that can open or download. They choose the level of security depending on where the page comes from, and how much they trust its source. Many Web sites adopt secure protocols to prevent unauthorized people from spying on the data sent and received. IE 4 recognizes these sites, and displays a lock icon on the status bar while viewing documents from them. IE4 lets users divide the Internet into four different zones, so that they can assign each Web site to a zone with a suitable security level. The zones are: Local Intranet, Trusted Sites, Restricted Sites, Internet. Local Intranet and Internet have a default security level of Medium. The default safety level is high in Trusted Sites and low in Restricted Sites, so consequently the security settings are Low and High, respectively.

All the files that IE 4 finds locally on your computer are assumed to be completely safe, and no security settings are applied to them. This allows you to open and run documents and programs on your computer without prompting or interruption. The security levels for each of the various zones is completely up to you.

Web sites in the Restricted Sites zone may be those from which you want to avoid running or opening any scripts or active content. Scriptlets are portions of Web information subject to security restrictions. IE 4 will only download a remote Scriptlet to your machine if you include the source Web site in a zone with a security level of Medium or Low. The Internet Options dialog box (available from the View menu) allows you to change the security settings, choosing from the general values (Low, Medium, High) or by defining your own settings. In this case, to avoid problems when loading remote Scriptlets, make sure that your have enabled the Script ActiveX controls marked safe for scripting option in your Security Settings, as illustrated here:

In addition, check that you haven't disabled the Controls not marked safe for scripting and initialization option. This should be Prompt or Enable, as shown:


The Structure of a Scriptlet

A Scriptlet is a regular HTML page that has two main parts: body and script code. However, a Scriptlet is also a very peculiar HTML page, whose scripting code makes it programmable from external callers, such as a host HTML page or a Visual Basic form. There is a strict relationship between the body and script. The body includes the objects on which script code will work in order to produce the desired effect. Since both code and body are contained in the same file, we have a self-contained component ready to be used as an object. In addition, the script code uses a conventional syntax to make public some of its own variables and routines. In this way we can automate the behavior and the appearance of the page:

As we mentioned earlier, a Scriptlet's automation interface is expressed in terms of properties, methods and events.


The Body of a Scriptlet

Since a Scriptlet is ultimately an HTML page, we can view it through any browser – though what we actually see won't be interesting in most cases. In other words, not all the Scriptlets are self-initialized and display something useful without being scripted. If we have a component that displays a user-specified text string in a particular fashion, we need to provide that text string before using the Scriptlet. Thus, outside its host site, it will probably show a default string which might be empty or contain copyright information.

The body can include images, links, lines, text, and even ActiveX controls, applets, sounds, and anything else. It represents the user interface of the module, and includes the constituent elements on which the script code within the Scriptlet will act. The actual body may be defined at design-time, or created dynamically depending on what we really need to do.

For example, if we want a dynamic text string to become a Scriptlet, we might want to have a body like this:

<body>
  <span id="text"> Change this text at runtime through a property. </span>
</body>

We change the actual text using an exposed attribute that ends up setting the outerText property of the given element. The following VBScript code that does just that:

Sub DoSetText( sText )
  Set coll = document.all.tags("SPAN")
  coll.item(0).outerText = sText
End Sub

Alternately, we could also have an empty body which will be filled when needed with an HTML string:

<body>
</body>
Sub DoSetText(sText)
  sPrefix = "<span id=text>"
  sSuffix = </span>
  sHtmlText = sPrefix + sText + sSuffix
  Set coll = document.all.tags("SPAN")
  if coll.length > 0 then
    coll.item(0).outerHtml = sHtmlText
  else
    document.body.insertAdjacentHTML "AfterBegin", sHtmlText
  end if
End Sub

In the above sample, we insert a new SPAN element if one does not exist, or change the existing one if it is already there.


The Script Section of a Scriptlet

The script portion of a Scriptlet has a conventional structure. We must expose the public interface of the object, as well as include some internal 'helper' routines. The script procedures we write will provide the Scriptlet behavior, and take care of responding to all the Dynamic HTML events that are supported by the Scriptlet itself. Using special naming conventions, we declare some of these routines as public and allow the container script code to access them. Scripting may use a mix of VBScript, JavaScript, JScript, and any other scripting language supported in the browser.

Defining the Public Interface

At the start of this chapter, we saw one way to define the interface of a Scriptlet – with VBScript and the implicit declarations which use the public_ prefix. JavaScript offers a public_description object that works as a class definition module. In a certain sense, using this object is similar to writing a header file for a C++ class. We can use it to assign properties and methods to the Scriptlet by name. For each attribute, we specify the name of the 'helper' script routine that implements it. A container's element will refer to that attribute using an external name, which is determined following a special naming convention.

First of all, we have to initialize the public_description object like this:

<script language="Javascript">
public_description = new CreateThisScriptlet();
</script>

CreateThisScriptlet is not a system routine, but just the name we give to the actual function we want to use to create the interface. This is a typical body for it:

function CreateThisScriptlet () {
  this.put_color = put_color;
  this.get_color = get_color;
  this.title = window.document.title;
  this.paint = doPaint;
  this.event_onPainting = "";
}

CreateThisScriptlet defines properties, methods and events for the new object, assigning them dynamically to the this pointer. As we saw earlier in the chapter, to fully expose a property called color we need to write two script procedures called get_color and put_color. They must be assigned to the corresponding elements in the public_description object, and take care of reading and writing the property value. In the get function we usually only need to return the current setting. The put function, however, saves the new value and often applies the changes to the Scriptlet output. Notice that we must use the put and get prefix to qualify it as a read/write property. If we want a read-only or write-only property, we just omit the declaration we don't need.

Defining Methods and Events in JavaScript

Methods don't follow any convention, and the name we assign to the public descriptor is the same one we invoke outside the Scriptlet. In the above sample, the doPaint procedure will execute after any external caller invokes the paint method. There is absolutely no need to implement the paint function with a script procedure called doPaint—though it is preferable for the sake of clarity. Feel free to give it the same name as your dog if you prefer!

Events have no actual code implementation. In an ActiveX control, for instance, you need to declare events just to let the development tool build the proper type library, so that the container knows about these events. This is also true for Scriptlets. A component fires events, but the actual code that runs in response is decided and executed in the container page. In other words, the host and object swap their respective roles. When raising an event, any object (Scriptlets, ActiveX controls) ends up invoking a dynamic method on the host, allowing the host to provide the code that is executed in response to the event.

At first sight, the line

this.event_onPainting = "";

seems to be necessary to make sure that event is properly notified and received. If you want a Visual Basic 5 ActiveX control to raise a given event, you absolutely need to declare it. This ensures that the container will get a pointer to a connection-point interface that includes an entry-point with the name onPainting:

Event onPainting(ByVal data As Long)

This is certainly true for ActiveX controls, but not for Scriptlets. If we want our Scriptlet to raise an event called onPainting, we don't need to declare it explicitly, because the Scriptlet custom events are all routed through the same entry-point: onScriptletEvent. More on this later. For now, just keep in mind that we aren't strictly required to declare events, but it is nice if we do so simply for documentation's sake.

Javascript vs VBScript

In the previous section, we discussed how to define the Scriptlet's public interface using JavaScript . As we saw at the beginning of this chapter, this is not the only approach we can take. By using a special naming convention for all the properties and methods you want to expose, you can make them public by design.

If you're an expert Windows programmer, this should sound familiar, and remind you of the two options you have for exporting functions from within a DLL. You can list all of them in a DEF file, or you can make them public using the special keyword declspec(dllexport) in the prototype.

Look at how to convert the previous sample using the public keyword:

Function public_get_color
Sub public_put_color
Sub public_paint
' note that there are no declarations for events

This approach, called default interface description, allows us to use VBScript instead of JavaScript. However, bear in mind that the JavaScript approach creates a single niche in our code where the whole of the object's public interface is described. Furthermore, it allows us to have different public and private names for all the methods and properties.

Note that the JavaScript approach always takes priority over the default interface descrition approach if both exist in the same Scriptlet.

Ambient Properties

A Scriptlet runs inside a special container object that offers the following ambient properties.

Property | Type | Description
scrollbar | Boolean | If true enables the use of scrollbars. If the Scriptlet content is too large for its actual size, both vertical and horizontal scrollbars will appear automatically. Default is false.
selectableContent | Boolean | If true allows users to select the Scriptlet's content and drag it or copy it to the clipboard. Otherwise the user can't select anything in the Scriptlet's area. Default is false.
version | String | Identifies the current version of the Scriptlet container object. At present it is "4.0 Win32".
frozen | Boolean | Read-only property. If false, indicates that the page hosting the Scriplet is ready to handle events.

These stock attributes are accessible through an object called external, which is a new property of the Dynamic HTML window object. We can use them to control the environment of our Scriptlet to some extent, for example we can enable the use of scrollbars from within a Scriptlet with a line like this:

window.external.scrollbar = true

Accessing the External Object

The external object is available only when the Scriptlet page is actually viewed as a Scriptlet within another page. If we load and view the Scriptlet as an ordinary HTML page (for example, select it up from the File | Open menu) the external object doesn't get initialized. Attempting to use it will then result in a runtime error. Curiously, this situation seems to be handled differently by the two scripting engines that come with IE 4. Consider the following HTML code:

<HTML>
<BODY>

<SCRIPT language="Javascript" for="window" event="onload">
  alert("External.version is: " + window.external.version);
</SCRIPT>

<SCRIPT language="VBScript" for="window" event="onload">
  MsgBox "External.version is: " & window.external.version
</SCRIPT>

</BODY>
</HTML>

Both the scripts catch the onLoad event of the window object, and try to display a message box with the current value of the window.external.version property. The JavaScript procedure traps the exception and returns the string "undefined", while the VBScript procedure produces the error like the one shown here:

Detecting When a Scriptlet is Running – Our Custom InScriplet Variable

A Scriptlet is nothing more than a Web page viewed through a special container. Though it may not always make sense, we might also want it to display properly if viewed directly with a browser, rather then embedded into another page. Unfortunately, this produces a runtime script error if we refer to the window.external object in our code. It would be nice to be able to detect whether the page is being viewed directly in a browser, or as a Scriptlet. In this way, we could only access the ambient properties when appropriate.

To do this, we introduce a custom JavaScript variable named InScriptlet into our Scriptlet code, and set it to true if we are running the page as a Scriptlet, or false if not. This is done using the JavaScript typeOf method, which tells us what kind of value a variable or property is holding:

InScriptlet = (typeOf (window.external.version)=="string");
if( InScriptlet ) {
  window.external.scrollbar = true;
}

JavaScript will never prompt us with a script error. In the above situation, the window.external.version property will be of type string if it actually exists (i.e. we are running a Scriptlet), or undefined if the external object doesn't exist because we are running the page directly.


The Container Object

All Scriptlets runs in an HTML page by the means of an embedded container. This container recreates for the Scriptlet the same habitat as IE 4, and makes available the ambient properties listed earlier. In addition, it provides the following methods:

Method | Description
bubbleEvent | Passes the current event down to the host environment, whether an HTML page or a Visual Basic form.
raiseEvent | Fires a custom event for the Scriptlet. Each event is identified by name but routed through the single onScriptletEvent of the container.
setContextMenu | Allows us to associate a pop-up menu with the Scriptlet. The menu will appear after a right-click on the Scriptlet's area of the page.

A Scriptlet receives notification of all the standard Dynamic HTML events for the window and document objects, and any other elements it contains. bubbleEvent is the method we use to pass (or bubble) any events we trap in the Scriptlet back up to the container. If we don't pass these events explicitly, event bubbling stops at the container and the host HTML page (or VB form., etc.) will never know that an event occurred.

If we want to handle an onclick event, for example, then bubble it back up to the container page, we could use the following code within our Scriptlet:

<script language="VBscript"
  for="document" event="onclick">
  if (InScriptlet) And Not (window.external.frozen) Then
    window.external.bubbleEvent
  end if
</script>

Aside from just checking to see if the Scriplet page is being run as a Scriptlet (indicated by the InScriptlet variable) we also have to check to make sure that the hosting container is ready to receive events. This is done by making sure that the window.external.frozen property is false.

Once the event gets back up to the container page, we can handle it there. We do this using the container window's event object, and examine it to get information about the event. So code to handle a bubbled event might look like this:

<script language="jscript" for="Scriptlet1" event="onclick">
// inspect event object to see which button was clicked
if (window.event.button = 2)
  alert ("Right-Button Clicked");
</script>

If the Scriptlet is hosted in a non-Web browser environment, such as a C++, Visual Basic, or Office97 application, we have to use the event object that hangs of the Scriptlet control, rather than the integral window object's event object. We'll look at hosting Scriptlets in these kinds of applications towards the end of this chapter.

Handling Custom Events

The raiseEvent method fires custom events, such as the onPainting event we discussed earlier. Its prototype is

RaiseEvent(name, data)

The following is a typical call to the raiseEvent method:

window.external.raiseEvent "onPainting", window.document

The first argument of raiseEvent is the name of the event we want to raise. The second is the data we want to associate with the event, and notify to the receiver. Whatever name we assign to the event, it will always be detected and handled in the container through the onScriptletEvent – which is the counterpart of raiseEvent. Because of this, there is no need to declare our events in a Scriptlet, as they all arrive through the built-in onScriptletEvent event.

When the client is notified of a custom event (i.e. one that is not a standard DHTML event), it can distinguish which of the Scriptlet's event this is with multiple if..then statements, or with a select..case or switch statement—based on the name argument. For example:

<script language="jscript" for="Scriptlet1" event="onscriptletevent(eventname, eventdata)">
if(eventname == "onPainting") {
    alert("Start painting");
}
else {
  if(eventname == "onEndPainting") {
    alert("End painting");
  }
}
</script>

The Scriptlet would raise these events to the container using:

window.external.raiseEvent "onPainting", window.document
window.external.raiseEvent "onEndPainting", window.document

The window.document parameter simply represents the data we want to pass to the container's event code. This, of course, will change according to the actual requirements of your code. As previously discussed, the window.external.frozen property should be checked before firing events.

All the events a Scriptlet raises are perceived by the container as occurrences of the same event, onScriptletEvent, with different parameters. This also means that we can define events at any time, and qualify them with a string.

Adding a Context Menu

The Scriptlet container object also allows us to create and assign a pop-up context menu to a Scriptlet. To do this we must use either VBScript or JavaScript, as these are the only script languages guaranteed to create arrays compatible with the expectations of the setContextMenu method.

To create a context menu that shows n items, we define an array of 2*n elements. For each pair of elements in the array, the first item is the caption that will be shown on the menu, while the second is the name of the function that will executed when the user selects that item on the menu (notice that these procedures cannot take parameters). If we want an item separator, just add a couple of empty items. Finally, we pass the array to the setContextMenu method of the window.external object:

<SCRIPT language="VBScript" for="window" event="onload">
   dim menuItems(6)

   menuItems(0) = "&One"
   menuItems(1) = "One"

   menuItems(2) = "&Two"
   menuItems(3) = "Two"

   menuItems(4) = "&Three"
   menuItems(5) = "Three"

   window.external.setContextMenu(menuItems)
</SCRIPT>

Once we have assigned the context menu to the external object, we're done. We have to do nothing more to set up the menu, and have no need to investigate which button the user presses. The container takes care of handling the right-click event, and running the appropriate code, automatically. Here's the result:

Creating Dynamic Context Menus

The best place to create the context menu is the window.onLoad event, as we did in the previous example. If we use a different event, we have to wait for that event to occur before the context menu is initialized and displayed. The context menu can be changed in code during Scriptlet execution, but all the items displayed at any moment in time must occupy consecutive locations in the menu items array. The following example produces the same menu as in the previous example the first time it is clicked. From the second time onwards, the menu changes to include a separator and a fourth menu item:

<script language="VBscript">
dim menuItems(10)
dim bFirstTime

Sub InitMyScriptlet
  if InScriptlet then
    window.external.selectableContent = mSelectable
    menuItems(0) = "&One"
    menuItems(1) = "One"
    menuItems(2) = "&Two"
    menuItems(3) = "Two"
    menuItems(4) = "&Three"
    menuItems(5) = "Three"
    menuItems(6) = "xxx"     ' stub the next items
    menuItems(7) = ""        ' stub the next items
    window.external.setContextMenu(menuItems)
    bFirstTime = 1
  end if
End Sub

Sub document_onmousedown
  if bFirstTime = 0 then
    menuItems(6) = ""       ' separator
    menuItems(7) = ""       ' separator
    menuItems(8) = "&Four"
    menuItems(9) = "Four"
    window.external.setContextMenu(menuItems)
  else
    bFirstTime = 0
  end if
End Sub

Sub One
  MsgBox "One"
End Sub

Sub Two
  MsgBox "Two"
End Sub

Sub Three
  MsgBox "Three"
End Sub

Sub Four
  MsgBox "Four"
End Sub
</script>

The context menu gets initialized first in window.onload event, then is updated in the document_onmousedown event. In any case, before the menu is displayed, our Scriptlet will receive an onmousedown event, so we have the opportunity to initialize it, or make changes to it, there. Here's the result—you can run or download this page, MenuDemo.htm, from our Web site at http://rapid.wrox.co.uk/books/0707:

If you aren't using different arrays for different menus, you probably declared an oversized array to make room for new items. In this case, we recommend you use:

menuItems(6) = "xxx"     ' stub the next items
menuItems(7) = ""        ' stub the next items

to suppress the unnecessary items, by assigning a non-empty caption and an empty or non-existing procedure. Otherwise you risk being flooded with item separators from the multiple empty strings.


Writing Our First Scriptlet

Images are widely used on Web pages, but you rarely see them in scrollable areas. Writing a Scriptlet that is able to scroll images is really a matter of a few minutes work. What we have to do is create and set up an IMG tag in the Scriptlet, and then enable the container's scrollbars. The body of the Scriptlet contains only an empty image element. Here is the entire source code for the Scriptlet:

<html id=MyPage>
<head>
<title>ScrollImage Scriptlet</title>
</head>
<body>

<script language="VBscript" for="window" event="onload">
  InitMyScriptlet
</script>

<script language="VBscript">

' Initialize the control
' ----------------------------------------------
Sub InitMyScriptlet
  if InScriptlet then
    window.external.scrollbar = True
  end if
End Sub

' Set the image to show
' ----------------------------------------------
Sub DoSetImage(sImage)
  document.images(0).src = sImage
End Sub
</script>

<script language="JavaScript">

// declare the object interface
public_description = new CreateMyScriptlet();
var InScriptlet = (typeOf(window.external.version) == "string");

function CreateMyScriptlet() {
  this.put_Image = put_Image;
}

function put_Image (sImage) {
  DoSetImage(sImage)
  return 1;
}
</script>
<img id="image" src="" alt="">
</body>
</html>

And here's the result, showing a couple of intrepid Wrox programmers on the trail of another exciting new technology:

You can run or download this page, Scroller.htm, from our Web site at http://rapid.wrox.co.uk/books/0707.

How It Works

The public interface is made up of a single property that is declared write-only for simplicity. It is defined inside the JavaScript CreateMyScriptlet function. Most Scriptlets also need a one-time initialization that is usually performed in response to the window.onload event:

<script language="VBscript" for="window" event="onload">
    InitMyScriptlet
</script>

Sub InitMyScriptlet
  if InScriptlet then
    window.external.scrollbar = True
  end if
End Sub

In this case, the event handler calls InitMyScriptlet, which simply enables the scrollbars. We don't have to worry about setting horizontal and vertical scrolling individually—all the magic is accomplished inside the Scriptlet container object automatically.

Initially, the Scriptlet has no image to display. This screenshot below shows how it appears if we view it as a standard HTML document. This is an example of a Scriptlet that absolutely requires scripting to do something meaningful.

The property image is implemented via the put_Image function. Note that put_Image is a JavaScript routine that ends up calling DoSetImage, which is VBScript, instead:

Sub DoSetImage(sImage)
  document.images(0).src = sImage
End Sub

DoSetImage just assigns the given image file name to the only IMG element found in the scriptlet body.

Extending Our First Sample

The first improvement for this example that comes to mind is adding programmable tool-tip text. Suppose we want to make available a new read/write property called Text:

function CreateMyScriptlet() {
  this.put_Image = put_Image;
  this.put_Text = put_Text;
  this.get_Text = get_Text;
}

By adding the two lines shown highlighted above, we declare the property as an attribute of our Scriptlet. The put_Text and get_Text could have the following implementation

m_Text = ""

function put_Text(sText) {
  DoSetText(sText);
  m_Text = sText;
  return 1;
}

function get_Text() {
  return m_Text;
}

When implementing a readable property, we should always consider maintaining an internal 'member' variable to make it persistent. In this example we're using the variable m_Text. When an external caller sets a new value to the Text property, the VBScript's DoSetText routine is called. because an IMG element has an alt attribute (which stores the alternative text representation of the image), we just need to assign our Text property to it to get the tool-tip we want. The Text property may be set via scripting in the container's page at any moment, according to runtime conditions:

Sub DoSetText(sText)
  document.images(0).alt = sText
End Sub

Hosting a Scriptlet in a Web page

You'll recall that we insert Scriptlets into an HTML page using the <OBJECT> tag. As a reminder, here's how:

<object id="ImgScrl1"
  data="ImgScrl.htm" width="450" height="300"
  type="text/x-scriptlet">
</object>

The data attribute specifies the file name or URL where the Scriptlet may be found, while type attribute tells IE 4.0 that it is attempting to load a Scriptlet. Once it has done so, everything works as if we are dealing with ordinary ActiveX controls. This means, for instance, that we can access the Scriptlet properties via the usual object-based syntax. The following is the source code that creates the scrolling image page you saw in the previous screenshot:

<html>
<title>Test page using ScrollImage</title>

<b>Test page using ScrollImage</b>
<script language="VBScript" for="window" event="onload">
ImgScrl1.Image = "image.gif"
ImgScrl1.Text = "Hassie and Jerry Lee"
</script>

<p>
<object id="ImgScrl1" data="ImgScrl.htm"
width="450" height="300" type="text/x-scriptlet">
</object>
</p>

<p>&nbsp;</p>
</body>
</html>


How Scriptlets Work Under the Hood

Whenever IE 4 shows an HTML page, it creates a container window that covers the entire client area. The window class name is Shell DocObject View. It has a child window called Internet Explorer_Server which is identical in size. This window represents the surface where MSHTML (the IE4 module providing the Dynamic HTML functionality) draws its output. In short, Internet Explorer_Server windows are the real HTML view objects. The illustration below shows the pile of IE 4 windows that are created when it displays a standard HTML file:

Now, look at what happens when IE4 is hosting a Scriptlet:

Behind the scenes, IE 4 uses another instance of its Internet Explorer_Server view object for handling Scriptlets. This allows the Scriptlets to inherit the Dynamic HTML object model and scrolling capabilities. Conceptually, running a Scriptlet is just like running a nested copy of IE 4 itself.


Writing Our Second Scriptlet

By now, you should have grasped the idea of what Scriptlets are, and how to write them. It's time to design and code a sample which is a little more complex. What we have in mind is a Clock object that you can put in all your pages as a reminder of time passing. A digital clock is made up of three components: hours, minutes and seconds. We can render it using a simple text string that needs formatting each time it is updated, or we can use a small table that keeps the various logical parts of the time separate. A very simple HTML table can be defined like this:

<TABLE>
  <TR>
    <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
    <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
    <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
  </TR>
</TABLE>

All the tags (TABLE, TR, TD) expose lots of attributes that set the formatting and layout of the table, but for our purposes they can all be ignored. However, a better approach would be to make them properties of the Clock Scriptlet. This sounds reasonable—especially for BORDER and CELLSPACING attributes.

A clock is a self-initialized component, so it doesn't need specific initialization code to work properly. If you open this Scriptlet in a browser you're able to use it straight away. Once more, this demonstrates that a Scriptlet is an HTML page that runs inside another HTML page:

The final 'extended' version of this page, which you'll meet later in the chapter, can be run or download from our Web site at http://rapid.wrox.co.uk/books/0707.

Coding the Clock Scriptlet

Our HTML clock component will exploit the setInterval and clearInterval methods of the window object. We just need to provide a function to stop and restart it, and some events. Here's the complete source code:

<HTML id=MyPage>
<TITLE>Clock Scriptlet</TITLE>

<script language="VBscript" for="window" event="onload">
    InitClock
</script>

<script language="VBscript">

Sub InitClock
  DoSetColor mBgColor, mFgColor
  DoUpdateClock
  if InScriptlet then
    window.external.selectableContent = True
  end if
  if mEnabled then
    mTimer = window.setInterval("DoUpdateClock", 1000)
    mStartedAt = Time
  end if
End Sub

Sub DoSetColor(b, f)
  Set coll = document.all.tags("TABLE")
  coll.item(0).style.backgroundColor = b
  coll.item(0).style.Color = f
  coll.item(0).style.fontFamily = "Tahoma"
End Sub

Sub DoUpdateClock
  t = Time
  mHour = Hour(t)
  mMins = Minute(t)
  mSecs = Second(t)
  set coll = document.all.tags("TD")
  coll.item(0).innerHTML = Right("00"+CStr(mHour),2) + ":"
  coll.item(1).innerHTML = Right("00"+CStr(mMins),2) + ":"
  coll.item(2).innerHTML = Right("00"+CStr(mSecs),2)
End Sub

Function DoFormatTime
  s = FormatDateTime(Time(), vbLongTime)
  DoFormatTime = s
End Function

Function DoGetTime
  DoGetTime = Time
End Function

Function DoAlarm
  if (InScriptlet And (Not window.external.frozen)) then
     window.external.raiseEvent "OnAlarm", window.document
  end if
End Function
</script>

<script language="JavaScript">
public_description = New CreateClock();  // declare the interface
var InScriptlet = (typeOf (window.external.version) == "string");

mBgColor = "gray";
mFgColor = "white";
mEnabled = 1;
mTimer = 0;
mStartedAt = 0;
mAlarm = 0;

function CreateClock() {
  this.put_Text = put_Text;
  this.get_Time = get_Time;
  this.get_StartedAt = get_StartedAt;
  this.put_ClockBgColor = put_ClockBgColor;
  this.put_FgColor = put_FgColor;
  this.get_FgColor = get_FgColor;
  this.put_BgColor = put_BgColor;
  this.get_BgColor = get_BgColor;
  this.Enable = Enable;
  this.Alarm = Alarm;
  this.event_OnStart = "";
  this.event_OnStop = "";
  this.event_OnAlarm = "";
}

function put_ClockBgColor(color) {
  document.bgColor = color;
  return 1;
}

function put_Text(sText) {
  document.all("Text1").innerHTML = sText;
  return 1;
}

function get_Time() {
  return DoFormatTime();
}

function get_StartedAt() {
  return mStartedAt;
}

function put_ForeColor( color ) {
  mFgColor = color;
  DoSetColor(mBgColor, mFgColor);
  return 1;
}

function get_ForeColor() {
  return mFgColor;
}

function put_BackColor(color) {
  mBgColor = color;
  DoSetColor(mBgColor, mFgColor);
  return 1;
}
function get_BackColor() {
  return mBgColor;
}

function Enable(b) {
  mEnabled = b;
  if (b) {
    mTimer = window.setInterval("DoUpdateClock", 1000);
    if (InScriptlet) and (not window.external.frozen)) {
       window.external.raiseEvent("OnStart", 0);
       mStartedAt = DoGetTime();
    }
  }
  else {
    window.clearInterval(mTimer);
    Alarm(0);
    if (InScriptlet and (not window.external.frozen)) {
       window.external.raiseEvent("OnStop", 0);
    }
  }
  return 1;
}

function Alarm(secs) {
  if (secs) {
    mSnooze = window.setInterval("DoAlarm", secs);
  }
  else {
    window.clearInterval(mAlarm);
  }
  return 1;
}
</script>

<BODY>
<TABLE>
<TR>
   <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
   <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
   <TD VALIGN="TOP" ALIGN="CENTER">0</TD>
</TR>
</TABLE>
</BODY>
</HTML>

The Clock Interface Definition

The public interface of the Scriptlet exposes the following properties, methods and events:

Name | Description
FgColor | Sets and gets the foreground color of the clock, i.e. color of the digits.
BgColor | Sets and gets the background color of the clock area.
ClockBgColor | Sets the background color of the Scriptlet area.
Time | Returns the current time formatted accordingly to the user settings.
StartedAt | Returns the last time when the clock Scriptlet was started.
Text | Sets the text to be displayed below the clock.
Enable(state) | A method that disables and enables the clock causing it to stop and go.
Alarm(secs) | A method that sets up an alarm to raise an onAlarm event every given number of milliseconds.
onStop | This event is fired each time the clock gets disabled.
onStart | This event is fired each time the clock is started.
onAlarm | This event signals that the given period is expired.

The Time property is read-only, and returns the current time. It makes use of the current user's format settings for the date and time with VBScript's FormatDateTime function. FgColor and BgColor define the color of the text and the clock background—by default, we have white text on a gray background. StartedAt has a secondary role, and its only purpose is to enrich the Scriptlet's interface. ClockBgColor is a write-only property assigned to the background color of the entire site area, which is usually darker than the clock itself.

Text is another write-only property that contains the text string we might want to associate with the clock. This text is the body of a SPAN tag with an ID of Text1. Since this is implemented via the element's innerHTML property, we can assign it text containing HTML tags as well, and be sure it will be correctly handled. This provides us with the opportunity to format the text, be it in italic or bold font, or to include images, links, etc. For instance:

Clock1.Text = "Current Time offered by <i>WROX Press</i>"

The final property, Enable, is at the core of the component. It accepts a Boolean value, and starts or stops the clock.

Updating the Time

All the updates of the clock are controlled by the setInterval method of the window object:

mTimer = window.setInterval("DoUpdateClock", 1000);

The setInterval method takes a string denoting the script code to be executed each time the given interval expires, and an interval period expressed in milliseconds. The code above causes the DoUpdateClock procedure to be called once every second. As you might guess, DoUpdateClock just refreshes the table representing the digital clock. First it obtains the current time in terms of hours, minutes and seconds. Then it sets the innerHTML properties of the three TD elements:

Sub DoUpdateClock
  t = Time
  mHour = Hour(t)
  mMins = Minute(t)
  mSecs = Second(t)
  set coll = document.all.tags("TD")
  coll.item(0).innerHTML = Right("00"+CStr(mHour),2) + ":"
  coll.item(1).innerHTML = Right("00"+CStr(mMins),2) + ":"
  coll.item(2).innerHTML = Right("00"+CStr(mSecs),2)
End Sub

Stopping the Clock

The setInterval method returns a unique identifier for the timer is starts running, and we saved this in a variable mTimer. To stop the clock, we just pass this variable to the window object's clearInterval method:

window.clearInterval(mTimer);

The onStop and onStart events are fired after the clock has been stopped and restarted. The onStart event isn't raised when the Scriptlet is loading for the first time. As you have seen in our earlier discussions, the event declarations are not really required for the Scriptlet to work. The host page will always receive onScriptletEvent events, whatever the actual event the component raises with the raiseEvent method. However, we've included the event declarations simply to document the Scriptlet's interface.

Hosting the Clock Component

A sample page that hosts our Clock component might look like this:

Here's the code for the complete page, including the <OBJECT> tags that insert the Clock Scriptlet:

<html>
<title>Test page using Clock</title>
<b>Test page using Clock</b>

<SCRIPT LANGUAGE="VBScript" FOR="window" EVENT="onload">
  Clock1.BgColor = "lightblue"
  Clock1.FgColor = "blue"
  Clock1.Alarm 5000
</SCRIPT>

<SCRIPT LANGUAGE="VBScript" FOR="Clock1" EVENT="onscriptletevent(n,o)">
  if n = "onStart" then
    MsgBox "Current time is " + Clock1.Time
  end if
  if n = "onStop" then
    MsgBox  "Started at " + CStr(Clock1.StartedAt)
  end if
  if n = "onAlarm" then
    CRLF = Chr(13) + Chr(10)
    sMsg = "Alarm " + Clock1.Time + CRLF + "Continue?"
    i = MsgBox(sMsg, vbYesNo, "Clock")
    if i = vbNo then Clock1.Alarm 0
  end if
</SCRIPT>

<SCRIPT LANGUAGE="VBScript">
Sub Button1_Click()
  if Button1.Caption = "Stop" Then
    Clock1.Enable(0)
    Button1.Caption = "Start"
  else
    Clock1.Enable(1)
    Button1.Caption = "Stop"
  end if
End Sub
</SCRIPT>

<p>
<OBJECT ID="Clock1" WIDTH=100 HEIGHT=70 align="bottom"
  type="text/x-scriptlet" DATA="clock.htm">
</OBJECT>
</p>

<OBJECT ID="Button1" WIDTH=96 HEIGHT=32
   CLASSID="CLSID:D7053240-CE69-11CD-A777-00DD01143C57">
   <PARAM NAME="Caption" VALUE="Stop">
</OBJECT>
</body>
</html>

During the onload event it assigns the clock colors. By clicking on the button you can stop or restart the clock.

Adding an Alarm to the Clock Container Page

Our clock page as it stands doesn't do very much, so we've added a function that simulates an alarm-clock. We can use several of the other methods and events that our Clock component has built into it. By assigning an interval in milliseconds using the Alarm method, like this:

Clock1.Alarm 5000

we are notified of a specific onAlarm event after that period expires (in this case, 5 seconds). Our Clock component provides this custom event via the onScriptletEvent in the container page. The Alarm method works by setting an interval:

function Alarm(secs) {
  if (secs)
    mAlarm = window.setInterval("DoAlarm", secs);
  else
    window.clearInterval(mAlarm);
  return 1;
}

To cancel the alarm, we simply pass zero to the Alarm method, which clears the interval timer. When that interval is up, the DoAlarm routine in our component is executed. This routine raises an event that can be detected in our page:

Function DoAlarm
  if (InScriptlet And (Not window.external.frozen)) then
     window.external.raiseEvent "onAlarm", window.document
  end if
End Function

Notice that our code checks both that we are actually running the component as a Scriptlet, and that the container page is ready to receive events, first.


A Few Gotchas

Before we go on to discuss how we can host Scriptlets inside desktop applications rather than Web pages, there are a few 'gotchas' to be aware of when developing Scriptlets. Some that almost drove us crazy are:

  • How to resize the Scriptlet area (or site).

  • How to cancel a Scriptlet event.

  • How to use hidden Scriptlets.

  • How to match in with the host page's environment

Resizing the Scriptlet's Site

The HTML code in our Clock component begins with:

<HTML ID=MyPage>

Why would we want an HTML page to have an ID, you might ask? It's because we then can access the frame window of the Scriptlet. This window (as you'll recall from the earlier section "What happens behind the scenes") is the second Internet Explorer_Server window. Once we can get a reference to it, we can change its size:

Set s = MyPage.style
s.pixelHeight = s.pixelHeight + mVertOffset
s.pixelWidth = s.pixelWidth + mHorzOffset

To change the height and width of a Scriptlet site, we need to access the style object of the HTML page itself, as shown above. Once we've got a reference to this style object (remember that the page represents the Scriptlet as a whole), it's easy to access and modify the pixelHeight and pixelWidth properties. If we don't define an explicit ID for the page, we're unable to do this.

Canceling Scriptlet Events

Custom Scriptlet events are a wonderful invention, but they don't return a value that can be examined to cancel the event—so you have to do your best to work around it. (For standard bubbled events, however, we can return a value via the event object).

A simple trick is to define a write-only property, say CancelEvent, then set it to true from the host page's script code if we need the Scriptlet to stop executing the rest of its code. Suppose that in the Scriptlet we raise an event before accomplishing a given task:

function put_CancelEvent(b) {
  mCancelEvent = b;
  return 1;
}
...
window.external.raiseEvent "onBeforeSomething", 0
if Not mCancelEvent then
  ...
  ... do something
  ...
end if
...

The member variable mCancelEvent is the internal buffer that represents the CancelEvent property. The onBeforeSomething event reaches the host environment before the Scriptlet performs the "do something" actions. Here, the host page can cancel the event by just setting the CancelEvent property to true:

<script language="VBScript" for="Scriptlet1" event="onscriptletevent(n,o)>
  If n = "onBeforeSomething" Then
    Scriptlet1.CancelEvent = True
  End If
</script>

Hidden Scriptlets

Scriptlets are code components that resemble ActiveX controls. Consequently, there might be situations where we would use a Scriptlet as a silent and invisible 'server'. For instance, think of the Clock sample we discussed earlier. In some situations, we might want to hide it and just exploit its alarm function. We don't want the Scriptlet to have a user interface, but we still need to use it as a regular object. The easy way out is to set the visibility property of its style object to hidden:

<object id=Scriptlet1 width=1 height=1 type="text/x-scriptlet"
   data="scriptlet.htm" style="visibility:hidden">
</object>

This technique can be used to add an invisible Scriptlet to any HTML page. If you just need a hidden component, that's all you need to do. However, sometimes things get a bit more complicated. For instance, we might want the object to appear and disappear on command. Look at the screenshot below:

You can run or download this page, RunClock.htm, from our Web site at http://rapid.wrox.co.uk/books/0707.

The page shows the existing Stop button, plus three new buttons related to the visibility of the clock. The Hide button removes and inserts the entire Scriptlet in the page, while the other two act to show or hide parts of the clock component in different ways, without affecting the container. We can take one of three possible approaches to hiding a Scriptlet dynamically:

  • Using the style.visibility property for the various objects.

  • Using the style.display property for the various objects.

  • Resizing its site.

Changing the style.visibility Property

visibility is a property of the style object, and applies to every element that can appear in an HTML page. To make an element visible we assign "visible" to the property, otherwise we hide it using "Hidden". We don't necessarily need to do it in the host page, as shown earlier. We can teach our Scriptlet to change the flag itself. To demonstrate this, we added a new SetVisibility method to the Clock sample, which runs when the Hidden button is clicked:

function SetVisibility(b) {
  if(b)
    document.all("Table1").style.setAttribute("visibility", "visible");
  else
    document.all("Table1").style.setAttribute("visibility", "hidden");
  return 1;
}

It works on the table element that actually implements the clock. The table has been assigned an ID of Table1:

<TABLE id="Table1">
...
</TABLE>

The function retrieves the style object of the element and changes the value of the visibility attribute according to the Boolean parameter passed to the routine. The effect of hiding it is shown below:

As you can see, only the table with the clock in it has disappeared. The text label and the rest of the Sciptlet's page remain unchanged. This happens by design, because visibility doesn't free the space occupied by the element it actually hides.

Changing the style.display Property

The display property, on the other hand, does just this. Except for the arguments it accepts, display works the same as visibility. The only difference is that it physically removes the element from the Scriptlet document:

Notice in this screenshot that the label shifts upwards when we remove the clock. The display property tells IE4 whether the element should be rendered or not—it expects the value "None" for removing the element, and an empty string (the default) for rendering it. We've implemented this capability in the SetDisplay routine that runs when the Display button is clicked:

function SetDisplay(b) {
  if (b)
    document.all("Table1").style.setAttribute("display", "");
  else
    document.all("Table1").style.setAttribute("display", "none");
  return 1;
}

At this point, you might think that hiding the entire Scriptlet from the host page is an insignificant task. You're wrong. Yes, you can access the display property of the whole Scriptlet like this:

document.body.style.setAttribute("display", "none");

or get the host page to change the Scriptlet's display property, like this:

Scriptlet1.style.setAttribute("display", "none");

Resizing the Scriptlet

Both the methods we saw above make the Scriptlet completely invisible. However, the Scriptlet's area of the host page is not always properly cleared—particularly in the case of the first method. And if we want to do the job from code within the Scriptlet component, this is the method we have to use. If we want the clock to disappear leaving no footprints, we may need to resize its window as well. This causes the container page to redraw itself properly, as if we had instantiated an invisible object:

Here's the code for the Hide button, that implements this technique.:

function Show(b) {
 if (b) {
   MyPage.style.pixelHeight = mHeight;
   MyPage.style.pixelWidth = mWidth;
   document.body.style.setAttribute("display", "");
 }
 else {
   document.body.style.setAttribute("display", "none");
   mHeight = MyPage.style.pixelHeight;
   mWidth = MyPage.style.pixelWidth;
   MyPage.style.pixelHeight = 1;
   MyPage.style.pixelWidth = 1;
 }
 return 1;
}

To access the style object for resizing the Scriptlet, we need to specify the ID of the Scriptlet page (MyPage) as we saw earlier. This code assigns the current height and width to global member variables, then resizes the site to1 x 1 pixels (using zero causes the Scriptlet object to be invalidated). It can be resized back to the original size using these stored values.

Matching in with the Host Page's Environment

A Scriptlet can access its parent (host page) environment, and read—say—the background color of the page in order to insert itself neatly in the host frame. The parent window object is available from the window.parent property. So, in order to adopt the parent's background color, the Scriptlet can just use the following code, probably in the window_onload event:

document.bgColor = window.Parent.document.bgColor

In fact, this technique can also be employed to inherit stylesheet information from the parent, or any other exposed properties of the host document.


Hosting Scriptlets in Desktop Applications

Scriptlets behave in a very similar way to ActiveX controls in a Web page, because—as we saw earlier—they are wrapped up in another software layer that exposes a COM interface. This wrapper is Internet Explorer_Server, otherwise known as the Microsoft Scriptlet Control. This also holds true when we start to consider the use we can make of Scriptlets in ordinary desktop applications. A Scriptlet can be hosted in a Visual Basic form or in an MFC-based application, just as easily as it can in an HTML page.

The Microsoft Scriptlet Control, which comes with IE 4, is responsible for this language-independence. It means that, outside a Web page, we can use Scriptlets everywhere that ActiveX controls are accepted. The Scriptlet Control is wrapped around the Scriptlet in the same way as in a Web page, and its ActiveX (COM) interface works just the same in VB, C++, Office 97, Delphi, and many other languages. The proof is in the screenshot below, which demonstrates that even from a VB Form, we find the Internet Explorer_Server window in the middle again!


Using the Scriptlet Control

In Visual Basic or MFC applications, you use the same Clock and the ScrollImage Scriptlets we saw earlier, as ActiveX controls. The only real difference is that in MFC applications—like JavaScript and JScript—the property and method names are case sensitive. Let's see what the control has to offer to programmers.

The illustration above shows how the Scriptlet Control presents itself from within the Visual Basic 5 Object Browser. The embed property isn't implemented yet, and event points to an object that hangs off the scriptlet control, and describes the events that occur in much the same way as IE4's window.event object. The scrollbar property plays the role we discussed earlier, when talking about the ambient properties that a Scriptlet document can inherit from its container. A few words, however, must be said about the readyState property.

Managing 'State Readiness' in Scriptlets

Making calls from an application into an object before it is declared to be ready is dangerously unsafe. If we're unlucky, it's easy to get a system error due to un-handled page faults. In other cases, our commands are simply ignored. The Scriplet container's read-only readyState property indicates the state of the Scriptlet HTML page. While IE 4 traps all exceptions, checking for the correct readyState becomes a must when using Scriptlets (and all Dynamic HTML-based code) from within a desktop environment.

An object passes through different states: uninitialized, loading, complete. Each time the property readyState changes, the container is notified with an onReadyStateChange event. Usually this event is fired multiple times while the Scriptlet is loading. The final time it denotes that the Scriptlet HTML page is fully loaded and we can start invoking script code safely:

Sub Scriptlet1_onreadystatechange()
  If Scriptlet1.readyState = 4 Then
    Scriptlet1.BgColor = "black"
    Scriptlet1.FgColor = "lightgreen"
  End If
End Sub

When the loading completed, readyState contains a value of 4. At this point, for example, we can set the colors.

Specifying the URL

Inserting Scriptlets into desktop application poses another problem: how do we specify the source path of the component? The Microsoft Scriptlet Control doesn't seem to offer a way to specify a relative path to the Scriptlet code. For example, we can't just indicate the file name, and leave the system to search in the current directory or in the common paths. Absolute paths work well if we're using Scriptlets from the Internet (when the paths are real URLs), but force us to indicate a specific installation directory in the case of local Scriptlets.

All this wouldn't be a problem if the URL property could be set at runtime. Unfortunately, we need to assign the URL at design-time, and if we leave the URL field blank, the Scriptlet Control doesn't load. If we specify a string that IE 4 isn't able to convert into a valid file name, we get a runtime exception and—again—the control won't load.

A possible partial workaround is to assign a semi-relative path that refers to the current drive, but specifies a fixed directory. It might be a common path where you store all the local Scriptlets. For example:

URL = file:///Script\Clock.htm

points to the file Clock.htm that must reside in the \Script directory on the current drive.

If your goal is to use Scriptlets as ordinary ActiveX controls within a desktop Windows application, then consider that a Scriptlet still remains a separate file that you have to distribute together with the application executables. A revolutionary approach may be saving the HTML file (and all the files it requires) in the resource file of the module. When the application is initializing, extract the resources and create temporary files. At this point specifying a fixed directory is no longer an issue, since you can create it dynamically and copy the Scriptlet files in.


An Example in Visual Basic

This example uses the Clock Scriptlet component we created earlier in the chapter, within a normal Visual Basic application—and proves that our Scriptlets can behave just like ordinary ActiveX controls. In Design view, the VB window looks like this. You can see the Scriptlets control in the Toolbox, and the Clock component on the Form:

The source code for this project is in the Scriptlets/Vbslet folder of the samples that you can download from our Web site at http://rapid.wrox.co.uk/books/0707.

What we need to do is

  • Insert the Scriptlet ActiveX control

  • Assign it an area (or site) on the form.

  • Set the URL name in the Properties dialog

  • Type in some code to handle the onReadyStateChange event

  • Initialize the Scriptlet as required

  • Add the various controls to the Form, such as buttons, list boxes, textboxes, etc

  • Write the rest of the code to link the Form controls and the Scriptlet together

If the Scriptlet requires code to be run when it loads, we do this in the onReadyStateChange event rather than in the Form_Load event. Until the control's readyState property is equal to 4 (i.e. loading is complete), it's unsafe to execute code within the Scriptlet because is in an inconsistent state.

The next screenshot shows the final application running, and you'll recognize the Clock component:

The application behaves like the Clock HTML page we saw earlier. In particular, it contains a button that sets an alarm in seconds, and changes the background color of the corresponding textbox to emphasize this. We multiply by 1000 since the Clock component's method expects its input in milliseconds:

Scriptlet1.Alarm (Val(Text1.Text) * 1000)
Text1.BackColor = &HC0C0C0

The alarm originates regular events that are handled in onScriptletEvent within the VB code:

Sub Scriptlet1_onscriptletevent(ByVal name As String, ByVal eventData As Variant)
  If name = "OnAlarm" Then
    Scriptlet1.FgColor = "red"
    Beep
    res = MsgBox("Would you stop the alarm?", _
          vbExclamation Or vbYesNo, "Clock Alarm!")
    If res = vbYes Then
       Scriptlet1.Alarm 0
       Text1.BackColor = &HFFFFFF
    End If
    Scriptlet1.FgColor = "lightgreen"
  End If
End Sub

In addition, the application shows the current location of the Scriptlet, and allows you to stop the clock and set a label.


Summary

Most seasoned HTML authors have been looking for a more versatile and flexible scripting language for some time. ActiveX controls and Java applets were fine, but often required long downloads and sometimes local installation. there could often be a delay while support files for components written in C++ or VB were installed. Scriptlets provide a way round all this, with small files that download quickly and perform relatively fast.

Many people are also coming to prefer the HTML metaphor to something like Microsoft PowerPoint, or other presentation software. In particular, Dynamic HTML provides lots of new ways of producing animated and exciting presentations. Scriptlets offer that extra something that finally makes the process of creating all kinds of pages so much easier where special custom effects are required. We just create a Scriptlet component to provide the effect we want, and can use it in a Web page, a PowerPoint presentation, a custom application, or even drop it into a Microsoft Word document. Scriptlets are HTML components that can fully exploit the power of Dynamic HTML, and offer an excellent way to put script reusability into practice.

It's difficult to say now if Scriptlets will be widely accepted by the programming community in the months to come. We've consciously used the word "programming" instead of "Web developing", because there is a good chance that Scriptlets will soon populate and animate the Windows 98 desktops and folders as well as appear in some new applications. As for the Internet, don't forget that today Microsoft's implementation of Dynamic HTML – on which Scriptlets are based – is supported only by IE 4, and only under Win32. This will change soon, maybe by the time you read this, but it's unlikely that Dynamic HTML will be supported by Netscape or other browsers in its present form. Like it or not, this will delay the acceptance of Scriptlets. On the other hand, we have the Active Desktop and Windows 98, which offer an integrated environment and put the Web browsing objects (and hence the browser metaphor) everywhere. It's likely that this, more than the Web side, will determine the success of Dynamic HTML and Scriptlets.

In any case, in this chapter, we've shown you what Scriptlets are, and what they can do. There's no doubt you'll agree that they are a compelling new technology, and one that can only make IE4 developer's lives that bit easier.


Professional Visual Basic 5.0 Business Objects

Published by
Wrox Press Ltd.
30 Lincoln Road
Olton, Birmingham, B27 6PA

Printed in Canada

Copyright © 1997 by Wrox Press

All rights reserved. No part of the contents of this book may be reproduced, stored in a retrieval system or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording or otherwise, without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

The author and publisher have made every effort in the preparation of this book to ensure the accuracy of the information. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Wrox Press nor its dealers or distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book.

ISBN 1-861000-43-X

Trademark Acknowledgements

Wrox has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Wrox cannot guarantee the accuracy of this information.

Credits

Author
Rockford Lhotka

Editors
Tim Briggs
Dominic Shakeshaft

Technical Reviewers
Allan Emery
Mike Erickson
Brian Francis
Richard Grimes
Christian Gross
John Harris
Mark Harrison
Steve Jakab

Development Editors
Jesse Liberty
Greg Peele
John Smiley
Mike Sussman
Dorai Thodla

Design/Layout
Graham Butler

Copy Edit/Index
Simon Gilks


Chapter 1: Introducing Business Objects

Overview

In the introduction, I presented the rationale for this book, which is that object-oriented design, business objects, and Visual Basic 5 allow us to develop highly scalable and reusable code. In this first chapter, we'll look at these concepts and the arguments for them in more detail. Specifically, we'll review:

  • Defining objects

  • Defining business objects

  • Using Visual Basic 5.0 to create objects

  • Re-using code with components

  • Business objects in action - application types

  • Object-oriented principles in Visual Basic 5.0


What are Business Objects?

This book is about developing applications using business objects. So the first thing we need to do is define how we'll be using the term business objects. Since this term means different things to different people, we need to make sure that we're all talking about the same thing before proceeding any further.

Just looking at the words, you might decide that these are objects that reflect or represent your business. And you'd pretty much be right at a high level, it really is that simple. The programs we write can be viewed as models of the real world. We create programs to simulate parts of the world around us - to record what's happening or to make predications about what might happen.


What is an Object?

To understand business objects, we need to understand the concept of an object. An object is a code-based abstraction of a real-world entity or relationship. In the computer, an object consists of some data (state information) and behavior (a set of routines).Our programs use an interface to get access to an object's data andbehavior. We can represent the object as something like thefollowing diagram:


Objects and Classes

All objects are defined by a class. A class is essentially a template from which the object is created, which means that we can create many objects based on a single class. Each object is referred to as an instance of the class.

The word class is pretty descriptive, since we're basically classifying our objects. For instance, if we have a couple of objects, Squirrel and Rabbit, they could be instances of a Mammal class.

To use a more business-like example, we may have a Customer class. From this class we could create instances to represent each our customers, such as Fred Smith and Mary Jones. Each actual customer would have its own object, but all the objects would be created from the same template, the Customer class:

In Visual Basic, we define classes using class modules. We can then create objects based on the class module, with each object being an instance of the class.

Class modules are like form or standard modules, in that they have a declaration section followed by a series of Sub, Function and Property subroutines. The difference is that the variables and code in a class module can only be used by creating an instance of the class.

Let's take a look at how Visual Basic's class modules are used to provide a template for our objects.

Objects Contain Data

All objects contain data about themselves. For instance, an object representing a simple cardboard box might have data about the box's height, width, and depth. The object might also know whether the box is open or closed. Furthermore, we might have many box objects, each with its own height, width and depth. Each of these objects could be an instance of a Box class.

Data values in objects are stored in instance variables. Each instance of an object has its own set of variables, separate from the variables of any other object, even those of the same class. In this way, two objects of the same class can each have their own individual data, so each of our earlier box objects can have its own height, width, and depth.

In Visual Basic, a class's instance variables are stored as module-level variables inside a class module. For example:

Option Explicit

Private dblHeight As Double
Private dblWidth As Double
Private dblDepth As Double
Private blnOpen As Boolean

As with any other module, module-level variables in a class module can be declared as either Public or Private.

Private variables are only accessible to code within the class module, and they are the preferable way to manage data within objects. For any client code to manipulate a Private variable, it must call our object's code to do the manipulation; it can't do anything directly. This gives us a lot of control, since we can implement business or validation rules against any values that the client code attempts to change.

Instance variables declared as Public are accessible to code within our object, but they are also directly accessible to any client code written to use our object. Use of Public instance variables is very bad practice. Any client code can directly manipulate a Public instance variable, without going through any of our object's code. This means our object gives up all control over the value, trusting the author of the client code not to break any rules or do anything that is invalid.

One of the primary principles of object design is encapsulation, a concept we'll explore throughout the rest of the book. Encapsulation means that objects should hide their internal design and data from the code that uses them.

Private variables help by forcing any client code to use our code to change our object's data, thus allowing us to change how the object is implemented at any time with little or no impact on other programs that use this object. Public variables directly break the principal of encapsulation, providing a client with direct and uncontrolled access to an object's data.

Objects Have Behaviors

Of course, the whole idea behind having an object is so our programs can interact with it. If we're encapsulating an object's data then we need to provide the ability for the client programmer to access the information that the object holds.

Objects need to provide an interface to allow their client programs to gain access to these services. Just as in the real world, where objects interact - it is our aim in programming with objects to facilitate and control their interaction - via properties, methods and events. These form the interface the object presents to the world.

In Visual Basic, an object's interface is composed of Property, Sub and Function routines, as well as any events declared using the Event keyword. Any of these routines declared as Public within the class module become part of the object's interface.

In reality, an object can have many interfaces, allowing client code to use the object in different ways as appropriate. We'll cover this in more detail in the next section.

Properties

A property is an attribute that describes the object. Objects often have many properties to provide client code with access to all of their attributes.

It's important to make that an object's properties are not the same as its data. Objects often contain data that would not be considered to be an attribute of the object. For instance, a Person object may have an Age property, but the underlying data might be a birth date. The birth date itself may or may not be available as a property of the object.

Properties allow us, as the object's designer, to pick and choose what information we want to make available about our object. We can also choose which property values can be changed by our object's users. Some of that information may come directly from our object's data, while other information may come from calculated values or other sources.

So in our earlier box example we may not care to let other programs see the box's dimensions, but we may want them to be able to get at the box's volume:

Public Property Get Volume() As Double
  Volume = dblHeight * dblWidth * dblDepth
End Property

Visual Basic provides us with three different types of Property routines. Property Get routines are used to retrieve a property from an object. Then there are two routines to put values into a property, depending on whether the value is a reference to an object or not. Property Let routines allow client code to put any value into a property other than a reference to an object, while Property Set routines are used only for object references.

Properties can be read-only (Property Get), write-only (Property Let/Set) or read-write (both properties). Even if we include a Property Let or Property Set routine for a property, we can include code in that routine to validate the new value or check any other business rules before allowing the client code to change the value.

Methods

Objects, like their real-world counterparts, need to provide services (or functions) when they interact. Using their own data, or data passed as parameters to the method, they manipulate information to yield a result.

Methods are simply routines that we code within the class to implement the services we want to provide. Some methods return values or provide information back to the calling code. These are called interrogative methods. Others, called imperative methods, just perform a service and return nothing to the calling code.

In Visual Basic, methods are implemented using Sub or Function routines within the class module that defines our object. Sub routines may accept parameters, but they do not return any result value when they are complete. Function routines can also accept parameters, and they always generate a result value that can be used by the calling code.

The difference between a Function routine and a Property Get routine is quite subtle. Both return a value to the calling code and, either way, our object is running a subroutine defined by our class module to return the value.

The difference is less programmatic than it is a design choice. We could create all our objects without any Property routines at all, just using methods for all interactions with the object. However, Property routines are obviously attributes of the object, while a Function might be an attribute or a method. By carefully implementing all attributes as Property Get routines, and any interrogative methods as Function routines, we will create more readable and understandable code.

Using our now familiar box example, programs may need to open or close the box - so we need to provide a way to do this:

Public Sub OpenBox()
  blnOpen = True
End Sub

Public Sub CloseBox()
  blnOpen = False
End Sub

With this approach, our client code might look like this:

  Dim objBox As New Box

  objBox.Open
  objBox.Close

Where this gets more useful is when we add code to check our rules. We shouldn't be able to open an already open box, or close an already closed box. With absolutely no impact to our client code, we can add the following rules to the Box class:

Public Sub OpenBox()
  If Not blnOpen Then Err.Raise vbObjectError, _
    "Box already open"
  blnOpen = True
End Sub

Public Sub CloseBox()
  If Not blnOpen Then Err.Raise vbObjectError, _
    "Box already closed"
  blnOpen = False
End Sub

In this case, we're simply raising an error if the client code tries to call either the OpenBox method when the box is already open or the CloseBox method when it's already closed. The real key, here, is that the implementation of these methods is totally encapsulated within our class module. We were able to make these changes without having to make any changes to our client code.

Events

In object-oriented design, the concept of an event is pervasive. In OO parlance, objects act in response to events, such as a user providing a value, or another object calling one of our object's methods. In the Visual Basic 5.0 environment, we have access to a somewhat different type of event.

Any Visual Basic developer is used to writing code to respond to events. Controls on forms generate events continually, and we write code behind those events to take appropriate action. For instance, a CommandButton control provides a Click event, firing it any time the user clicks on the button. We then write code to respond appropriately:

Private Sub Command1_Click()
  MsgBox "The user clicked the button"
End Sub

Before Visual Basic 5.0, there was no way for a Visual Basic developer to cause events, only to respond to them. Now we can use the Event statement to declare an event and the RaiseEvent command to fire it off.

Going back to our Box class, let's allow the user to stretch our box by implementing a Stretch method:

Public Sub Stretch(Percentage As Double)
  dblHeight = dblHeight * Percentage
  dblWidth = dblWidth * Percentage
  dblDepth = dblDepth * Percentage
End Sub

As a convenience, perhaps we want to raise an event to let any client programs know that our box has a new size. Visual Basic makes this very easy. We just need to add an Event declaration in the (General) (Declarations) section of our Box class module:

Event Stretched()

Events are always Public in scope, since they're always raised back to whatever client code has a reference to our object.

With our event declared, we can now fire it off using the RaiseEvent command:

Public Sub Stretch(Percentage As Double)
  dblHeight = dblHeight * Percentage
  dblWidth = dblWidth * Percentage
  dblDepth = dblDepth * Percentage
  RaiseEvent Stretched
End Sub

We'll go into much more detail about events in Chapter 3.

Multiple Interfaces

So far, we've looked at an object as having a single interface composed of its Public Sub, Function, and Property routines, along with any events it has declared. With Visual Basic 5.0, things are a bit more complex. Our objects can actually have many different interfaces simultaneously, although a client can only use one at a time.

If we create an object from a class we built in Visual Basic, that object will have at least the one interface we defined in our class module. In this simple case, it appears as though our program has direct access to the object, although in reality we're getting at the object through an interface defined by the class:

With Visual Basic 5.0, a client program never has direct access to an object, only to one of the object's interfaces.

Sometimes, one interface isn't enough. Our object may represent more than one real world entity, and we need to be able to model this in Visual Basic. For instance, a Customer object is great, but a real customer is also a person. To model this in our application, we might need our Customer object to also act like a Person object from time to time.

To accomplish this, we can just add the Person interface to our object. Then a client program can use our object through the Customer interface with its set of properties and methods. It might also use the object through the Person interface, with its own separate set of properties and methods. This way, the client can use the object in whichever way is appropriate at any given time:

As we've seen, when we create a class in Visual Basic, its Public members comprise the interface for our object. However, we can also use the Implements statement to add another interface to our object. The Implements keyword requires that we supply a class name with which it will work.

With our Box example, for instance, we might also include an interface for a more generic Container class. To do this, we'd need to add an Implements statement in the general declarations section of our Box class module:

Implements Container

As soon as we add this line, Visual Basic will require that we add code to our class module to implement all the Public properties and methods from the Container class. Suppose the Container class module contains a single method, called PutIn, and a property, called IsOpen:

Public Sub PutIn(Item As Object)

End Sub

Public Property Get IsOpen() As Boolean

End Property

The property and method may or may not have code in them. As I've shown them in this example, there's no code. It doesn't really matter to our Box class, since we need to add our own code in the Box class module regardless. For instance, the Box class's implementation of PutIn may add any new items into a Private collection variable, and IsOpen may just return our blnOpen flag:

Option Explicit
Implements Container

Private colItems As New Collection
Private dblHeight As Double
Private dblWidth As Double
Private dblDepth As Double
Private blnOpen As Boolean

Public Sub OpenBox()
  If Not blnOpen Then Err.Raise vbObjectError, _
    "Box already open"
  blnOpen = True
End Sub

Public Sub CloseBox()
  If Not blnOpen Then Err.Raise vbObjectError, _
    "Box already closed"
  blnOpen = False
End Sub

Private Sub Container_PutIn(Item As Object)
  colItems.Add Item
End Sub

Private Property Get Container_IsOpen() As Boolean
  Container_IsOpen = blnOpen
End Property

There are a couple interesting things about these new routines we've added. First off, they are declared as Private routines, so at first glance they don't appear to be available to any client code. This is deceptive, however, since they are available to clients through the Container interface.

If these routines were declared as Public, they'd be part of the Box interface. That's not what we are after though. Instead of declaring them as Public, we declare them as Private, and then indicate that they belong to the Container interface by putting Container_ in front of the name of the routine.

We'll discuss multiple interfaces in more detail later in the book.


Business Objects

The world around us is made up of both physical objects and concepts. To effectively model or simulate the real world and human processes in our code, it would be good if our software could be made up of representations of these real-world objects. As we have seen in the previous section, object-oriented programs attempt to do just that.

Business objects are designed to represent real 'objects' and concepts in our business. Here are some examples of these real 'objects':

  • An employee

  • A purchased part

  • A manufactured part

  • A work order

  • A customer

All of these are objects in the physical world, and the whole idea behind using software business objects is to create representations of these same objects inside our applications. Then our application can make these objects interact with each other just as they would in the physical world. For instance, an employee might create a work order to build a manufactured part from one or more purchased parts that are then sold to the customer.

Following this logic, we can create a work order business object that contains all the code it needs to manage itself - so we never need to replicate code to create work orders, we just use the object.

Similarly, a customer object contains and manages its own data. A well-designed customer object can contain all the data and routines needed to represent a customer throughout an entire business and this can be used across all the applications for that business. We may use our customer object when taking an order. We might later use it when sending out a bulk mailing and analyzing sales patterns - without affecting any of the code that uses the object to take an order. Of course, over time, the customer object may evolve: it may gain new properties or new methods to support more functionality; the point is that the previous properties and methods can and should remain the same, so that existing applications need not be changed.


Why Use Business Objects?

Having defined the essence of a business object, let's look at the reasons we might want to use them.


Modeling Our Business

Of course, the reason we're developing applications in the first place is to solve some problem or automate some process. Having a set of abstract objects in software that represent physical objects can simplify this whole process.

As we'll see in Chapter 3, we can identify and design our objects in such a way that they provide a model of our business domain. With our initial identification of these business objects, we can then begin to model the business process we want to automate - or the problem we need to solve.

We can draw a picture showing how these objects relate or interact with each other in the real world. Using that picture, we can make our software objects work with each other in the same way. Essentially, we're defining relationships between our various business objects, showing how each object calls methods of other objects in order to simulate the real-world process.

This is a very intuitive approach to software development, since our applications become software models of the real world.

We've already started to do this with our description of an employee creating a work order and so forth. If we break that statement down into pieces, we can see a number of similar interactions between these objects:

I'll demonstrate the whole process of creating and modeling objects in Chapter 3, when we get into some more specific examples.


Code Reuse

Most applications developed today are essentially built from scratch. Over and over, we recreate many of the same processes and procedures from earlier programs. Granted, many programmers copy code from existing programs into their new programs, and then modify the copied code to fit any new requirements. But this means that the same code is copied over and over again. If we need to change something in the original code, we'll also have to find and change that same code everywhere it's been used. Which is far from ideal.

You've probably done this type of code reuse many times over. And you've probably spent countless hours going through dozens of programs making the same code change. Then you've spent hours debugging all the changed programs. Talk about expensive!

In other situations, code reuse is managed through libraries of code, or maybe even precompiled routines. But even in these cases, code reuse is difficult. In one of the best environments I've ever worked, we had a large and complete library of precompiled routines. By using these routines, we were able to dramatically improve the productivity of our development team.

Still, it wasn't uncommon to run into a program that required a change to our library. Unfortunately, a change to the library required modifying and rebuilding all the applications that relied on the library. Worse yet, sometimes the new functionality required actually going back and changing all the programs that used the library to bring them up to date. This was expensive, and required us to modify what was otherwise stable program code, thereby increasing the risk of bugs.

So copying code helps, but it's far from ideal. Creating libraries of code, or pre-compiled routines, can help a lot; but both techniques still leave a lot to be desired. What we really need is a solution that allows code reuse yet retains the flexibility of selective changes based on the need of the particular application. It would be great if we could derive what we needed and modify or extend the behavior. Fortunately, objects provide just this capability.


Creating Building Blocks

Traditional programs are centered around the processes and procedures that the program is trying to model. Object-oriented programs, on the other hand, are centered around the real-world objects that the program is trying to model. Once these objects have been identified and modeled, we can write programs that use these objects to simulate the processes and procedures in a much simpler and cleaner way than other programming approaches would accomplish.

If we base our applications on business objects then we'll create a collection of reusable building blocks that we can use time and time again in applications throughout our business. And, perhaps more importantly, we can continue to change and evolve our objects, over time, with little or no impact on the older applications that are already using those objects.


What are Components?

If you've followed the software industry for long then you'll have realized that object-oriented technology is not exactly new any more. In fact, it's been around for many years; but only now is it really becoming a popular concept in business. Why is that?

There are probably a lot of different reasons, but there is one in particular that we need to explore. As we discussed earlier in the chapter, a lot of the impetus for new development techniques is to facilitate code reuse. Unfortunately, just creating objects in code is not enough to maximize reuse.

Until recently, if we had the code for a Customer object in one program, and we wanted to use that object in another application, we'd have to copy the code into our application. We got to reuse the code, but, if anything changed, we had to find and change each copy of the customer object in all the different applications where it could be in use.

The problem is that as soon as

there are two or more different copies of the same code, there's

no easy way to keep them all synchronized. In many ways, we're back where we started - with simple copy-paste code reuse. At best, we've simply come up with a new way to create source code libraries:

This is where component-oriented design comes in. Components build on object technologies to provide a better way of achieving reuse of our objects.

The term component is one of those overloaded words that has a different meaning to just about anyone you'd care to ask. Even within the OO community, there's a great deal of disagreement about the meaning of the term. Many people would argue that a component is nothing more than a large or complex business object.

In fact, the terms 'object' and 'component' are often used interchangeably, but there's a distinct and important difference between them:

  • Objects are created from classes. Classes are made up of source code that defines the object's data and the routines that are needed to manipulate that data.

  • Components are precompiled binary code. Since components are precompiled, they are independent of the language in which they were created.

Since we're using a Microsoft development tool to create software for Microsoft operating systems, we'll stick with Microsoft's definition of a component. In their view, components are ActiveX servers or ActiveX controls. They are precompiled units of code that expose one or more interfaces for use by client programs:

Even within the Microsoft world you'll find some confusion. To many people, ActiveX controls and components are synonymous. In reality, however, ActiveX controls are merely one type of component, along with various other types of ActiveX servers. We'll look at all these different types of components in the next section.

With binary components, we can achieve a whole new level of reuse in our applications. Going back to our Customer object, we can put the code into our component rather than into each individual application. The applications can then use the object directly from the component:

We get the same functionality as when we copied the source code into each application, but now we only need to maintain that code in a single place - the component.


Component Interfaces

Basically, what we're saying is that a component is a pre-compiled group of objects. Some of these objects can be used by client programs, while others may be hidden from the outside world, available for use only within the component itself. In many ways, a component is like a small application, but one that makes its functionality available for use by other applications.

Earlier, we discussed object interfaces and how they are made up of the object's Public properties, methods, and events. Since a component is really a group of objects, the component's interface is made up of its public objects.

Of course, this implies that there is a way to indicate which of our objects are to be public and which are private. There's no real analog to this concept within traditional object-oriented design.

Object-oriented design always assumes that any object can interact with any other object within our application. Component-oriented design gives us greater control, since we can choose which of our component's objects are available for use by other applications:


Types of Components

Components can be designed for use in different areas within our programs. In general, there are three different types of components in an application:

  • Standard/general components

  • Application-specific components

  • Industry-specific components

Standard/General Components

I like to think of general components as the sort of thing you can buy out of just about any programmer's catalog. In fact, there are many components that are generally available from Windows itself or that come with Visual Basic. These are components that provide services so broadly useful that you can use them just about anywhere. Some good examples include DAO, RDO, graphing controls, grid controls and mathematical library components.

For the most part, we're better off purchasing general components rather than rolling our own. Most companies that sell components make their living off the sales, so the components are likely to be of pretty decent quality. Furthermore, just about any popular type of component will be developed by multiple vendors, so competition will tend to get us better quality and more features as well.

Of course, with Visual Basic 5.0, we can easily create components that are quite general in nature. If we can't find a component that fits our needs then we can just create our own. Better still, if we find a component that's close to what we need, we can use it inside a new Visual Basic component and just build on what it provides.

Application-Specific Components

Each application is unique, and provides some specific functionality. We can put all of those components that belong entirely to an application into the application-specific component category.

Perhaps our program needs to do some specific graphical display, or some complex calculations. These might be built as components - even though they aren't useful to any other applications.

Clearly, since we're not likely to find this type of component on the commercial market, we'll be writing our own. I'll touch on this concept here and there throughout the rest of the book, but the majority of these components will be controls to customize our interface - so they really aren't going to contain business objects.

Industry-Specific Components

These are components that are targeted at a specific industry, or perhaps a specific area of a business. As we'll discuss later in the chapter, there are a number of efforts underway to develop vertical market components, and those efforts are almost always industry-specific.

Vertical components are rather rarer in the commercial area. Still, if we look around we can find more and more components that fit into this area. Many standalone products are beginning to expose public object interfaces, so we can write our programs to use parts of the product as a

component. Products such as accounting systems, or shop floor control systems, can be used as components if they're designed to provide a public interface.

On the other hand, we may find that there simply aren't components for our particular industry to suit our needs. In that case, we'll have to develop our own components for use in our applications.

Business objects really come into play here. By using business objects to build a model of the actual 'objects' used in our industry, we can create very powerful components. It is primarily this type of component that we'll be focusing on throughout the remainder of the book.


Creating Business Components in Visual Basic

Hopefully, by now, you're beginning to get a solid feel for the benefits that business objects can offer. Business objects provide both code reuse and an understandable, manageable way to model our business processes in our software. With the ability to organize and group our business objects into precompiled binary components, we can fully realize the benefits of the technology.

Visual Basic allows us to create components in a variety of ways, and we can organize our business objects into these components. Using Visual Basic, we can create ActiveX servers, both in-process (DLL's) and out-of-process (EXE's). Additionally, we can create ActiveX controls (OCX's), which are another form of component. Any of these types of components can house our business objects, each with different strengths and weaknesses.


Out-of-Process Servers

An out-of-process server is a component that is effectively a separate program containing objects. This type of server will always run in its own process space, and will thus be largely independent from any client programs.

There are two main options with out-of-process servers. We can create the server so that each object runs in its own process, or we can create the server so that all the objects in the server run within a single process.

There are cases where our business objects will be running in a number of separate processes. The most notable scenario is where we have a full-blown application that can also be used as a component. We might also create out-of-process servers when we want to start some task and let it run in the background while the user continues to work with our application.

There is a cost to this approach however. Most applications require many different business objects to be active all at the same time and, if each object runs in its own process, we'll have a lot of processes running concurrently. Having a great many processes active on a workstation can be devastating to performance.

The Windows operating system does some work every quantum (20 milliseconds or so) to determine which process should get to run next. Even if a process is idle, it is still in the list of processes to check, and so the operating system has to do some extra work to check our idle process. If we have a lot of processes, this can add up and become a problem:

On the other hand, we can create a server where all of our objects exist inside a single process. If we put business objects inside this type of server, all of the objects can work together, sharing resources and memory. Many clients can use the objects in our server, and all the objects from all those clients can easily communicate and work with each other inside the same process.

Either type of out-of-process server suffers from one very significant drawback: performance.

All Windows components use the Component Object Model (COM) to allow communication with objects in other processes. We'll discuss COM in more detail later in the chapter. For now, all we need to know is that COM handles all communication between objects in different processes.

Unfortunately, COM imposes a very high overhead on each call from one process to another (cross-process calls). Therefore, on each reference to a property and each call to a method, we take the hit of this overhead - and performance can suffer substantially.

There are techniques that can be used to help minimize the impact of this overhead, and I'll demonstrate those later in the book.


In-process Servers

An in-process server is a component where our objects run inside the client's process. Each client essentially gets its own private copy of our component - and therefore a copy of all our objects.

In-process servers avoid the overhead of running in their own process, thus conserving resources and memory for the system. Additionally, the communication between the client and our objects does not have to go from one process to another - and so we avoid almost all of the overhead imposed by COM on out-of-process server communication.

Since in-process components run inside the client's process, we use less memory and we don't create extra processes. We'll also improve performance, but we do give up some capabilities. Since each server is running in each client's process space, our business objects are effectively split up - so they can't share resources or memory with each other, but only with each client.

By judicious use of out-of-process servers combined with in-process servers, we can create applications that draw on the strengths of each. I will demonstrate this at various points throughout the book.


ActiveX Controls

Finally, let's look at ActiveX controls. These are yet another type of component, but one that's typically used to create visual objects as part of an interface design. Some ActiveX controls are invisible; but if we get right down to it, there's very little difference between an invisible ActiveX control and a regular in-process server.

Business objects can be placed in an ActiveX control just as easily as they can be placed in an in-process server. Many developers may find it easier to work with objects packaged in this type of component, since they can literally drop the component on a form and work with it from there - just as if it were a regular control.

Also, ActiveX controls can be very powerful in a Web application. Microsoft's Internet Explorer will automatically download and install controls, but not in-process servers. This makes it far easier to include a control into a Web page than to include an in-process server.

One significant drawback to ActiveX controls is that they require our client programs to have at least one form that can be a container to hold the control. More than simply having to include the form in the project, we actually have to have the form loaded in memory in order to gain access to the objects inside the control. This is somewhat less efficient than getting the objects directly from an in-process server.


Fitting Business Objects into Applications

Now that we've established what business objects are, and we've explored the different types of components that can contain them, we need to look at just where the objects fit into our typical application. We'll discuss this in much more detail in Chapter 2, but it will be useful to take a quick, high-level look at some different application models, and where business objects fit into them.

For the client/server experts reading this, I should point out that we're really blurring the lines between the physical and logical models in this overview. In each case, I'm really proposing a 3-tier logical model; but it is useful to see where everything falls physically regardless. In Chapter 2, we'll be more precise, as we get into more detail on the architecture we'll use throughout the book.


Single-tier Applications

A single-tier application is the simplest of the models that we'll look at. This type of application is simply a program that runs on the user's machine. It might talk to a database, but that database resides on the same machine (or perhaps on a mapped network drive). The key thing, here, is that all the work takes place on the same machine: the user interface, business- and data-processing are all on the user's computer.

A diagram of this type of application would look like this:

Now let's look at where the business objects fit into the picture:

In this type of application, everything is pretty simple. Since our entire application is running on a single machine, the business objects just fit right into the application.

Don't worry about the data service objects just yet. We'll discuss what they are and what they do in Chapter 2. For now, let's just look at them as being the code that communicates between business objects and the database.


Two-tier Client/Server

The simplest type of distributed computing is two-tier client/server. In this type of application, the data processing is moved off the user's machine on to a dedicated database server machine. Two-tier client/server is the most common type of client/server application built today. It offers significant benefits over a simple application, since the processing of data is centralized and becomes a shared resource between all the users:

Still, with two-tier client/server, the only work that has really been moved off the user's machine is the data management - storing and retrieving data. The user-interface and any processing done by the application remain pretty much where they were:

The primary difference lies in the role of the data service objects. In this model, they have to do much less work, since a lot of the data processing is moved to the database server - usually in the form of stored procedures and triggers written in SQL. Again, we'll talk in more detail about these data service objects in Chapter 2.


Three-tier Client/Server

Over time it has become apparent that two-tier client/server is simply not powerful enough or flexible enough to handle many larger applications. Since each client workstation maintains a dialog with the central database server, the network traffic can be quite high. Furthermore, the central database server can become a performance bottleneck - as many users try to access the same resources at the same time.

Three-tier client/server helps address these issues by putting another physical server between the users and the database. This central application server can more efficiently manage network traffic and the load on the database server.

Better still, it is typically much easier to add more application servers than it is to add more database servers. Adding a

database server would require us to split our data between the database servers, whereas adding an application server should

just require some relatively minor changes to parts of our application:

Just like two-tier client/server, the data processing is handled primarily on the database server. However, we now have an application server that can do a lot of work. This means that we can move a substantial part of our application off the user's workstation and onto the application server.

There are a number of benefits to distributed processing, including:

  • Centralization of shared resources

  • Rich, flexible client interfaces to the data

  • Spreading our workload across various machines

  • Putting processing in the most efficient location possible

How much processing is moved to the application server may vary. It depends on many factors, but mostly we need to decide how much work we want our client workstations to do. Designs range from having the client workstation do virtually nothing to the client workstation doing almost everything. We'll discuss the pros and cons of each approach in Chapter 2.

As I've shown it in this diagram, much of our business logic remains on the user workstation. In Chapter 2, we'll explore why it can be very beneficial to have the business objects on the client workstation as shown here.


Web-based Applications

The models that I've presented so far leave a fair amount of the application on the client workstation. Applications designed for the World Wide Web take an entirely different approach. Web applications put as little of the application as is physically possible on the client, and keep all the processing centralized on one or more servers:

Since the browser will only provide a user-interface, the rest of our application needs to move down to the server:

In this figure, we still have an interface, our business objects, and our services. What we've done is moved virtually everything down to the server, leaving a very light user interface on the user's workstation.

The big difference is that instead of a user-interface on top of our business objects, we now have a web-interface. This is a layer of code that interacts with the web server to communicate with the client. The web-interface is basically a layer of 'glue' between the business objects and the HTML that gets sent to the user's browser.

The web interface provides HTML to the browser via the web server and accepts the user's input through the browser and web-server. There are many technologies available to build this web interface - including CGI, ISAPI, Active Server Pages and others.

There are some other fairly significant issues with this type of architecture. In Chapter 11, we'll implement a browser-based user-interface using Active Server Pages as the glue between the business objects and the web server.


Object-based vs. Object-oriented Approaches

Visual Basic has always been object-based, meaning that we could always use objects in our programs; but the language provided no way to create objects until Visual Basic 4.0.

Perhaps it would be more accurate to say that it has always been component-based, since, until Visual Basic 4.0, we've been unable to use objects per se but rather we've used pre-built binary components.

Originally, these components came in the form of VBX's (Visual Basic Custom Controls), which really set the stage for where we are today. VBX components provided us with pre-built functionality that we could just plug into our programs and start using. They were so useful, in fact, that an entire cottage industry grew up around them - and various development tools have now been enhanced to support their use.

Unfortunately, VBX technology was pretty limiting in a number of ways. VBX controls were difficult to create, and the way they communicated with Visual Basic was strict and somewhat limited. Because of these problems, Microsoft developed a new standard for component development. These new components were called OLE Controls, and are now called ActiveX controls. Either way, they are commonly known as OCX's (OLE Custom Controls).

Visual Basic 4.0 added a number of actual object-oriented abilities to the language, by adding the class module, and allowing us to create objects based on our own classes. Now, with version 5.0, we have even more capabilities with the addition of the Friend and Implements keywords, along with the ability to raise our own events - all of which we'll discuss in detail later in the book.

Visual Basic 4.0 also introduced the ability to create our own components. This has given us the ability to easily package our objects together into reusable binary modules, as we discussed earlier in the chapter.

Most people look at four criteria when deciding if a language is object-oriented. These criteria are abstraction, encapsulation, inheritance and polymorphism. Let's look briefly at each of these from Visual Basic's viewpoint. I don't want to get into too much detail, here, since there are entire books written about this subject alone; but I do want to quickly illustrate where Visual Basic stands.


Abstraction

Abstraction is the process by which we can think about specific properties or behaviors without thinking about a particular object that has those properties or behaviors. We know that most cars have four wheels, a steering and an engine, without thinking of any particular car, or how the internals of the engine operates.

Abstraction allows us to recognize how things are similar and ignore differences - to think in general terms, and not the specifics. A TextBox control is an abstraction, because we can place it on a form, and then tailor it to our needs by setting properties. Visual Basic allows us to define abstractions using class modules.

Any language that allows a developer to create a class from which objects can be instantiated meets this criteria, and Visual Basic is no exception. We can easily create a class to represent a car, essentially providing an abstraction. We can then create instances of that class, where each object can have its own attributes such that it represents a specific automobile.


Encapsulation

Encapsulation is the concept that an object should be a 'black box'. Another way to put this is that an object should totally separate its interface from its implementation.

This means that an object should completely contain any data it requires, and that it should also contain all the code required to manipulate that data. Programs should interact with our object through an interface, using properties and methods. Client code should never work directly with the data owned by the object.

In object-speak, programs interact with objects by sending messages to the object. These messages are generated by other objects, or by external sources such as the user. The way the object reacts to these messages is through methods. What Visual Basic calls properties would otherwise be called attributes.

Both Visual Basic 4.0 and 5.0 provide full support for encapsulation through class modules. Using these modules, we can create classes which entirely hide their internal data and code, providing a well-established interface of properties and methods to the outside world.

For example, if we create a class module and add the following code:

Option Explicit

Public Property Set Form(Target As Form)

End Property

Public Property Let CurrentX(X As Single)

End Property

Public Property Let CurrentY(Y As Single)

End Property

Then we've created an interface for the class. From here, we could do virtually anything in terms of implementing the class. For instance, we could just store the values:

Option Explicit

Private frmForm As Form
Private sngX As Single
Private sngY As Single

Public Property Set Form(Target As Form)
  Set frmForm = Target
End Property

Public Property Let CurrentX(X As Single)
  sngX = X
End Property

Public Property Let CurrentY(Y As Single)
  sngY = Y
End Property

Or maybe we just want to move to the coordinates:

Option Explicit

Private frmForm As Form

Public Property Set Form(Target As Form)
  Set sngForm = Target
End Property

Public Property Let CurrentX(X As Single)
  frmForm.CurrentX = X
End Property

Public Property Let CurrentY(Y As Single)
  frmForm.CurrentY = Y
End Property

Either way, we haven't changed the interface of the class, and so any program working with this class would have no idea if we have switched from one implementation to the other. This is the essence of encapsulation.

Obviously, the user might have a problem if we made such a change to our object. If applications were developed expecting the first set of behaviors, and then we changed to the second, there could be some interesting side effects. However, the point here is that the client programs would continue to function, even if the results were quite different than when we started.

We'll be making heavy use of encapsulation throughout this book, so you'll become very familiar with it if you haven't seen it before.


Polymorphism

Polymorphism is often considered to be directly tied to inheritance (which we'll discuss next). In reality, however, it's independent to a large degree. Polymorphism means that we can have two classes with different implementations or code, but with the same interface. We can then write a program that operates upon that interface and doesn't care about which type of object it operates at runtime.

Polymorphism through Late Binding

For example, if we have a class called Square and another one called Cowboy then they may both have a method called Draw. We can write a program that tells an object to draw by calling the Draw method. This program won't care if it is working with a Square or a Cowboy - even though each object must obviously implement the Draw method differently.

If the Square class contains the following code:

Option Explicit

Public Sub Draw(Target As Form)
  Target.Line Step(0, 0)-Step(0, 100)
  Target.Line Step(0, 0)-Step(100, 0)
  Target.Line Step(0, 0)-Step(0, -100)
  Target.Line Step(0, 0)-Step(-100, 0)
End Sub

And the Cowboy class contains this code:

Option Explicit

Public Sub Draw(Target As Form)
  Target.Print "Draw from holster"
  Target.Print "Aim"
  Target.Print "Fire"
End Sub

Then we can put this routine in a form:

Public Sub DoDraw(SomeObject As Object)
  SomeObject.Draw Me
End Sub

This routine doesn't care if it is passed a Square or a Cowboy, even though the two classes are implemented in an entirely different fashion. Notice that the routine accepts a parameter of type Object. By doing this, we have allowed the routine to accept literally any object we care to send it. This simple example of polymorphism works in Visual Basic 4.0 as well as Visual Basic 5.0.

Visual Basic 4.0 implements polymorphism in the same manner as Smalltalk, by using something called late binding. We've just seen an example of late binding with the Draw method of the Square and Cowboy objects.

Late binding means that the Visual Basic compiler makes no effort to determine if calls to our objects are valid. All the checking takes place while the program is running. Each call to an object is checked as it's called, and an error is raised if the call turns out to be invalid.

Polymorphism with Early Binding

Visual Basic 5.0 retains this ability, but also implements a stricter form of polymorphism (similar to that found in C++) through its support of interface inheritance using the Implements keyword.

For example, if we create a Shape class with the following code:

Option Explicit

Public Sub Draw(Target As Form)

End Sub

Then we can use Shape as a base to create a Square class:

Option Explicit

Implements Shape

Public Sub Shape_Draw(Target As Form)
  Target.Line Step(0, 0)-Step(0, 100)
  Target.Line Step(0, 0)-Step(100, 0)
  Target.Line Step(0, 0)-Step(0, -100)
  Target.Line Step(0, 0)-Step(-100, 0)
End Sub

And we can also create a Circle class, again using the Shape class as a base class:

Option Explicit

Implements Shape

Public Sub Shape_Draw(Target As Form)
  Target.Circle Step(0, 0), 200
End Sub

With these classes defined, we can try them out. The following code accepts a parameter and then calls its Draw method:

Public Sub DrawShape(objSomeShape As Shape)
  SomeShape.Draw Me
End Sub

Notice the difference between this example and the previous one with the Square and Cowboy classes. Instead of accepting an Object type as a parameter, we're now able to accept a parameter of type Shape. We've ensured that the only objects passed to our routine will be shapes. Since we'd probably never use the Shape class as a base to build a Cowboy class, we don't have to worry about getting a Cowboy when we wanted a Square or Circle.

This uses early binding. Early binding occurs as the program is being compiled, which means that the compiler can help us out by doing type checking and by verifying that the properties and methods we call on the object are valid.


Inheritance

Inheritance is the concept that a new class can be based on an existing class, inheriting its interface and functionality from the original class.With inheritance, a class gains all the properties and methods that make up the interface of the base class, and it can then extend the interface by adding properties and methods of its own. The new class can also extend or change the implementation of each of the properties and methods from the original base class.

For instance, if we had a class called Fruit that had all the properties and methods which apply to all types of fruit, then inheritance would make it very easy to create classes for Apple, Orange and so forth. We wouldn't have to recreate all the code for an apple to be a fruit: it would get that automatically from the original Fruit class.

Interface Inheritance

Visual Basic 5.0 doesn't directly support the concept of inheritance. It does provide something called interface inheritance using the Implements keyword. We've already used this keyword when we discussed polymorphism, but it can be applied here to help simulate a form of inheritance.

Interface inheritance lets us inherit the interface of one class into a new class. The downside is that all we get is the interface, not the original object's data or behavior. Worse still, we can't extend an interface created with the Implements keyword. One of the key benefits of inheritance is the ability to extend the base interface, so this is a substantial drawback.

If we look at a Fruit class, we'll find an interface that includes elements appropriate for any type of fruit. In this example, we've got a growing climate and color for the fruit:

Option Explicit

Public Property Get GrowingClimate() As String

End Property

Public Property Get Color() As String

End Property
Public Property Get Bites() As Integer

End Property

Notice that there's no code in the routines shown here. That's because all we'll be inheriting is the interface; any code in these routines wouldn't be used anyway.

Now we can create a new class, Apple, and inherit the interface from the Fruit class by using the Implements keyword:

Option Explicit

Implements Fruit

Private Property Get Fruit_GrowingClimate() As String
  Fruit_GrowingClimate = "Moderate"
End Property

Private Property Get Fruit_Color() As String
  Fruit_Color = "Red"
End Property

Private Property Get Fruit_Bites() As Integer
  Fruit_Bites = 20
End Property

Earlier in the chapter, we discussed multiple interfaces and how the Implements keyword works. Instead of declaring GrowingClimate, Color and Bites directly, we need to make them Private in scope and put Fruit_ in front of each name so Visual Basic knows that these routines belong to the Fruit interface.

We might also create an Orange class, again based on the Fruit interface:

Option Explicit

Implements Fruit

Private Property Get Fruit_GrowingClimate() As String
  Fruit_GrowingClimate = "Warm"
End Property

Private Property Get Fruit_Color() As String
  Fruit_Color = "Orange"
End Property

Private Property Get Fruit_Bites() As Integer
  Fruit_Bites = 20
End Property

Now, contrary to what we'd expect, we can write code to compare apples and oranges:

Public Function CompareColor(AnApple As Fruit, AnOrange As Fruit)
  CompareColor = (AnApple.Color = AnOrange.Color)
End Function

More importantly, we've created two different classes, both having inherited the same interface from Fruit. This is a very powerful technique when we have otherwise dissimilar classes that have a number of the same interface elements. We can write client code to treat all the objects the same - even though they are different.

Unfortunately, this technique doesn't allow us to extend the interface, and so it is less a way to implement inheritance than it is to implement polymorphism, as in the last section. The Orange class can't add properties or methods to its version of the Fruit interface. This limits our flexibility, and really prevents us from using this to simulate full-blown inheritance.

Containment and Delegation

Fortunately, it is possible, with a little extra work, to roll our own inheritance using Visual Basic - and thus gain many of the advantages we'd get in other languages that fully support inheritance. This is achieved by combining a concept called containment with another called delegation.

Containment is the idea that an object can have a private instance of another object inside itself. Were we to have a Human object, it might contain a Heart object. The Heart object would be private to the Human object, since no client code would normally interact with the Heart.

Delegation means that one object delegates work to another object rather than doing the work itself. If our Human object has a PulseRate property, it might ask the Heart object for the rate rather than figuring it out by itself. It has effectively delegated the responsibility for the PulseRate to the Heart object.

If we combine these two concepts, we can simulate inheritance. Let's look at how that can work with our fruit example.

Again, we'll create a class called Fruit; but this time we'll put some code in the GrowingClimate routine. With inheritance, we'd expect to be able to use this behavior in any new classes we create based on Fruit:

Option Explicit

Public Property Get GrowingClimate() As String
  GrowingClimate = "Moderate"
End Property

Public Property Get Color() As String

End Property

Public Property Get Bites() As Integer

End Property

Now we'll create an Apple class, but this time we won't use the Implements keyword. Instead, we'll manually recreate the interface from the Fruit class - with absolutely no changes to the declarations of either property:

Option Explicit

Private objFruit As New Fruit

Public Property Get GrowingClimate() As String
  GrowingClimate = objFruit.GrowingClimate
End Property

Public Property Get Color() As String
  Color = "Red"
End Property

Public Property Get Bites() As Integer
  Bites = 20
End Property

The first thing you might notice is that we're creating a Private Fruit object within our new class. Our new Apple class is using the concept of containment to have an instance of the Fruit class inside itself:

Private MyFruit As New Fruit

In the GrowingClimate routine, our new Apple class just delegates the work down to the private Fruit object:

GrowingClimate = objFruit.GrowingClimate

Now let's create an Orange class by following the same technique:

Option Explicit

Private objFruit As New Fruit

Public Property Get GrowingClimate() As String
  GrowingClimate = "Warm"
End Property

Public Property Get Color() As String
  Color = "Orange"
End Property

Public Property Get Bites() As Integer
  Bites = objFruit.Bites
End Property

Public Property Get Slices() As Integer
  Slices = Bites / 2
End Property

Since the GrowingClimate for an Orange is different, it doesn't delegate that call down to the Fruit object. Instead, it replaces, or overrides, the functionality by implementing the routine itself.

We have also extended the interface for our Orange object by adding a Slices property. While we've simulated the inheritance of the GrowingClimate, Color and Bites properties, we're also able to extend the interface as needed for each specific class.

Back to our client code, which compares apples and oranges, we need to change how we accept our parameters:

Public Function CompareColor(AnApple As Object, _
    AnOrange As Object)
  CompareColor = (AnApple.Color = AnOrange.Color)
End Function

The code within this routine is the same as we had before, but now our parameters are of type Object instead of type Fruit. Since our new classes don't use the Implements keyword, we can't have Visual Basic treat these objects like a Fruit object.

However, we do know that they have the same set of properties and methods, and so we can write our code to treat them identically. We just need to tell Visual Basic that they are generic objects rather than objects of a specific class.


COM - The Plumbing behind the Scenes

The Common Object Model (COM) is Microsoft's standard for object interaction. COM provides a standard mechanism by which objects and components can communicate, independent of the language in which the components were created.

Other major Windows technologies rely on COM to function. The two most notable of these technologies are OLE (Object Linking and Embedding) and ActiveX. These both use COM to facilitate all the interaction between objects. OLE uses COM to communicate between applications, allowing users to link or embed parts of one application's data and display into another application. ActiveX is broader, using COM to support communication between controls, in-process, and out-of-process servers.

Microsoft has designed Visual Basic to fit seamlessly into COM. Components created with Visual Basic are ActiveX components, which means that they use COM to communicate with each other and with their client programs. In many other languages, it can be very complex to create components that fit into this model, but Visual Basic takes care of all that complexity behind the scenes.

Distributed COM (DCOM) extends the communication capabilities across the network. With DCOM, any COM object can transparently communicate with objects on other machines over the network. This works with components created in Visual Basic as well, which means that Visual Basic developers can create objects and distribute them across the network with very little effort.

Visual Basic 5.0's object capabilities are very well matched to those of COM. COM objects typically have multiple interfaces and, as we've seen, Visual Basic objects can have multiple interfaces using the Implements keyword. COM, like Visual Basic, has no direct support for inheritance, but instead relies upon aggregation so we can group objects together to provide more complex functionality.

Because Visual Basic 5.0's capabilities are so closely matched with COM's capabilities, it is an ideal development tool when we're working in a COM environment.


Other Industry Efforts

COM isn't the only distributed object platform on the market, so we'll take a quick look at a couple of other options, mainly to try and see how important communicating components and business objects are in modern computing.


CORBA

The Object Management Group (OMG) is a consortium of companies that have banded together to develop an object communication standard. The Common Object Request Broker Architecture (CORBA) is a standard that competes with Microsoft's Component Object Model (COM) in many ways.

The various companies involved, and the industry press, tend to dwell on the differences between the two competing standards. While it's true that there are differences, what I want to emphasize here is the similarities between these standards.

Both CORBA and COM provide standardized mechanisms for objects to communicate with each other. They specify how components and applications can be built in a distributed and organized fashion. CORBA, like DCOM, provides for communication between objects distributed across a network, providing for complex multi-tiered applications based on the standard.

Perhaps most importantly, CORBA and COM both encourage the use of business objects in their architectures. And they both advocate a separation of business rules from the presentation layer, in such a way that the main application logic is contained in an object model that's largely independent of the user-interface.


Vertical Market Groups

Another major effort underway by various companies and organizations is the creation of business object frameworks that support specific vertical markets, such as IBM's San Francisco initiative. These are collections or groups of objects that are built to service specific business needs.

For instance, a vendor might build an object framework that encompasses the typical requirements of managing a shop floor in a factory. The vendor can then market their framework to companies who are building shop floor control software. These companies can then base their software development on the pre-existing business objects, extending or modifying them as needed to meet their specific business needs.

It remains to be seen whether this type of object framework will be viable in the marketplace. While many business processes are generally similar from company to company, it is also true that every company is unique. The level of modification required to take a framework and make it useful in a given situation could be quite large, and it could offset the benefits to be gained.


Summary

In this chapter, we've introduced the concepts that will be covered through the remainder of the book. At this point, you should have a good understanding of what business objects are - and why they're so important to the future of software development. You should also have a general understanding of component technology and how business objects can be managed and distributed through components.

From here, we'll explore these concepts in more practical detail. We'll look at:

  • The design and creation of business objects

  • How these objects fit into our applications

  • How business objects interact with the user-interface

  • How business objects interact with the database

  • How business objects can interact with each other


Chapter 2: Application Architecture with Business Objects

Overview

In Chapter 1, we ran through a quick overview of what business objects are and why they're so important to software development. We also took a brief look at the concept of component-oriented design, which allows us to group business objects together into reusable binary components.

In this chapter, we'll review traditional Visual Basic development techniques, and see how business objects and components can improve the way we develop software.

Traditional Visual Basic development is very fast and very powerful, but it doesn't necessarily lend itself very well to large and complex applications. Using the capabilities of objects and components, we can more easily create and maintain very complex applications. The best part of this is that we can continue to leverage our existing Visual Basic skills, because working with business objects is really an evolutionary change rather than an entirely revolutionary approach.

The most significant change in our development, when we use objects, is that we follow a somewhat different architecture than we've used in traditional development. As we go through this chapter, we'll be looking at traditional Visual Basic architectures and the impact of business objects and binary components upon those traditional architectures.

Most large applications are implemented in a client/server environment, with the application's processing spread across a number of machines, including the user's workstation, a database server, and possibly an application server that sits in the middle. We'll look at this type of architecture, and see how we can use our business objects to take full advantage of all these machines.


Building Applications with Components

To begin with, let's take a quick look at some of the key techniques and concepts we've used to develop applications, and how these traditional techniques can be used when we're working with business objects. Most of these techniques will carry over, though sometimes there will be some subtle differences.


Event-Driven Programming

Visual Basic programming (and Windows programming, more generally) is almost entirely geared towards event-driven programming. This means that our programs are typically composed of subroutines that are run only in response to external events - such as a mouse click. It is a rare Windows program that runs a series of procedures from top to bottom and is done.

Most mini-computer, mainframe and DOS-based applications were sequential in nature. They gathered the user's input in a specific order, processed it, and generated specific output. This was all very predictable, since the application itself was very much in control.

In an event-driven environment, such as Windows, our applications effectively give control over to the user. The user gets to choose in which order they want to enter any input, when it should be processed and often how and where the results should be displayed. Rather than being in control, our application code sits in the background waiting for the user to indicate what should be done.

For instance, let's look at how we might get input from the user. In a sequential application, we'd prompt the user for input when we needed it. In some form of DOS BASIC, that process might look like this:

Print "Name? ";
Input strName
Print "Age? ";
Input intAge
Call DoProcessing(strName, intAge)

Here, the program is in control: it asks the user for each bit of information in turn, and the user has no choice but to follow the program's flow from top to bottom.

Within Visual Basic, the approach is entirely different. We display a form to the user, allowing them to choose into which field, if any, they want to enter information:

Our code, rather than being a single sequential list of commands, now becomes a series of subroutines that are only run when specific events occur. For instance, as the user enters values into each text box on the screen, we may react to the input events and store the values in variables:

Private Sub txtName_Change()
  strName = txtName
End Sub

Private Sub txtAge_Change()
  intAge = Val(txtAge)
End Sub

Then, in response to the Click event caused when the user clicks the OK button, we might perform some processing:

Private Sub cmdOK_Click()
  DoProcessing(strName, intAge)
End Sub

Either approach gets the job done, but the event-driven approach turns a lot of control back over to the user. This is, generally, a positive thing: users usually like to have a choice about how they use their applications.

Events and Objects

Event-driven programming works well when we're dealing with objects. This is because an event is basically a type of message. When the user clicks on a button, our program gets a message saying, "Hey, the user clicked the button". Of course, the message comes to us in the form of an event - in this case, the Click event for our button. Still, what we basically received was a message that something happened.

When we're dealing with objects, we always interact with an object though its interface; that is, its properties and methods. When we call an object's method, we do so by sending it a message. This may not be very obvious, since, within our code, it looks as if we just called a method on the object. Take this line of code, for example:

  objCustomer.CreateInvoice

This code looks like a call to the CreateInvoice subroutine within the Customer class module. However, it's important to remember, when dealing with objects, that this kind of code actually sends a CreateInvoice message to the objCustomer object.

Our objects react to such events in the same way that Visual Basic programs have always reacted to events: using a subroutine.

Public Sub CreateInvoice()
  ' create the invoice here
End Sub

Any time we interact with an object, we're doing so by sending it a message. From the object's perspective, a message is basically an event caused by some outside force (typically, our program's code or some other object), and so our objects are always driven by events - just like our traditional Visual Basic applications.


Code Behind Forms

In traditional Visual Basic applications, most programs have tended to respond to the events caused by the user - through the forms and controls that we've provided on our forms. So it's been very common to find the code to handle these events directly behind the controls that raise them.

Many developers have therefore written the bulk of their code in the subroutines that run when particular events fire:

Private Sub cmdButton_Click()
  ' do all the work here
End Sub

Meanwhile, other developers have tended to call centralised subroutines in the module's General section, or subroutines that have been placed in a separate code module:

Private Sub cmdButton_Click()
  DoWork()
End Sub

Private Sub DoWork()
  ' do all the work here
End Sub

Either way, the results have been the same: Visual Basic code has been directly invoked by the events raised in the user-interface (UI).

Putting code behind events from the UI hasn't provided us with much in the way of code reuse. The most that we've been able to hope for is to use code modules - so that we can call our code from multiple forms.

To use code in other projects, we've had to copy original code into our new projects, causing duplicate code and maintenance problems down the line. Another approach has been to include the same code module in multiple projects; but then we run the risk that modifying the code in the module will break any other programs that rely on the module. Even if we don't break the programs, they need to be recompiled and redistributed to take advantage of our changes.

By designing our applications with business objects and components, we now have the opportunity to make it significantly easier to reuse our code. Instead of trying to reuse source code, we can package that code into binary components and use those components anywhere we need them.

If we create a component that provides some key functionality - everything to do with our customers, for instance - then we can use that component in any program that needs to deal with customers. As long as we are careful not to change or remove any properties or methods from our objects, we can change or add anything we want to our customer component with little risk of breaking existing programs.


Access to Data

Most business applications deal with data. Traditional Visual Basic applications have implemented code to work directly with the database. The database could be local, or on a mapped network drive, such as a Microsoft Access database. Others would be maintained by an actual database server such as Microsoft SQL Server or Oracle.

Visual Basic hasn't always had easy access to databases. In fact, it wasn't until Visual Basic 3.0 that we had easy access to databases - through a data access model and the infamous data control. Using the JET database engine,we could finally open up databases and work with the data using native elements of the Visual Basic language. Through JET we even had access to SQL database servers through Open Database Open Database Connectivity (ODBC).

The JET engine worked well enough to create two-tier client/server applications, but it tended to be slow and inflexible. There was no easy way to tap into the power of the SQL database server from our applications. Without resorting to direct calls to the ODBC API, or using proprietary database libraries, there was no way to avoid the use of the JET engine for database access.

Visual Basic 4.0 added a couple of important capabilities Remote Data Objects (RDO), and an enhanced data access model in the form of Data Access Objects (DAO). RDO is a thin object layer built on top of the ODBC API that provides a substantial performance boost over JET - as well as giving us more capabilities to use the power of a database server. The enhanced DAO with Visual Basic 4.0 offered powerful new capabilities for working with Access databases, but provided little help for dealing with ODBC data sources.

Now, with Visual Basic 5.0, we have even more capabilities. RDO 2.0 offers even more powerful and faster access to ODBC data sources. We can use the familiar DAO model to efficiently access ODBC data sources using ODBCDirect. ODBCDirect provides us with the same DAO object model we use for Access databases, but it uses RDO behind the scenes to provide us with significant performance benefits.

Even more importantly, Visual Basic 5.0 allows us to create components. These components can be run on the user's workstation, on the database server, or on an application server sitting in the middle. Using the current data access technologies, such as RDO and ODBCDirect, we can create components that interact with the database. We can then use those components to develop our applications - and the application won't have to know about or deal with any database issues at all. We'll get into a lot more detail through the remainder of this chapter and at various points throughout the book.


Use of Controls

As we observed in Chapter 1, controls are one of the distinguishing features of Visual Basic, and probably Visual Basic's greatest contribution to the software industry.

VBX Controls

Unfortunately, VBX technology has some severe limitations. In particular, there's very little flexibility in the way that Visual Basic interacts with the controls. VBX controls have to provide a predefined list of methods, regardless of what the control actually does. Furthermore, a control developer can't add new methods to a control, making it very difficult to provide advanced functionality to the end developer. Instead, many controls provide an Action property, where the developer can set the property to a whole range of values to get different effects. This certainly doesn't lead to readable code; who knows what this might do:

myControl.Action = 21

To cap it off, Microsoft chose not to support the VBX standard in the Win32 environment. This means that VBX controls are now only available to developers working in 16-bit Windows. For 32-bit development, we need to use ActiveX controls.

ActiveX Controls

ActiveX controls have largely removed the limitations of VBX controls by extending the VBX concept with more flexibility and more powerful capabilities. Like VBX controls, ActiveX controls typically provide a visual 'widget' for use in developing user-interfaces. However, they are also binary components; that is, they can contain multiple objects. This means that ActiveX controls can implement a complex interface, including any properties or methods that are needed to make the control useful.

Rather than implementing a generic and hard-to-read Action property, an ActiveX control can implement a descriptive method, for example:

myControl.SpinLeft

As the Visual Basic tool has matured, so have its capabilities with regard to controls. Today, ActiveX controls are a specialised form of component for use in building user-interfaces. As a component, ActiveX controls can contain objects, and they can make those objects available to help implement any required functionality in a program.

With Visual Basic 5.0, we have the ability to create our own ActiveX controls. We can create them as purely visual widgets for use in developing user-interfaces, or we can make them more complex by treating the control as a component and including objects.

Certainly, most controls are just visual widgets, and that is their primary function. At the same time, there are more techniques available for the distribution of controls to client workstations, or across the Internet, than there are for other ActiveX servers such as DLLs or EXEs. There are times when it's preferable to package objects within a control simply because that control can be automatically downloaded to a Web browser or other client where the objects are needed.


Use of Components

Components, as I've defined them, are binary entities that contain one or more objects that can be used by applications. The components we can create with Visual Basic are always ActiveX servers: either in-process servers (DLLs) or out-of-process servers (EXEs). With Visual Basic 5.0, we can also create ActiveX controls, which are a more specialised form of component.

Let's review the benefits of using components, which we discussed in Chapter 1.

Reuse

One of the key benefits of binary components is a high level of reuse. We can use a single component in many applications, continually enhancing and improving the component without having to recompile or redistribute the client programs. For instance, we can create an application that uses version 1.0 of a component, as shown in the figure:

If we then find that this component would be useful in another application, with minor enhancements, we can use it as shown in the next figure:

As long as we make sure the properties and methods in version 1.0 of the component are not changed as we enhance it to version 1.1, our original application can continue to use the component without change. This means that we don't need to recompile or redistribute the first application - a very important consideration if our application is installed on hundreds or thousands of client workstations.

Flexibility

The components that we create using Visual Basic are always ActiveX servers. This is important, because ActiveX servers can be used by many development languages and tools - even ones that we may not foresee, as we create our applications.

For instance, we may create an application where our business objects are in an ActiveX DLL. We could then create a user-interface for those objects using Visual Basic forms and controls, pretty much just like any other program. After we deploy our application (both the UI program and our DLL), someone else may create their own user interface for our DLL, perhaps using Microsoft Excel, Active Server Pages or just about any other Windows development environment.

This is a powerful advantage to us as developers. We can provide an unprecedented level of flexibility to our customers, since they can use part or all of our application as building blocks for the development of their own custom solutions. While we may provide a base application with a standard user-interface, our customers may choose to create their own user-interface - or extend our application's functionality to suit their specific needs.

Client/Server

The ability to create components that contain one or more objects is key to the development of applications that are highly scalable. Since version 3.0, it's been practical to develop two-tier client/server applications using Visual Basic. Using components, we're now able to create more complex applications across any number of tiers.

This is true regardless of how many physical computers are involved. Even on a single machine, we can separate our application's business functionality from the user-interface through the use of components. If our business logic is contained in business objects, and those objects are in an ActiveX DLL, then we've effectively separated our objects from any user-interface that might be created to use that DLL.

Alternatively, we might put our business objects in an ActiveX EXE, or use Microsoft Transaction Server, so that they're running on another physical machine across the network. This technique allows our applications to utilise the processing power of many machines and to minimise the amount of information that's sent back and forth over the network.

In the next section, we'll look at a specific application architecture, which we'll call the Component-based Scalable Logical Architecture (CSLA for short), that can be used on a single workstation and yet easily be converted to run across many machines on a network.


Introducing the Component-based Scalable Logical Architecture (CSLA)

To effectively use business objects and components to develop our applications, it's very helpful to start with a good logical architecture. This means that we want to start with a firm idea about what types of services our application will provide, and whether we want to implement them in our user-interface, in components or in a SQL database server.

In this section, we'll look in detail at the logical architecture that we'll be using throughout the remainder of the book. The CSLA is designed to work well whether it's placed entirely on a single workstation or whether it's spread across a number of machines in a network. With this type of design, we can scale an application from a single to many users with relative ease.

A logical architecture includes all the parts of an application. For instance, a typical three-tier application is composed of the following parts:

  • Presentation

  • Business processing

  • Data processing

The following diagram shows how these parts, or tiers, work together to make up an application:

The presentation tier, or user-interface, acts as a client to our business objects. The business processing is handled by the middle tier. This tier is composed of our business objects, all working together to implement the functionality required for our application. The business objects themselves rely on the third tier to manage data processing, storing, retrieving and manipulating data within our databases.

The best way to see how an application can be divided into tiers is to look at a traditional application and break it apart - so that's what we'll do now.


Splitting Up a Traditional Application

Let's take a simple Visual Basic application, like any we may have created in Visual Basic 3.0. An application typically has at least one form for the user-interface, along with some code to do any work that needs to be done, and the application probably talks to a database to store and retrieve data.

The Application

To keep this very simple, let's build a quick Visual Basic application that has a single form and uses a simple text file as a logical database. This example demonstrates the distinct layers of an application.

We'll just have a simple form, as shown below. The text box is called txtName and the command buttons are cmdGet and cmdPut:

Let's suppose that we have two business rules. The first is that the name must be in uppercase, and the second is that the name is required.

To ensure that the name is uppercase, we'll add the following code:

Option Explicit

Private Sub txtName_LostFocus()
  txtName.Text = UCase$(txtName.Text)
End Sub

And to make sure we don't save a blank name, we'll add this code:

Private Sub cmdPut_Click()
  If Len(Trim$(txtName.Text)) = 0 Then
    MsgBox "You must enter a name", vbInformation
    Exit Sub
  End If
End Sub

Now let's just add some code to save the name to a file. This would normally be saving to a database, but we want to keep the program very simple to illustrate the different processes that are happening:

Private Sub cmdPut_Click()
  Dim lngFile As Long

  If Len(Trim$(txtName.Text)) = 0 Then
    MsgBox "You must enter a name", vbInformation
    Exit Sub
  End If

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Output As file
  Print #lngFile, txtName.Text
  Close lngFile
End Sub

Finally, we'll add some code in behind cmdGet to pull the name back in from our 'database':

Private Sub cmdGet_Click()
  Dim lngFile As Long
  Dim strInput As String

  file = FreeFile
  Open "C:\TEMP.TMP" For Input As lngFile
  Line Input #file, strInput
  txtName.Text = strInput
  Close file
End Sub

Now run the program. As you can see, we're able to store and retrieve a name. Furthermore, we won't save a blank name, and any name that we do save is stored in uppercase.

Now let's examine the program from the viewpoint of a three-tier logical model. We'll try to determine which parts of the program perform the presentation, which parts the business logic and which parts the data processing.

The Presentation Tier

This is really the user-interface, or how the program presents information to the user and collects information from the user. In our simple example, the presentation layer certainly includes the form itself:

The presentation layer also includes some of the code that we put behind the form, but certainly not all the code is directly related to the presentation. In the following listing, the presentation code is highlighted:

Option Explicit

Private Sub cmdGet_Click()
  Dim lngFile As Long
  Dim strInput As String

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Input As lngFile
  Line Input #file, strInput
  txtName.Text = strInput
  Close file
End Sub

Private Sub cmdPut_Click()
  Dim lngFile As Long

  If Len(Trim$(txtName.Text)) = 0 Then
    MsgBox "You must enter a name", vbInformation
    Exit Sub
  End If

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Output As lngFile
  Print #lngFile, txtName.Text
  Close file
End Sub

Private Sub txtName_LostFocus()
  txtName.Text = UCase$(txtName.Text)
End Sub

Out of all that code, almost none of it is involved in actually presenting data to the user. Take a look at the lines that we've highlighted. For instance, we need to display the name after we've retrieved it from storage:

txtName.Text = strInput

Notice, however, that this line is stuck right in the middle of several other lines of code that have nothing to do with presenting the data.

Detecting that txtName is blank is a business rule, so that is not part of the presentation. Notifying the user that it is blank, however, is indeed part of the user interface:

MsgBox "You must enter a name", vbInformation

Again, our presentation code is in the middle of other code that is not interface-related. The upshot of this is that if we want to change the business rule, or we want to change the presentation, then we're going to be changing the same code block for either task.

That wasn't too bad, but the line in the LostFocus event is not so cut and dried. This line does two things: it converts the name to uppercase (a business rule), and it displays the name back into the form so that the user can see the final result (a presentation choice):

txtName.Text = UCase$(txtName.Text)

Looking at these lines of code, you can see just what a mess we've gotten ourselves into. We have no way of changing the interface without tampering with business-related code, and we can't change our business rules without affecting the interface code.

The Business Tier

Let's now look at the parts of the program that handle the business logic or business rules. Again, it's only a part of the overall program, so we'll highlight the areas that we need to examine:

Option Explicit

Private Sub cmdGet_Click()
  Dim lngFile As Long
  Dim strInput As String

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Input As lngFile
  Line Input #lngFile, strInput
  txtName.Text = strInput
  Close file
End Sub

Private Sub cmdPut_Click()
  Dim lngFile As Long

  If Len(Trim$(txtName.Text)) = 0 Then
    MsgBox "You must enter a name", vbInformation
    Exit Sub
  End If

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Output As lngFile
  Print #lngFile, txtName.Text
  Close lngFile
End Sub

Private Sub txtName_LostFocus()
  txtName.Text = UCase$(txtName.Text)
End Sub

In the cmdPut_Click event, we need to make sure that the name is not blank - and avoid saving the value if it is:

If Len(Trim$(txtName.Text)) = 0 Then
  MsgBox "You must enter a name", vbInformation
  Exit Sub
End If

Once again, notice how there is code managing the presentation layer right in the middle of our business processing.

The situation is worse (but now rather familiar) within the LostFocus event: we see the same line that we saw in the presentation layer, now working for our business processing to make sure that the name is in upper case:

txtName.Text = UCase$(txtName.Text)

Furthermore, suppose we need to provide access to this name to some other program, or from some other part of the same program: since the business logic is so tied into the display of this form, we'd have to duplicate the business code elsewhere.

Clearly, this is not an ideal way to organise an application.

The Data Services Tier

In our simple example, the data service code (highlighted in the following code) is fairly straightforward:

Option Explicit

Private Sub cmdGet_Click()
  Dim lngFile As Long
  Dim strInput As String

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Input As lngFile
  Line Input #lngFile, strInput
  txtName.Text = strInput
  Close file
End Sub

Private Sub cmdPut_Click()
  Dim lngFile As Long

  If Len(Trim$(txtName.Text)) = 0 Then
    MsgBox "You must enter a name", vbInformation
    Exit Sub
  End If

  lngFile = FreeFile
  Open "C:\TEMP.TMP" For Output As lngFile
  Print #lngFile, txtName.Text
  Close file
End Sub

Private Sub txtName_LostFocus()
  txtName.Text = UCase$(txtName.Text)
End Sub

Still, in both the cmdGet_Click and cmdPut_Click event code, the data processing is mixed right in with the presentation and business logic, making it difficult to change the data processing without risking the other parts of the program.

The n-tier Solution

Certainly, there are conventional solutions to help deal with these problems - including putting code in BAS modules, or creating class modules to hold this code. Unfortunately, these solutions don't fully meet the needs of code reuse and separating (or partitioning) the various parts of the application.

The ideal solution is to pull the user-interface code into one area, the business logic into another and the data processing into a third. By breaking our application into these tiers, we can make it much easier to change any section of our program with less risk of causing bugs in the other tiers. By avoiding the case where a single routine includes code for both presentation and business logic for instance, we can change our presentation code without impacting the business code itself.

We can also facilitate code reuse. For instance, the code in the business tier may be useful to more than one form in the user-interface tier. If our business code is intermingled with the presentation code, it can be very difficult to use that business logic across various different presentations. By separating the code, we can make it very easy for our application to show the same business information through various presentations.


Splitting up Business Processing

Developing a logical multi-tier architecture for our applications helps to partition code by separating the presentation, business and data processing. Once we've partitioned the application into manageable parts, we can build components to hold the objects at each level within the program.

Deciding exactly where to split the functionality is not easy. Even in the simple example that we just looked at, the code to convert the name to uppercase essentially belongs to both the presentation and business layers:

txtName.Text = UCase$(txtName.Text)

If we choose to put this line in the presentation tier then we're able to give the user timely feedback - indicating that the name has been converted to uppercase. But this means that we've put a business rule in the presentation tier, which is far from preferable.

We could put the code in the business tier - a good place for it, since it's a general rule that should be enforced regardless of the interface. Doing this, however, can make it much more difficult for the user-interface to provide timely feedback to the user. It's entirely possible that the business tier will be running on another machine somewhere in the network, so we could be adding a lot of network traffic if the presentation tier is asking the business layer for field-by-field information all the time.

Another possibility is to put the code in both locations. This provides timely feedback to the user, and lets the business tier centrally enforce the rule. Of course, we've duplicated the code by doing this, and so the program becomes much harder to maintain in the long run.

None of these are optimal solutions. This is one of the hardest issues we face as developers of distributed systems. It would be nice if there was a definite answer, so we could say this is the way to do it. Unfortunately, the choice depends on many factors, and these factors are different for each project.

So, the question remains: what are we to do about this partitioning problem? In general, there are two possible approaches: the rich (interactive) interface, and the batch interface. We'll take a look at each of these now.

Rich Interface

Advocates of the rich interface look at the evolution of software from dull, non-interactive screens, to highly-interactive, graphical displays, and want to provide that experience for the user.

Word processing is an excellent example. Years ago, people entered their text, along with various arcane codes, into what was essentially a glorified text editor. From there, they could print their text and see what it looked like after it was formatted. User demand pressured vendors into developing what-you-see-is-what-you-get word processing, so the user eventually received a highly interactive interface with immediate feedback on appearance. Even more recently, tools such as Microsoft Word have begun to subtly provide feedback on spelling by underlining misspelled words as the user types. In short, users like highly interactive, well-designed interfaces.

However, there is a cost to providing this rich interface: it requires that many of the business rules be easily and quickly accessible from the presentation layer. If the rules are located on a server out on the network, then the presentation layer will have to make calls across that network - which will probably result in dismal interface performance.

The solution is to put some or all of the business rule processing on the clients, thus allowing the presentation layer to easily and quickly access those rules - which should provide a highly interactive experience for the user.

The major drawback to this solution is that it requires that we run part of our application on the client workstation. This can be a serious problem if we need to distribute the application to hundreds or thousands of client workstations. Fortunately, there are many new technologies becoming available, in the Windows environment, to automatically manage this distribution. For example, Microsoft Internet Explorer and Microsoft's Zero Administration Initiative both automate the installation of client-side components.

Batch Interface

On-Line Transaction Processing (OLTP) developers have long subscribed to a batch input concept. Web browser interfaces also follow along this line, since it minimises network traffic and leaves virtually no processing on the client.

Interfaces constructed with this technique allow the user to enter virtually anything on the screen, and perform little or no screening of input while the data is being entered. When the user accepts the screen, the business rules check all the entries and report a list of problems with the data.

The benefit to this approach is significant. All the business rules are centralised and so easily maintained. The presentation layer does virtually no work beyond that required by the controls with which the user interacts. This approach virtually eliminates the logistic problems of maintaining hundreds or thousands of clients.

The downside to batch interfaces is that the user-interface is very much non-interactive, since it gives the user no immediate feedback. In many cases, this type of interface will be unacceptable to the end users, who often want their application moved to Windows specifically to escape a batch-style interface.

The Best of Both Worlds

The conflict between rich and batch interfaces is significant. As developers, we want to provide the user with the best, most interactive interface that we can manage. At the same time, we need to provide good performance, and design the application to be easily manageable - since it's going to be deployed to hundreds or thousands of client machines.

Visual Basic provides us with the tools to overcome these problems. To do this, we need to adopt an architecture where we split the business processing into two parts:

  • User-interface (UI)-centric business processing

  • Data-centric business processing

The idea behind splitting the business processing apart is to keep processing as physically close to where it is needed as possible. This means that the processing needed by the presentation layer should be physically close, typically on the client. Processing that mostly works with data should be closer to the data source, often on a central application server.

If we look at a typical business object, we'll find that it provides some services that are only useful for developing an interface. We'll also find that it provides some services that are mostly useful for supporting data processing.

For instance, let's consider a simple Product object. Such an object might implement the following services:

Service | Purpose
ID | ID value of the product
Name | Product's name
Price | Product's price
QOH | Quantity of the product on hand
Receive(Quantity) | Receive some amount of the product into the inventory
Allocate(Quantity) | Allocate some amount of the product from the inventory
Load(ID) | Load the object from the database
Save() | Save the object into the database
Delete() | Delete the object from the database

Typically, we'd implement a Product object such that it implements all of these services. The problem is that we're left with a single object that needs to run on one machine - either the client or the server.

This puts us back at the rich versus batch interface crossroads. If we put the object on the client, we can provide a rich interface, but we've also put the data access methods on the client. If we put the object on the server, the client will need to go across the network to use any of the services provided by the object, so the user-interface would typically communicate with the object in a batch format; in other words, we're back to providing a batch interface for the user.

However, suppose we split our Product object into two separate objects: a UI-centric Product object, and a data-centric Product object. Let's consider this now.

UI-centric Business Processing

Our UI-centric Product object would provide the following services:

Property/Method | Purpose
ID | ID value of the product
Name | Product's name
Price | Product's price

Property/Method | Purpose
QOH | Quantity of the product on hand
Receive(Quantity) | Receive some amount of the product into the inventory
Allocate(Quantity) | Allocate some amount of the product from the inventory

All these services would very likely be used only by the user-interface. By making these available in their own object, we have the option of putting that object in a component that will be installed on each client. Now the UI code will be able to interact with this object locally, without any network traffic. Any business rules that are implemented by these services will be enforced interactively. Thus we can implement a rich, highly interactive interface for the user.

In general, the UI-centric business processing resides on the client machine, so it's readily accessible to the presentation layer. This is processing that does not require interaction with the database or with other centralised resources. We're including a lot of business rules and business processing in this category.

For instance, if a data field can only accept a future date, this is the place for that rule. If a series of values need to be totalled together and run through a mathematical formula, this too is where the processing belongs.

On the other hand, if a field needs to be checked against a column in a large table, or verified against some external data source (like a credit check), then this is the wrong place to do the work.

By keeping the UI-centric processing close to the client, we can build some very nice, highly interactive user-interfaces. While we can't tell the user about every business rule violation that they might cause, we can probably catch the majority of them and present it to the user in a timely fashion.

Data-centric Business Processing

The data-centric Product object would provide the following services:

Property/Method | Purpose
Load(ID) | Load the object from the database
Save() | Save the object into the database
Delete() | Delete the object from the database

These services interact exclusively with the database to retrieve, add, update or remove our object's data. These are not the type of service that the user-interface requires on a field-by-field basis, so we won't take any serious performance hit by running them on a separate machine from the client workstation.

A lot of business rules require quick access to the data source or some other centralised resource, such as a credit checking service via TCP/IP or messaging such as a central fax service. Again, in keeping with the philosophy that processing should be performed physically close to the source, this type of business rule belongs on some central machine. Usually, this would be an application server, although that may vary. We'll discuss physical architectures more thoroughly in Chapter 4.

It's important to understand that data-centric business processing is not the same as data services. Data services are mostly concerned with storage, retrieval and integrity of data; business processing, on the other hand, is all about enforcing business rules and performing any business logic.

A good example of a data-centric business rule is that of verifying a user's entry against a column in a very large table. It is not practical, or desirable, to download all that data to the client machine, so the rule can't be enforced there.

Such a rule might belong in the data services (implemented in SQL) if it's just a simple lookup. But suppose the business rule is more complex - such that more processing is required beyond just the lookup. This is often much easier to do in a high-level language, such as Visual Basic, than it is to do in a SQL stored procedure.

As another example, suppose we have an order entry application, and our users want automatically generated sequential order numbers that need to be displayed as soon as the order screen is presented to the user. Generating sequential order numbers is a form of business processing, but to make them sequential across all clients requires centralised processing. We could do this at the database level, but it would probably make more sense to handle it through our business logic. Implementing a simple business service to provide the next sequential order number is easy, and keeping it at this level can help reduce the load on the database server.

Tying Together the Business Logic

Even though we've split the business processing into two parts, they are still integral. In fact, they must work together efficiently, since neither the UI-centric nor data-centric processing can, by themselves, provide all the business logic required for an application.

What we need is for the two parts of the business processing to handle all their communications internally, so that neither the presentation layer, nor the data processing layer, is aware that the business logic has been split apart. Essentially, we want it to appear as single layer of software.

Each UI-centric (client-side) business object must be able to efficiently communicate with any corresponding data-centric (server-side) object. The communication between these objects will typically be across a network, and so the goal is to minimise the dialog between the objects.

In Chapter 4, we'll go into the details and see how we can make this communication fast and practical.


An n-tier Architecture

So far, we've looked at a three-tier architecture and how we can split our business processing into two distinct parts: UI-centric and data-centric. What we've really done is move from a three-tier architecture to an n-tier architecture. The new architecture consists of the following four layers:

  • Presentation

  • UI-centric business objects

  • Data-centric business objects

  • Data services

If you look back at the three-tier diagram in Chapter 1 and compare it to the following figure, you'll note that all we've done is split the business tier into two parts:

This may seem like a small change, but in reality it is very significant. By splitting the UI-centric processing from the data-centric processing, we've given ourselves much more flexibility. We can now keep the UI-centric processing on the client, so we can provide a rich interface for the user. At the same time, we have the option of moving the data-centric processing away from the client to a machine with faster access to the database, so we can gain significant performance benefits.

Let's go through each part of this architecture and see how it works. We'll be using this architecture throughout the remainder of the book, so we need to make sure it's clear in our minds as we go forward.

Presentation Tier

To start off, we have the most visible part of the whole application: the user-interface. Traditionally, these are the forms, menus, and controls that the user uses to interact with our program. It's quite reasonable, however, that we might need to expand this definition to fit into today's environment. The presentation layer for our application might not just be made up of Visual Basic forms anymore. Rather, it might be an HTML page, a spreadsheet, a word processor, or another entire application.

The key thing to keep in mind about the user-interface is that it is not the application. As we'll see shortly, the business objects are the application; the user-interface is just how the user chooses to interact with our objects.

The goal is to create programs that are independent of the presentation to the user. This means that our application can have many different user-interfaces implemented in different tools and technologies.

As we discussed earlier, in a typical Visual Basic application almost all the code, and therefore almost all the processing, lives in the user-interface. Programmers put most of their code behind controls on their forms. Other programmers put the code in code modules to be called from the forms, but really the processing is still entirely centered around the interface:

This is an ideal technique when we're writing event-driven programs, and so it has worked well for us so far. The events come from the user-interface, and so it makes perfect sense for all the code to be directly linked to that interface.

Unfortunately, this approach doesn't work nearly so well when we're trying to develop an object-oriented model for our program, since the focus of the program is on the interaction between objects rather than on an interaction with events in the interface.

Our users often want more than just a traditional user-interface; they may also want a web browser interface, for instance. A more advanced user might even want access to our application from Visual Basic for Applications (VBA), so they can work with the application from within their spreadsheet or word processor.

Also, the part of an application that typically changes the most is the user-interface. Users like to tweak their interface almost continually, and so it seems as if there is a constant stream of requests to move the controls around or replace radio buttons with a combo box and so forth. If our code is primarily placed behind the controls on forms then even an apparently simple interface change can be complex. Not only do we have to change the interface, but we probably have to rearrange the actual business logic to compensate for the changes.

This is not an easy request to fulfil if we have a substantial amount of code tied to our Visual Basic form interface. Even putting much of the code in BAS modules doesn't help, since neither Excel nor browser-based interfaces will have easy access to the code.

On the other hand, if our program is built based on components that contain business objects then we can easily write a flexible interface, or even a number of interfaces, to talk to our application:

Changes to a Visual Basic form interface now become much simpler, and safer, since our business code is independent of the code inside the form.

Also, our application automatically supports any program that can act as a client to an ActiveX server. This includes most Windows development tools and productivity software (such as Microsoft Excel). Even the Web browser problem becomes much simpler, since we can use Active Server Pages to directly access the application's business objects to create HTML pages for the browser:

At this point, it may not be entirely clear how this architecture can be implemented with Visual Basic. As we get further into business objects, we'll delve into the details and see just how well this whole idea works out. This architecture has been used in the development of a number of large projects with great success - that's happy users, good performance and long term maintainability!

UI-centric Business Objects

Instead of putting the bulk of the code for our application behind Visual Basic forms, we'll move that code into business objects. By doing this, we can apply object-oriented design techniques to build an object model, and put the business rules and processing into the objects of that model.

In Chapter 1, we defined what a business object is - on a fairly high level. In summary:

Business objects are the core of your business solution. With Visual Basic, they are instances of class modules that are designed to represent the real-world 'objects' with which our application is dealing.

In Chapter 3, we'll go through some techniques that we can use to identify real-world objects and translate them into software objects.

The user-interface is really a client, or user, of the business objects. While the user-interface displays and retrieves data from the user, the business objects need to provide all the business functionality and capabilities required by the user interface. At the same time, the business objects need to enforce all the business rules and relationships to meet the design requirements of our application.

The business objects should provide appropriate access to all the functionality of the application. The presentation tier, even including macros in other applications such as Microsoft Word, should be able to use the components containing our business objects to do their work.

The business objects contain the business rules for the real-world entities that they represent. For instance, if a business rule says that a customer must have a last name, there should be code in the customer business object to make sure that the last name isn't blank. You might find something like the following code in the LastName property of a customer object:

Public Property Let LastName(Name As String)
  If Len(Name) = 0 Then
    Err.Raise vbObjectError + 10, "Customer class", _
      "Last name can not be blank"
  Else
    strLastName = Name
  End If
End Property

Business objects interact with each other by following the rules laid out by our object model. Business rules dictate how the objects work with, or relate to, each other.

As an example, perhaps a customer can have many phone numbers. Translating this into business objects, our customer object will have many phone objects. There is a relationship between the customer object and each phone object, in that each phone object is owned by a customer. This relationship needs to be coded somehow into either the customer or the phone objects. For instance, the phone objects may be stored as a collection inside the customer object:

Option Explicit

Private PhoneNumbers As New Collection

Our UI-centric business objects should be designed to accurately reflect the real-world business entities that our application needs to model. This means that each object should represent some entity, along with any attributes, methods and business rules that affect that entity. Our overall object model should also enforce any business rules regarding how objects interact.

Any user-interface will interact exclusively with our UI-centric business objects, and so these objects must not only implement and enforce all our business rules, but they need to provide all the capabilities that the UI developer will require to create a rich, interactive interface for the user.

Data-centric Business Objects

By splitting the data-centric processing out of our business objects, we've provided ourselves with a great deal of flexibility in terms of how we deploy our application. We can choose to implement the data-centric business objects in a DLL, so that it runs on the client workstation, or we can put them in an EXE running on another machine on the network. We can easily compile them into a DLL and run them within Microsoft Transaction Server (as we'll see later in the book).

Our data-centric business objects typically provide certain services that are used by our UI-centric business objects. When a UI-centric business object needs some work done that it can't do by itself, it just has a data-centric object do the work.

Our UI-centric business objects should provide the UI developer with an intuitive and reliable model of the real-world objects in our business. Our data-centric objects need to provide the UI-centric objects with a robust and powerful mechanism by which data can be saved and retrieved as needed. This means that all interaction with data sources should be managed by our data-centric objects; the client-side business objects should have no idea how data is stored or retrieved.

ODBC Data Sources

Most applications deal with data, and most data is stored in some type of database that we can reach using ODBC (Open Database Connectivity). Because of this, most of our data-centric business objects will make heavy use of data access technologies such as DAO, RDO and ADO.

Since our server-side business objects will handle all the database interaction, this is where we'll find all the SQL statements, recordsets, resultsets, database objects and so forth. It's the job of the data-centric business objects to translate all these relational database concepts into data that is useful to our client-side business objects.

As an added bonus, our data-centric business objects can be used to shield much of our application from the differences across various SQL databases. Since all the database interaction is managed by our server-side objects, we have only one place to change code when we need to support a new database.

While it's true that we can use ODBC to help minimise the differences between various databases, there is a cost. A prime example is an application that needs to run on a single workstation with an Access database and in a WAN environment with an Oracle database. Using ODBC, we can write a single set of data-centric objects that work with both databases. Unfortunately, our code will be optimised for neither Access nor Oracle, very possibly leading to unacceptable performance.

With little or no impact to our user-interface or the UI-centric business objects, we can make our data-centric business objects smart enough to deal with both types of database. These objects can be written to detect the type of database and to run the appropriate queries and use the appropriate data access techniques for either Access or Oracle as needed.

Along the same line, if we need to migrate from one kind of database to another then we only need to change the server-side business objects, not all the business objects on the clients. Perhaps our database was a legacy system, and we used screen-scraping technology to get at the data. If we later convert to Microsoft SQL Server, we can just change the data-centric business objects to get the data from the new source - with no impact to our user-interface or the client-side business objects.

Non-ODBC Data Sources

It's important to remember that data isn't always stored in a relational database. Many applications need access to specialised hardware or software. For instance, we may have objects that represent information that is stored in a proprietary laboratory information management system (LIMS) or some equivalent type of system. Perhaps our objects represent physical devices that load trucks with an appropriate mix of cement, sand and gravel to make ready-mix concrete.

As we discuss 'data-centric' business objects, we need to keep in mind that we aren't always working with ODBC, SQL, and other relatively simple data sources. Fortunately, the CSL architecture we're talking about allows us to shield our client-side business objects from all these details through the use of server-side objects. Our data-centric objects can interact with whatever data source we need to deal with, whether complex or not. At the same time, our UI-centric business objects can provide an easy, consistent interface for use by the UI developers.

Minimise Licensing Costs

If we're implementing our architecture across multiple physical machines, we have the option of moving the data-centric objects off the client workstations and on to a central application server machine. We might do this by putting the objects in an ActiveX EXE server and using DCOM, or we might put them into an ActiveX DLL server running within Microsoft Transaction Server. We'll look at examples of each approach later in the book.

By centralizing the data-centric objects on a separate application server machine, we've also centralised all the database interaction. This means that the clients don't need to have any direct access to the database, only to the application server.

This has a couple of benefits. Logistically, we may have simplified the client desktop - since we don't need to install and maintain ODBC drivers and data connections out there. Also, suppose we have 100 client workstations: traditionally, we'd have to buy 100 client licenses for our database. By using a centralised set of data services, we can let the clients share the connections to the database, thus reducing the number of client licenses that we need to purchase. Since most database vendors charge licensing on a per connection basis, we can save on licensing costs.

Data Services Tier

The final area we need to discuss is the data services tier. At a minimum, this tier is responsible for creating, updating, retrieving and deleting data from a data source. In many cases, it also includes more complex features, such as enforcement of relational integrity and other data-related rules.

In most cases, this data handling will actually take place on a database server. This allows us to put quite a bit of processing on the database server, using SQL-stored procedures and triggers. This can often provide important performance benefits, since most SQL database servers can optimise stored procedures so they run very efficiently.

It's important to note, however, that much of the work that a database server might have done in a application may actually be more appropriately placed in our data-centric business objects rather than in SQL code. SQL is notoriously difficult to work with. It doesn't compare to tools such as Visual Basic for features such as debugging and flexibility. While it's still very valid to do some SQL coding in an application, it is often much more cost effective to move the processing to a different layer of the application - where we can bring more powerful development tools to bear on the problem.


Summary

In this chapter, we've looked at event-driven programming, code behind forms, and other traditional design concepts. We saw how Visual Basic 5.0's object- and component-oriented features build upon older concepts to provide us with new and more powerful capabilities for software design.

We also walked through the concept of a logical three-tier application model, separating presentation, business logic and data processing. We developed a specific application model including a presentation tier, UI-centric business objects, data-centric business objects and a data services tier. We discussed how this CSL architecture can be implemented in environments ranging from a single workstation to a network where the processing is spread across multiple machines.

At this point, you should have a good understanding of how this architecture works and how business objects and binary components are used to implement it. As we progress through the remainder of the book, we'll build on these concepts and implement an application by making use of them.

From here, we'll explore these concepts in more practical detail. We'll look at:

  • The design and creation of business objects

  • How business objects are implemented in Visual Basic

  • How business objects are used to create components

  • The different types of components we can create using Visual Basic

Specifically, in the next chapter, we'll cover:

  • Analysis of our business domain

  • Identification of business objects

  • Design of individual business objects

  • How business objects interact

  • Using business objects to model our business


Chapter 3: Business Object Analysis

Overview

We've done some important work in the first two chapters. We've defined what we mean by business objects, we've considered the benefits of using business objects, and we've looked at some of the logical architectures that can use business objects to their best advantage. Now it's time to think about the relationship between our business objects and the real world they represent. Specifically, we're going to take a look at some of the techniques that can help us identify, in the very first instance, the actual business objects that we need to create when we're modeling business situations in the real world.

In this chapter, and through the rest of this book, we'll be creating three interrelated systems that are designed to help run a video rental store. We'll perform a detailed business object analysis of these systems within this chapter, which will provide us with a sound introduction to object analysis.

We'll be making extensive use of the use case technique in this chapter. This is a form of analysis that can be very useful for identifying and designing business objects. We'll also look at some of the common types of object relationship that can be found in real-world business systems, and we'll consider how we can design our business objects to support these relationships.

So here are the main business analysis themes in this chapter:

  • Identifying and defining business objects

  • Object attributes and behaviors

  • Object relationships

  • Analysis: our Video Store example project

In this chapter, we'll be focusing on the UI-centric business objects that were located within the Component-based Scalable Architecture (CSLA) that I presented in the last chapter:

The UI-centric business objects are the objects that most closely represent the real-world entities that we're trying to model with our software. As such, UI-centric objects also happen to be the heart of any application that's built upon this particular model.

Any presentation layer will be built directly on top of these UI-centric business objects, so a key requirement is that they provide a robust, easy-to-use model of our business processes.

In this chapter, we'll look at real-world business entities and determine how to represent them as software objects. The technique we'll use to represent these business entities will be to model them, and the particular part of the business that we want to model will be our business domain. As we'll see, within this chapter, there are two major steps that we must accomplish if we are to successfully model our domain:

  • We need to figure out which real-world entities need to be part of our object model. From there, we can decide which attributes (properties) and behaviors (methods) these objects will need if they are to successfully represent our business entities.

  • We need to evaluate the relationships between our different objects. Objects can interact with or relate to each other in many different ways. We need to add properties, methods, and events to our objects so that they can work together to model our business processes.

Another major aspect to the design comes when we decide which objects should go into different binary components, such as EXE's or DLL's. This is part of the implementation design of our objects, and we'll cover these issues in Chapter 4, where we discuss the creation of business object components.

Once we've identified our business objects and their relationships, we'll have created a logical object model, which will help us decide how we can implement those relationships using Visual Basic.

The relationships we'll be discussing are commonly used in object-oriented design, but exactly how we implement them in Visual Basic may vary from implementation methods commonly seen in other languages.


Identifying and Defining Business Objects

Here's a question that I'm often asked when I'm talking about business objects: how do you figure out what objects to create? It's a good question. Sometimes, objects are pretty obvious; a customer object in a sales system, for instance. Unfortunately, not all business objects are so obvious, and it can take some work to identify them.

It's always important to do a good job of analysis when we're developing any software; but it's even more critical when we're designing software using object-oriented techniques. Since one of the main aims in using objects and components is to promote reuse, it's important to identify and design our objects effectively.

There are plenty of books and articles that tell us all about rapid development and how we should quickly put together some objects and then let the object model evolve to meet future needs. This seems like an excellent idea; but unfortunately, this approach goes a long way toward defeating the reusability of the objects in the model.

If our evolutionary process requires that we change certain properties or methods that we've already added to an object, or that we must replace a single object with multiple objects, then we'll have to change all our client programs as well.

While it's true that we don't have to design every detail of our objects up front, it is very important that we identify our objects carefully. It's also very important that we get to the central behaviors of each object right from the start. The better the job that we can do during the analysis and design of our objects, the less risk we run of having to make substantial and costly changes later.

There are a number of approaches we can use to analyze and identify our business objects. These techniques include:

  • Decomposition of business concepts and processes

  • Deriving our objects from a data model

  • CRC (Class, Responsibility and Collaborator) cards

  • Use cases or scenarios

In an ideal world, we'd be able to design our applications without ever having to worry about an existing data model. And if we're lucky enough to be in this situation, we can start with our business object design and then build our data model to support our objects.

Since this is rarely an ideal world, however, it's much more likely that we'll be using an existing data source with a preset structure. But even in this case, we should still start by creating a business object model - with the understanding that we may have to compromise our model later on to fit it in with the existing database.

Using the CSLA, we can minimize the impact of the database design, since the data-centric business processing is handled separately from the objects that represent our business entities.


Use Case Analysis

At its most basic level, a use case is simply a written description of a business process. We can employ this technique as a high-level analysis tool to determine overall system requirements, and to identify broad business objects that are involved in high-level processes. At the other end of the spectrum, we can get very specific in the use case description, defining detailed functionality and objects.

The process of starting with high-level use cases, and then zooming in to a number of increasingly detailed functional use cases, until we reach a set of specific objects and behaviors, is a technique known as functional decomposition.

This is very much a top-down approach to software analysis, and one that end users can usually relate to very well. It's easy to get a user to describe their high-level requirements, and then just continue to ask more and more questions to get a more detailed view of what they need. Usually, we can take the written use cases back to the user to make sure that what we heard and wrote down was actually what they meant.


Requirements Use Case or Scenario

At the broadest level, we can create use cases that help to define overall system requirements. This type of use case is known as a requirements use case, or a scenario. A requirements use case can help to identify some of the more major business objects in our business domain - despite the fact that the process is actually geared more towards gathering requirements than describing any specific functionality.


Use Case 1: Renting a Video

Here's an example of a requirements use case for a common activity: renting a video. Obviously, there are a number of other high-level use cases involved in the store's overall operation, but this case covers one broad process that's central to the store's business:

Requirements Use Case for Video Rental

The video store maintains an inventory of rentable videos for perusal by customers. A customer enters the store and browses the inventory to select any number of videos to rent.

The customer then brings the videos to the front counter where an attendant identifies the customer, makes note of each video, and calculates the total rental price. The attendant gives the customer an invoice, which lists the videos and shows the total amount due.

The customer pays for the rental and leaves with the videos. To avoid setting off the security alarm, the attendant hands the rented videos to the customer as they leave.


Use Case 2: Problem Recording

Here's another example of some related requirements that might be implemented, in a separate system, to track problems with videos:

Requirements Use Case for Video Problem Recording

The video store maintains an inventory of rentable videos. As the videos age they begin to have problems. By tracking the number of problems over time, the store can stop renting the videos before they get too bad.

The customer reports the problem to an attendant, either when the video is returned or sometimes over the phone. The attendant records the video's ID into the computer and indicates the nature or category of the problem, along with the date and (optionally) a text description.


Use Case 3: Rental Analysis Reporting

Finally, here are some requirements for a system that reports an analysis of video rentals.

Reporting requirements are often quite different from the rest of an application. Actual reporting is typically done through a reporting package that works directly against the data source. Data analysis is often best done by within business objects, since we can take full advantage of the logic we've coded into our objects.

Requirements Use Case for Rental Analysis

In order to decide what kinds of videos to provide for renting in the future, the store owner wants to analyze the inventory to identify which types of video make the most profit.

To do this, the computer needs to look at the number of times each video was rented and what the rental price was for each rental – essentially looking at the revenue generated by the video. This needs to be compared to the purchase cost of each video to determine whether the video recouped its cost effectively.

All this data needs to be consolidated to show the profitability of the videos - both by producing studio and by video category.


Identifying High-Level Requirements

The requirements use case descriptions are not very specific. There are a number of areas where we've identified requirements but not specific functionality. For example, some of the requirements we can derive from the video rental use case could include:

  • The store maintains an inventory

  • The inventory must be available for browsing

  • The store employs an attendant

  • There is a mechanism by which the attendant can identify a specific customer

  • The attendant can identify specific videos

  • There is a security system triggered by videos leaving the store


Identifying High-Level Objects

We can also pick out some potential high-level objects. This is an important step, since these objects are likely to be the ones that are shared across multiple use cases, and very likely across multiple applications within our organization. Basically, we just want to look through the use case and pick out all the nouns. Each noun has the potential to become a business object. Whether it ends up as a business object is something we'll determine through the rest of the process.

Here's a list of potential high-level business objects from the video rental use case:

StoreAttendant

InventoryInvoice

VideoRental price

CustomerPayment

Front counterSecurity alarm

We'll use this list later on, as we go through the functional use cases and analysis. During that process, we'll identify some of the objects listed here as actual objects in our application; others, of course, won't be required. In this first phase, our objective is to identify any objects within the business domain - not just those that we'll code into our application.


Functional Use Case

A functional use case is much more specific than the requirements use case we just saw. Most requirements use cases will actually encompass a number of more detailed functional use cases. These are used less for determining system requirements than for defining required functionality. They are also very useful for identifying business objects.

Let's use a single sentence from the Video Rental requirements use case, above, as an example of a functional use case:

The customer then brings the videos to the front counter where an attendant

identifies the customer, makes note of each video, and calculates the total rental price.

There are a lot of things going on here, but the description is not nearly detailed enough to fully describe the functionality. All it does describe, of course, are the requirements. So let's develop a use case that defines the functionality to support these requirements:

Functional Use Case for Video Checkout

The customer places the videos on the counter and provides an ID card to the attendant. The attendant chooses the video rental menu option on the screen, then enters the customer's ID number by scanning the ID card with a barcode reader. This automatically retrieves the customer's name and address from the database, and displays the information on the screen. The entry point is left in position to enter the first video's ID number.

The attendant takes each video, in turn, and uses the barcode reader to enter the video's ID number by scanning the video jacket. This automatically retrieves the video's description and rental price, and displays them on the screen next to the video ID number.

As each video ID number is entered, a subtotal price is calculated and displayed at the bottom of the screen. Based on the subtotal price, the system automatically calculates and displays the tax and the end total.

Of course, this is only a small part of the whole picture. If this was more than just an example, we would continue through the requirements use case, building more functional use cases to fill in the rest of the details. But let's keep up our momentum: it's time to identify some real functionality from this use case.


Identifying Functionality

Our functional use case for Video Checkout, above, is still focused on the business process; but its level of detail is sufficient for us to be able to define specific functionality - and get right to the business objects that we'll need to create. First, let's go through and list the functionality that's described:

  • The customer has an ID number

  • The video rental transaction is handled via a specific screen

  • The customer's ID number is sufficient to retrieve the customer's information

  • The customer's name and address are in the database

  • Videos each have an ID number

  • Once the customer data is shown, the cursor moves to the video entry field

  • The video's ID number is sufficient to retrieve the video's information

  • The video's title and rental price are in the database

  • The system must keep a running subtotal for all videos entered

  • The system must calculate the tax

  • The system must calculate a total rental fee

Notice that not all the functionality that's been described fits into business objects. For instance, the requirement that 'once the customer data is shown, the cursor moves to the video entry field' is not a business rule, but a presentation rule. This functionality is no less important to the end user of the system, but we do need to make sure that presentation rules are handled by the user-interface, not the business objects.


Identifying Business Objects

Now let's find the business objects that are required to support this functionality. As before, a good technique for this process is to pick out the nouns from the use case. Of course, this could get carried to an extreme, so your judgement is required. What we need to do is to pick out possible real-world objects that might need to be modeled in the software:

CustomerID number

VideoBarcode reader

ID cardDatabase

AttendantSubtotal price

ScreenTax

ID numberTotal Price

Clearly, some of the possible objects we've listed won't end up being created as objects in the software; nevertheless, this is a good starting point. The next step is to determine how these objects relate to each other. During this process, we'll find out which of the objects are important to the software and which are not.


Defining Object Relationships

Although it may sound old-fashioned, it can be very valuable to write the name of each object that we listed on to a small card or piece of paper, one note per object. Then we can use a white-board, or any other large surface we can draw on, and stick the objects to the surface. This lets us move the objects around as needed, and lets us draw lines between the objects to show how they're connected.

There are commercial case tools on the market that make this very easy to perform on a computer. Rational Rose for Visual Basic and Microsoft Visual Modeler are good examples. These programs can be very useful, as they offer the same abilities as putting cards on a white-board, but allow us to save the diagrams to disk, add comments to our objects and relationships, and sometimes even generate code based on the object design.

Once our objects are written down and placed on a surface, we're ready to move on and figure out how they relate to each other. This is fairly straightforward, since all we need to do is go back through the use case and look for verbs and other connecting phrases between the nouns. Again, this could get carried to an extreme, so some judgement is required!

Types of Relationships

There are some key types of relationships that we need to find. Other relationships exist, and are important, but these are ones we need to make sure we get:

Relationship | Description/Example
Ownership (owns, has) | Parent-child relationships
User (uses) | One object uses or employs the services of another object
Aggregate (contains, made of) | One object is made up of other (subordinate) objects
Generalization (is a) | One object is a specific type of another object

Something else to keep in mind is that we might need to add some new objects as we go along. Picking out the nouns from the use case is great, but the use case might have implied some other objects that we don't have yet. For instance, we may find that we need objects to represent some of the verbs in our use cases. That's fine, just don't be surprised if it happens.

Following the same logic, there will be objects that we don't need, and we'll be getting rid of those.

Example Relationships

So let's look through the use case and pick out some relationships:

  • A customer owns an ID card

  • An ID card contains a customer number

  • A customer has an ID number

  • A video has an ID number

  • Attendant uses the screen

  • Attendant chooses a menu option

  • Barcode reader scans the ID card

  • The screen displays (or uses) the customer information

  • The screen displays (or uses) the video information

  • The screen displays (or uses) the subtotal, tax, and total

  • The subtotal price uses the video prices

  • The tax uses the subtotal price

  • The total is made up of the subtotal and tax

There are other relationships that we could identify; these are just some examples that illustrate the common relationships we'll encounter. This information is more clearly presented in diagrammatic form:

The figure shown here was drawn using Microsoft Visual Modeler, part of the Visual Studio package. The notation used is the Universal Modeling Language (UML). Appendix B contains a brief explanation of UML as used here.

Eliminating Unneeded Objects

Now the challenge is to eliminate the objects that we don't need to put into our software model. There are two main things to look at:

  • Objects that just aren't needed in this particular use case

  • Objects that fit into other layers of the application, such as the presentation layer

The first thing we should do is look at those objects that don't have a lot of relationships in the diagram. Neither the attendant nor the barcode reader are well connected, so they need to be looked at carefully.

It's unlikely that we'll need to have an attendant object to handle the functionality in this use case, since the attendant just interacts with the screen. We won't completely write it off as an object, however, because it was also listed in the requirements use case - so it may be used to provide some other functionality. Still, we don't need it here, so we can remove it from our diagram.

The barcode reader is probably not required either. This is more of a common sense issue, but barcode readers usually just provide input to the software that's indistinguishable from keyboard input, so it's hard to imagine what such a business object would do for our program.

Now look at the objects that the attendant and barcode reader touched. The screen object is still very connected, but the ID card is a borderline case. Since the ID card is just a way of expressing the customer's ID number, we can probably eliminate it from the picture as well.

The next step is to look at each object and eliminate those that belong in either the presentation or data processing layers of the application. In our case, the screen object certainly fits the bill for belonging in the presentation layer.

However, it's very useful to leave something representing the presentation layer in the model, for the sake of clarity. At the same time, we don't want to mistake it to be an actual business object, so we need to keep that in mind when we look at the figure.

After all the changes, our diagram looks like this:

Consolidating Objects

At this point, we've got a pretty clear picture of the objects that we need, and how they're all related to each other. Now is a good time to look at the diagram with a critical eye to determine if there are objects that can be consolidated to form a more general object.

There may also be objects in our diagram that are really attributes of other objects, but we won't worry about those yet. We'll discuss attributes and properties in the next section.

Looking at the diagram, there are three objects that are all very much interrelated. The tax and total objects are both generated upon the basis on the subtotal object. It would make a lot of sense to add a new object to the diagram that would give us a more general view of these objects.

If we look back at our requirements use case, we'll see that there is a potential business object called an invoice.

It is important to keep looking back at the requirements use case, and to look at all the other related functional use cases, to find objects that overlap or have relationships with the objects at hand. Our functional analysis is not happening in isolation, so it's important to regularly stop and take a look at the bigger picture.

The invoice object needs to display the total amount along with some other information, so it's a good candidate to use as an object to contain the total, tax and subtotal objects. Here's a list the relationships for our new invoice object:

  • The invoice has a total

  • The invoice has a subtotal

  • The invoice has a tax

  • The invoice refers to customer

  • The invoice has a list of videos

Looking back at the previous example, we can see how complex the model is for the presentation designer. The screen entity is communicating with a lot of different objects in our model, so it may be very difficult for the programmer of the presentation layer to use our model effectively. In this new model, however, the UI developer only needs to worry about interacting with three objects: invoice, video and customer. All the other objects are available through one of those three.

If we add in the invoice object, we end up with the following diagram for our objects:

At this point, we have a pretty good understanding of the business objects that we need to handle for the functional use case. We may not be done yet, however: it's possible that we'll still need to add or remove some objects as we finish up the analysis.

Now we need to move on to figure out some specifics about the objects; in particular, we need to list the properties, methods, and events that each object needs in order to support the use case's functionality. First, let's look at the properties.


Identifying Object Attributes (or Properties)

Our programs interact with objects using the interface (properties, methods, and events) that the object provides. Properties are the attributes, or elements, that describe an object.

Although the analytical processes to determine the properties and methods are similar and interrelated, it is often easier to start with the properties, keeping the behaviors in mind, and then move on to finalize the methods.

We'll get our properties using two primary techniques:

  • We'll go back to our functional use case to find any properties that we need to meet the requirements listed there.

  • We'll look at the object model diagram from the previous section, since some of the objects in our model might be better expressed as attributes of other objects - something we'll cover in much more detail shortly.

Additionally, some of our objects have relationships to other objects, such as the invoice having a reference to a customer object. In this case, we'll want to add a property to the invoice object so that the customer object is accessible.

As well as the properties needed to fulfil the business requirements, the objects will also have properties to support the way we implement our components. We'll ignore these particular properties for now, and cover them in Chapter 5.

Identifying Attributes - Looking through the Use Case

So, our first technique to identify object attributes involves us going through each object in our model, looking at the relevant use case to decide what attributes are appropriate.

A simple example is the customer object. Looking at the use case, we can see some fairly obvious attributes that will be required; in particular, the name and address. The customer's ID number may also be an attribute, although we've put it into our model as an object. We'll talk about the ID number shortly.

Of course, the address is likely to contain a number of attributes, such as street, city, state, and zip code.

Alternatively, we may determine that we have other addresses from other functional use cases, so perhaps the address is really a separate object. Translating from use cases into an object design is a holistic process: we need to continually look back at other use cases and related object models to see where and how they overlap. This is how code re-use is tied in to the object model we are building.

In this case, we'll just assume that a customer has a single address with a street, city, state, and zip code. Here is a list of the objects from our model, along with the properties that we're likely to derive from the use case:

Object | Object's Attributes
Tax | Tax amount
Subtotal | Subtotal amount
Total | Total amount for invoice
Invoice | Since the invoice isn't actually in our use case, and we added it later, we won't get any properties from the use case
Customer | Name Street City State Zip
Video price | Rental price
Video | Description
ID number | ID Number

Identifying Attributes - Looking at the Object Model Diagram

Our second technique to identify object attributes involves an examination of each object in our object model to make sure that it really is an object - and not just an attribute of a related object. This distinction is somewhat subtle, and requires us to step back and look at the requirements use case and any other related functional use cases that might impact the model.

A good clue to start with is to examine the attributes we've assigned to each object at this point. Any object with just a single property is a good candidate to become a simple property of its parent object. Look at the ID number object. It only has a single property, so does it really warrant the status of being an object, or is it just an attribute of the customer and video objects?

This depends very much on how the ID number object is used in other use cases. In this particular use case, it is simply an attribute - but if it's used the same way elsewhere then it should probably just become a property.

We need to keep in mind that properties are not the only thing that might define an object. As we'll discuss shortly, methods and behaviors can also define an object. Before assuming that the ID number object is just an attribute, we need to ensure that it doesn't provide any behaviors that make it more appropriate to being an object.

In going through the object model, look for objects that have a single property and those that have a parent. A parent is typically an object that has or owns or is made up of the object that we're looking at. Another way to look at it is that the child object belongs to the parent. The following objects look like good candidates to become properties of other objects:

Object | Parent
Video price | belongs to the video object
ID number | belongs to the customer and video objects
Tax | belongs to the invoice object
Subtotal | belongs to the invoice object
Total | belongs to the invoice object

At the end of this process, we have the following objects and properties:

Object | Attributes
Invoice | Tax Subtotal Total
Customer | ID number Name Address City State Zip
Video | ID number Description Price

At this point, we have properties for each of the objects individually; but, according to our diagram, the invoice object contains both the customer and video objects. We need to decide how the presentation layer will get at the customer and video objects through the invoice object. This is a very complex issue with a lot of different solutions.

To give this issue the space it deserves, we'll discuss it fully in the next section of this chapter. Therefore, we'll leave the object properties as they are for now.

Remember that everything we've done so far is tentative; we may still need some of the objects that we've eliminated if we find that they have methods in the following step. Still, it's a pretty good bet that we're close - so it is worth putting together an object model diagram that we can use to figure out the methods:

The object model has been simplified dramatically through this process. The analysis is geared toward building a simple model with just the objects that we need to represent the business process within our application. Since we chose not to remove the screen entity in the diagram during this process, we've also been able to simplify the model so that it will be easy to use when it's time to develop the presentation layer.


Identifying Object Behaviors (or Methods)

Objects do not only have attributes; they also have behaviors. Our programs ask an object to act out its behaviors by using methods. A method is very much like a subprogram, or function, that our program can call; but whereas a subprogram is simply a routine in our program, a method is a routine defined for our object.

Objects have behaviors that help model their role in business process. Very often, they also have behaviors, or methods, that let them interact with the other objects in the model - in ways that aren't necessarily obvious from the use cases.

We're going to discuss business process behaviors in this section. We'll cover object relationship behaviors in the next section.

The technique we need to follow to determine an object's behavior is very similar to that which we've just seen for an object's properties. First, we need to go through the functional use case to find any methods that are required. Then, we need to look at our object model and add any methods that are required for objects to interact with each other.

Identifying Behaviors - Looking through the Use Case

Let's look through each object in our object model and read through the use case to decide what behaviors or actions each object might be able to take. Many business objects primarily provide data, so don't be surprised if many of our objects don't have methods.

Looking at the customer object, we can see that the actual person does a lot of things, like putting the videos on the counter, and providing an ID card. However, our software doesn't need to be aware of these actions - so we don't need to provide methods for them. But our software does need to retrieve and display the customer's name and address once the ID number is entered. Now, since these are attributes of the customer object, it's a good bet that our customer object will have some behavior to load this information.

Next, let's look at the video object. Again, the video object doesn't play an active part in the use case as far as our application is concerned - other than that it needs to load the description and rental price once its ID number is entered.

Our last object is the invoice object. This object isn't directly in the use case at all, but it does have the subtotal, tax and total values as attributes. The use case indicates that these values are calculated, so the invoice object will probably need a method to support this behavior.

At this point, our objects have the following behaviors:

Object | Behavior
Customer | Load customer data
Video | Load video data
Invoice | Calculate subtotal Calculate tax Calculate total

If we look back at the requirements use case, we can see that there are other behaviors that we are not taking into account. For instance, the invoice needs to be printed, and it is implied that the rented videos will be recorded in the inventory system. We don't need to worry about these issues at this point, since they would normally be covered by other functional use cases. This illustrates how closely all the different use cases are related to each other, and how they are all used together to come up with a comprehensive object model.

Identifying Behaviors - Looking through the Object Model

The customer and video objects both have behaviors that comprise loading data from a database. This type of behavior is integral in making objects persistent, as we discussed in Chapter 2. In Chapter 4, we'll get into the code for implementing this behavior. For now, however, we're just interested in defining our objects' interfaces, so we'll add a Load method to support this behavior, and worry about the details later.

We've listed three different calculation behaviors for the invoice object, all of which are closely related. This is an interesting situation that requires some thought. There are three ways to approach these behaviors:

  • Implement three calculation methods

  • Implement a single calculation method

  • Automatically recalculate when properties are changed

When we're making decisions about our objects' interfaces, we need to keep our customer in mind. We have two main design issues: modeling the business entities, and making our objects easy to use at the presentation layer. Here, our customer is the programmer who is writing the user interface.

Implementing three different calculation methods means that the UI programmer needs to understand and call all three methods where appropriate. Since all the calculations are directly tied to each other, it would surely be much better to combine them into a single method for the programmer to call.

Better still, suppose the programmer didn't need to call any method to do the calculation. The calculation would only need to happen when a video was added or removed from the list of rentals. The invoice object owns the list of videos, so it should be able to detect when a video is added or removed - and automatically recalculate the values.

All three solutions are valid, and we could choose any of them and have a working program. We should always strive to provide the simplest object interface for the UI programmer, since it helps them to be more productive and it makes the code less prone to bugs. In this case, the upshot is that we'll opt to automatically recalculate the subtotal, tax and total, so that those values are always correct. This means we don't need any calculation methods on our invoice object.

Ideally, we'd be able to notify the UI when the values have been recalculated, thus making it easy for the programmer to update the display. Fortunately, Visual Basic 5.0 provides the RaiseEvent command to let us do just that.


Notification (or Events)

There are many times when objects can perform some action in response to a property being set or a method being called. This has traditionally been difficult to implement, since an object has usually needed to notify the calling program that the action has taken place.

In the previous section, we discussed the invoice object and how it can detect when a video is added or removed from the list of rentals. Either of these conditions should cause the subtotal, tax and total amounts to be recalculated. The challenge is that the UI needs to be notified that these values have changed so that the display can be updated.

Traditional Solutions

Traditionally, this would have been handled either by having the UI refresh the values when it added or removed a video, or by calling a special Calculate method to force the calculation.

If the UI updates the values when adding or removing a video, we've effectively coded some business logic into the UI. After all, the UI would have to know all the conditions under which the values might change, so that it could know when to refresh the display.

Using an explicit Calculate method is also far from ideal. If we have such a method, then we're relying on the UI code to call it, rather than recalculating our values automatically. If the UI doesn't call the method when appropriate, we could end up in a situation where the subtotal, tax, and total properties on the object would be incorrect. This solution can not only lead to bugs, but it also requires that the UI programmer must have more knowledge about how our object works than we might otherwise prefer (such as a more appropriate time to call the Calculate method).

Callbacks

A more complex solution to the problem would be to have the invoice object call a method of another object and have that object update the UI. This solution solves most of our problems, but it's definitely harder to implement for the UI developer.

To implement this, the UI itself would have to create an object to receive the message sent by the invoice object. Of course, the invoice object would need to know about the UI's object so that it could call it; we'd therefore need to add a method to the invoice object so that the UI could pass a reference to its object!

Once the invoice object had a reference to the UI's object (let's call it Displayer), it could call some predefined method on the Displayer object whenever a message needed to be sent to the UI. This is shown in this diagram:

Let's look through some code that could handle this.

Displayer Object

First off, our UI developer will need to provide an object to update the display. Create an ActiveX DLL project, create a new class module called Displayer, and add this sample code:

Option Explicit

Public Sub UpdateValues()
  ' in this routine we'd update our display
  ' with the properties from the Invoice
  ' object
End Sub

This example assumes that we've implemented our business objects in an ActiveX DLL, and that the UI is in a different EXE or DLL. We'll keep our Displayer class in a separate ActiveX DLL for now, which will allow us to create a simple UI test harness in a Standard EXE. (Alternatively, if we had created our UI in a ActiveX DLL, then the Displayer class could have been included within the main UI project.)

The Displayer class must also have its Instancing property set to 5 - MultiUse so that we can make it available to the Invoice object.

The upshot of all this is that if we did want our Displayer class to be within the main UI program, then our UI project would need to be an ActiveX server - whereas it may have only needed to be a regular program prior to the introduction of the callback mechanism.

Invoice Objects

Within the main DLL project for our business objects, the Invoice class also needs some special code to support the callback. It needs a method that will allow the UI to give it a reference to the Displayer object.

For our current example, add a sample DLL for our business objects, using the File-Add Project menu option, then add a class module, using the Project-Add Class Module menu option, and call that class module Invoice. Now add the following sample code to Invoice:

Option Explicit

Private objCallback As Object

Public Sub SetCallback(Display As Object)
  Set objCallback = Display
End Sub

Performing the Callback

Then, in the routine where the Invoice object does the calculations, we need to call the UpdateValues method of the Displayer object:

Public Sub Calculate()
  ' here we'd do the calculations

  objCallback.UpdateValues
End Sub

Releasing Object References

Finally, we need to make sure that the Invoice object releases its reference to the Displayer object before the Invoice goes away. If we don't do this, we run the risk of having objects hanging around in memory after we've tried to close down the program:

Private Sub Class_Terminate()
  Set objCallback = Nothing
End Sub

The UI Code

Now that all that has been set up, we can write some UI code that creates the Invoice object. Since we're going to use a Standard EXE to hold our sample UI and test our callback mechanism, add a Standard EXE project using the File-Add Project menu option, add a standard module to that project, using the Project-Add Module menu option, and enter this example code:

Private objInvoice As Invoice
Private objDisplayer As Displayer
Set objDisplayer = New Displayer
Set objInvoice = New Invoice

objInvoice.SetCallback objDisplayer
objInvoice.Calculate

Notice that, as well as creating an Invoice object, we create a Displayer object, and give the Invoice object a reference to it.

You'll need to make a reference to the Invoice object from the UI project. Select the UI project in the Project window, and use the Project-References menu option to select the project containing our Invoice object.

Since we've created a separate EXE for the UI test harness, we also need to add references to the projects containing the Displayer object and the Invoice object. Select the UI EXE project in the Project window, and use the Project-References menu option to select the appropriate references to the other projects.

If you run the sample program as it stands, you won't see much happen: but if you step through the program execution, you'll see the callback mechanism alive and well. The UI code successfully establishes the Displayer callback method to itself before it calls the Invoice object's Calculate method. Within the Calculate method, the callback is then made to the Displayer class's UpdateValues routine.

This solves our problem, in that it allows an object to notify our program when something has happened. In our example, the Invoice object notifies the UI object that Calculate method has been called. In some ways, this is very attractive, since the interaction between the two objects is very well defined, and the UI can give the Invoice object a reference to any object that implements an UpdateValues method. Since this technique uses regular method calls between the two objects, we can write our code to pass any parameters that our program might need.

On the other hand, this solution is somewhat complex: it forces the UI developer to add a new class to the project, or make the UI program an ActiveX server. It also tightly couples the business object to the UI implementation, since it requires that the UI provide an object with a specific method.

Raising Events

Visual Basic 5.0 introduced the ability to raise events from our objects. This means that we can write code in our class module that will raise an event in the calling code, just as a button control can raise a Click event in a form.

This is done by introducing three new keywords to the Visual Basic 5.0 language. In our class module, we need to use the Event and RaiseEvent keywords, respectively, to declare and raise events. In the calling code (Form or Class module), we need to declare the object using the new WithEvents keyword.

Sticking with the Invoice object example, we could have the Invoice object raise an event when the subtotal, tax and total amounts have changed. Then the UI programmer would just need to write code behind the event to update the display. This is a very nice model, since all Visual Basic programmers are familiar with writing code to respond to events - so the learning curve for the UI programmer is very small.

Invoice Objects

Inside our Invoice object, we need to have a line to declare our event:

Public Event UpdateValues()

Then in the routine where we calculate the amounts, we can add a line to raise the event:

Private Sub Calculate()

  ' here we'd do the calculations

  RaiseEvent UpdateValues

End Sub

All references to our objCallback object are now unnecessary, and we no longer need our SetCallback routine, since we've managed to create the same mechanism using events.

The UI Code

The UI programmer just needs to make a change to the declaration of the Invoice object to include the WithEvents keyword:

Private WithEvents objInvoice As Invoice

At this point, the objInvoice object will appear in the Object dropdown in the upper-left of the code window, along with any other controls or objects available to the module. If we choose objInvoice, then the Procedure dropdown in the upper right will list all the events that are declared in the Invoice class - just UpdateValues in this case. This means we can write a regular event procedure:

Private Sub objInvoice_UpdateValues()
  ' here we'd put the code to update the
  ' values on the display
End Sub

All references to our objDisplayer object are now unnecessary; and, since we no longer need our Displayer class module, the UI no longer needs to be an ActiveX server.

This solution is not very different from implementing a callback, but it does have some significant advantages. The UI programmer doesn't need to implement a special class. In fact, the UI programmer can draw on existing knowledge and techniques to write code behind the object's events - just as if they were events from a control on a form. Furthermore, the Invoice object author doesn't need to worry about being passed some external object, or any specifics about the calling program at all. They may appreciate that.

The bottom line is that the RaiseEvents command works independently of whether anyone actually receives the event that's raised. This means that we've effectively separated the UI programmer's tasks from the object developer's tasks.


Other Use Case Formats

The concept of a use case is pretty broad. There are many different things called use cases that are written up in many different ways. That's fine - it's the concept of having a detailed description of a business process that is important.

The format that we used earlier in this chapter is a simple prose or text description of the process. A couple of other popular formats that we might encounter include: a step-by-step outline in a table format, a flowchart or a flow diagram.

Functional use cases are typically grouped together with the higher-level requirements use case from which they flowed. All of these use cases, when combined, provide a solid and traceable base of documentation for the subsequent design, development, testing and documentation of the system.

Functional use cases frequently include user-interface mock-ups, often created in Visual Basic to look somewhat like the screen that the users will see at the end. This can be very useful, because it can help the users visualize the flow of the system as they proof our use case descriptions.

Developing prototype screens using Visual Basic can be very powerful. Not only can we create screens to assist our users in visualizing the system, but, if we're careful, we can get a significant head start on the development of the UI itself. Visual Basic is not only good for prototyping - it's also good for easily converting prototypes into full-blown applications without having to start over from scratch.


Object Relationships

At this point, we've gone through our use case scenarios, and we've defined all the objects that we need to model our business process. We've even gone so far as to determine how all the objects are related to each other at a high level.

Thus far, however, our objects don't have any properties or methods that actually let them interact with each other. Essentially, we've identified relationships between objects; but we haven't added any way to implement those relationships.

Earlier in the chapter, we discussed the common types of object relationships:

Relationship | Description/Example
Ownership (owns, has) | Parent-child relationships
User (uses) | One object uses or employs the services of another object
Aggregate (contains, made of) | One object is made up of other (subordinate) objects
Generalization (is a) | One object is a specific type of another object

Let's go through each relationship and discuss how to implement that relationship with Visual Basic.


The Ownership Relationship - Implementing Collections

Ownership really defines a parent-child type relationship between objects. There may be one child, or this may be a one-to-many relationship. In this section, we're going to focus on the situation where one parent has many children.

If there can only be one child, it's often more appropriate to view the relationship as a form of aggregation, which we'll talk about later in this chapter.

This ownership relationship is the basis for most object hierarchies we'll find in Windows applications. It is usually viewed as a collection of child objects that are owned or controlled in some way by the parent. The best way to manage this type of relationship is to build a collection class to contain the child objects, and then implement a read-only property on the parent object to provide access to the collection object.

The Invoice Object

If we consider our video store example, our Invoice object is likely to have a list of line items: one line item for each video that the customer rents. This is a parent-child type relationship, where it would be good to have a collection of LineItem objects available through the Invoice object.

In our Invoice object, we'll add a property so that the calling program can access the collection of videos. We'll call the new property LineItems, since it will be a list of LineItem objects. The code to handle this in the Invoice class would look something like this:

Option Explicit

Private WithEvents objLineItems As InvoiceItems

Private Sub Class_Initialize()
  Set objLineItems = New InvoiceItems
End Sub

Public Property Get LineItems() As InvoiceItems
  Set LineItems = objLineItems
End Property

Private Sub objLineItems_AddItem(strID As String)
  CalculateAmounts
End Sub

Private Sub objLineItems_RemoveItem(strID As String)
  CalculateAmounts
End Sub

Private Sub CalculateAmounts()
  ' here we'd recalculate the subtotal, tax
  ' and total amounts for the invoice
End Sub

The LineItems property is the key, since it allows the calling program to get at the InvoiceItems collection. We've also included events so that the Invoice object will be notified when a LineItem object is added or removed from the collection. This ties in with our earlier discussion about raising events to recalculate the subtotal, tax and total amounts on the invoice.

The InvoiceItems Collection Object

The InvoiceItems collection class is based on Visual Basic's native Collection class. Our collection uses a Visual Basic Collection object, internally, to store all the LineItem objects - but we've enhanced the Add method so that it only supports LineItem objects.

The normal Visual Basic Collection will accept just about anything to store, which doesn't bode well if we only want to get LineItem objects.

Here's what the code to support our InvoiceItems collection object might look like:

Option Explicit

Event AddItem(strID As String)
Event RemoveItem(strID As String)

Private colItems As Collection

Private Sub Class_Initialize()
  Set colItems = New Collection
End Sub

Public Function Add(strID As String) As LineItem
  Dim objItem As LineItem

  Set objItem = New LineItem
  objItem.Load strID
  colItems.Add objItem, strID
  Set Add = objItem
  RaiseEvent AddItem(strID)
End Function

Public Sub Remove(ByVal varIndex As Variant)
  Dim strID As String

  strID = colItems.Item(varIndex).ID
  colItems.Remove varIndex
  RaiseEvent RemoveItem(strID)
End Sub

Public Function Count() As Long
  Count = colItems.Count
End Function

Public Function Item(ByVal varIndex As Variant) As LineItem
  Set Item = colItems.Item(varIndex)
End Function

Public Function NewEnum() As IUnknown
  Set NewEnum = colItems.[_NewEnum]
End Function

In this code, we've simply encapsulated a Collection object, colItems, by creating a Private variable and exposing our own Add, Remove, Item, and Count elements.

The key here is that the new Add method only supports LineItem objects, so that no unexpected object types can get into our collection. The Add method is also responsible for creating the LineItem object, and arranges to load any data based on the ID value supplied - by calling the Load method.

  Set objItem = New LineItem

  objItem.Load strID

Although we can't see it in the code, the Item method has been made the default by using a new capability of Visual Basic 5.0. This means that where we'd previously have written code like this:

  Set objLineItem = objItems.Item(1)

We can now write the same line like this:

  Set objLineItem = objItems(1)

To set a method as the default,

choose the Tools-Procedure Attributes menu, and click on the Advanced button when the dialog comes up:

In the Name field, choose the method name to be changed. Then, in the Procedure ID field, choose the (Default) setting and click OK.

Our new class also contains a NewEnum method. This is a special method that allows the calling code to use the For Each...Next style of accessing the contents of our collection. This means that we can write client code like this:

  For Each objLineItem In objItems

    Debug.Print objLineItem.Price

  Next

Until Visual Basic 5.0, the For Each structure was only available for native Visual Basic Collection objects. Now, by implementing the NewEnum method, we can make our user-defined Collection objects support the For...Each structure as well.

Our NewEnum method simply returns a hidden object within the native Visual Basic Collection class, called _NewEnum. Like the Item method, we need to use the Tools-Procedure Attributes menu option to set some special values for the NewEnum method. The following diagram shows the settings that are required:

The Procedure ID field is set to –4, which is the value required to work with For...Each structures. Also, the Hide this member box is checked, to make our method hidden in the COM type library.

Notice that the InvoiceItems class declares and raises two events: AddItem and RemoveItem. These events allow the Invoice object to know when the calling program has added or removed a video from the collection, so that the invoice amounts can be recalculated.

The LineItem Object

We'll keep the LineItem object simple. We'll just give it an ID and a Price property, along with the Load method. Here's the code:

Option Explicit

Private strID As String
Private dblPrice As Double

Public Sub Load(ID As String)
  strID = ID
  ' eventually there will be code here to go
  ' load the video information from the
  ' database, but for now we'll just make up
  ' a price
  dblPrice = 1.99
End Sub

Public Property Let ID(strValue As String)
  strID = strValue
End Property

Public Property Get ID() As String
  ID = strID
End Property

Public Property Let Price(dblValue As Double)
  dblPrice = dblValue
End Property

Public Property Get Price() As Double
  Price = dblPrice
End Property

The LineItem object has no extra code to support collections or any other relationships. The object is entirely designed around modeling the real-world video entity.

The Calling Program

In order to bring this whole thing together, let's look at some code that will add some line items to the invoice and then interrogate the price of the first item:

  Dim objInvoice As Invoice

  Dim strPrice as String

  Set objInvoice = New Invoice

  objInvoice.LineItems.Add "1"

  objInvoice.LineItems.Add "2"

  strPrice= objInvoice.LineItems(1).Price

Notice how we've just used the InvoiceItems method as though it were a collection, calling its Add method for each video.

This approach to implementing parent-child relationships is very powerful, and yet it provides a very easy interface for the end programmer who'll be using our objects to develop the application.


The User Relationship - Calling Objects

When an object holds a reference to another object, and calls its properties or methods, we have a uses type relationship. Another way to look at this is as a client-server relationship between the two objects. The client calls properties or methods on the server, and gets an appropriate response.

This is the simplest of the relationships, and is one that virtually all Visual Basic programmers are familiar with - so we won't spend much time discussing it.

The user or client simply declares a variable and creates an object:

Dim objItem as LineItem

Set objItem = New LineItem

And then the user calls the object's properties or methods:

objItem.Load "ABC"
MsgBox objItem.Price

When the user is done with the object, they just releases it by setting its reference to Nothing:

Set objItem = Nothing

Probably all of us have used objects, or at least controls, in the Visual Basic programs we've written. Having an object use another object is no more difficult than using an object from a form or code module.


The Aggregate Relationship - Combining Objects

There are many times when an object is composed of other objects, in whole or in part. For instance, our Invoice object is partially composed of the Customer object. Without the Customer object, our Invoice doesn't have enough information to be useful. The Customer is subordinate to the Invoice, but the Invoice can't exist without the Customer.

There are two different approaches that we can use to handle this situation. The first of these is simple aggregation; the second involves combining interfaces.

Our discussion will therefore run as follows:

  • We'll look at a simple aggregation example

  • We'll alter that example to use a combined interface technique

  • We'll investigate how one object might expose another object's data
    as a read-only property

Technique 1 - Simple Aggregation

The first approach we'll look at is simple aggregation. With this approach, we can simply expose the subordinate objects as properties of the top-level object. For instance, look at the following code:

Option Explicit

Private objCustomer As Customer

Private Sub Class_Initialize()
  Set objCustomer = New Customer
End Sub

Public Function Customer() As Customer
  Set Customer = objCustomer
End Function

All we've done, here, is create a private Customer object, and allowed the calling program to gain access to it through our Customer method. This is very simple to implement, and it gives the calling program full access to any of the objects we've aggregated into our top-level object.

There are, however, a couple of drawbacks to simple aggregation. The most obvious problem is that the calling program can do virtually anything to the aggregated object. In the example above, the calling program has full access to the Customer object, so it can change any properties or call any methods. In some cases, this might be fine, but we may want greater control over what the calling program can do, such as validation and error handling.

When we're designing an object model, it's important to assume that everything will get misused or called incorrectly. Objects need to protect themselves, and if an object is composed of other objects then it needs to protect the subordinate objects as well.

Technique 2 - Combining Object Interfaces

Now that we've seen how easy it is to implement simple aggregation, let's look at an approach that gives us more control over how the client code can use the aggregated object.

Although this doesn't follow true object-oriented design, it's often a better idea to simply merge the properties and methods of all the objects into a single object interface. With this approach, we can still have the aggregated objects instantiated inside our top-level object, but we don't make them directly available to the calling program.

Consider the following code from the Invoice class:

Option Explicit

Private objCustomer As Customer
Private curTax As Currency

Private Sub Class_Initialize()
  Set objCustomer = New Customer
End Sub

Public Property Let Name(strValue As String)
  objCustomer.Name = strValue
End Property

Public Property Get Name() As String
  Name = objCustomer.Name
End Property

Public Property Get Tax() As Currency
  Tax = curTax
End Property

Note that this class is made up of both its own data and the Customer object; but rather than exposing the Customer object itself, this implementation has its own Name property that just calls the Customer object's Name property. This technique passes a lot of control to the Invoice object - at the expense of object-oriented philosophy.

Of course, this approach also has some drawbacks. If an aggregated object's interface changes, perhaps by adding a new property, then the new interface elements aren't immediately available to client programs. In that situation, we would need to add the new elements to the top-level object's interface first. With simple aggregation, on the other hand, where we would expose the aggregated object as a property, this problem would never arise.

Technique 3 - Combining Data

As we've seen, simple aggregation and aggregation through combined interfaces are very good ways to solve the problem where one object relies on another for data or functionality. Unfortunately, neither solution is truly ideal in a client/server setting.

Let's diverge a bit from object theory, and look at a common scenario in business programming. Most of us work with data that is stored in relational tables. Even if we choose to represent that data as objects, such as Customer or Invoice, we'll often run into cases where the data needs to be consolidated (or aggregated) together across object boundaries.

With our current example, the Invoice object needs data from the Customer object to function - in our case, the Customer object's Name property. Essentially, we're trying to consolidate some of the Customer object's data into the Invoice. So far, we've done that using a couple different techniques to implement aggregation.

Unfortunately, this can be very inefficient. In order to get the Name property from the Customer object, our Invoice object has to load the entire Customer object from the database. This means that we're loading two whole objects - one of them just to get a single property value. Most relational database engines can pull information together much faster than we can when we load individual objects and put them together ourselves.

Let's take an example. Here's a functional use case from the Rental Analysis requirements:

Consolidated Reporting

This use case covers the specific steps used to consolidate the rental counts and prices for each video.

The system must scan all invoice detail lines for each video. Each invoice for a video will count as a single rental, and so the system must count the invoices. For this analysis, the system must also calculate an average rental price, based on each invoice for the video. Prices may change over the life of the video, and so the analysis needs to use the average price.

Once the system has calculated a video's rental count and average price, it needs to provide these numbers - along with the name of the producing studio and the video's category for analysis. The studio and category are stored with the other video information.

We'll discuss two different ways of solving this business situation. First, let's look at how we might solve it using pure aggregation of objects; then, we'll look at an alternative solution that provides much better performance.

Object Method

Looking at this problem with a pure object focus, we can quickly pick out the LineItem and Video objects as containing the data we need. We'll add in a VideoData object, which will contain the business logic to accumulate all the data together. So far, we have the objects and properties as shown in the table on the following page.

Using objects, our VideoData object would contain the logic to load all the LineItem objects for each invoice, and accumulate a rental count and average price for each Video. It would also aggregate the Video object to get the Studio and Category data.

In Chapter 4, we'll discuss a couple of efficient techniques for loading and saving object data in a database. As good as these are, they don't compare to the performance we can get by processing a lot of data in the data tier.

Object | Properties
LineItem | VideoID Price
Video | VideoID Studio Category
VideoData | VideoID RentalCount AveragePrice Studio Category

It's relatively inefficient for the VideoData object to load all the LineItem objects from the database. The rental count and average price can be calculated with a simple SQL statement in the database itself, and this would be much faster. With an extra JOIN, the SQL statement can pull in the Studio and Category data without even having to load a separate Video object.

Database Method

Following a database train of thought, we would have one type of object: VideoData. This object would be loaded directly from the database using a SQL statement to pull together all the data at once, very efficiently. The following is an example SQL statement that we could use in Microsoft Access to achieve this:

SELECT InvoiceDetail.VideoID, Count(InvoiceDetail.VideoID) AS CountOfVideoID, Avg(InvoiceDetail.Price) AS AvgOfPrice, Videos.Studio, Videos.Category
FROM InvoiceDetail INNER JOIN Videos ON InvoiceDetail.VideoID = Videos.VideoID
GROUP BY InvoiceDetail.VideoID, Videos.Studio, Videos.Category;

The following figure (overleaf) shows this query in Access.

Of course, there are some drawbacks to building objects this way rather than through regular aggregation. Suppose the LineItem object had business logic behind the Price property. By pulling the price directly from the database, we will have bypassed that logic. This might force us to replicate the code inside the VideoData object. In that case, we'd want to think hard about which solution is the most appropriate.


The Generalization Relationship - Containing Objects

Generalization is an analysis technique used to find common ground between a number of objects. Once we've established that a group of objects have a set of common behaviors and properties, we can create a more generalized class to represent all those common elements. We can then use that generalized class as a base from which we can create all the other, more specialized, classes.

We can view this new, general class as a sort of parent to all the more specialized classes. The process of creating the specialized classes from the general class is called inheritance - something we discussed in a different context in Chapter 1. When we view our generalized class as a parent of the specialized classes, we're creating what's called a class hierarchy.

A class hierarchy is like a family tree for our objects. As we trace back from one object to the next, we become more and more general. Let's look at a quick example to see what this means:

This diagram demonstrates how a class hierarchy might appear for various types of Person. If we look at an Hourly employee, we can see that they're a specialized type of Employee. In other words, the Employee class is a parent to Hourly and Salaried. We can follow the flow all the way back to the most general class, Person. All of these classes are derived from the Person class.

Generalization is a very powerful analysis technique. By identifying the commonalities between various objects, we provide ourselves with an opportunity to achieve a high level of reuse. Since we can implement all the common functionality in one class, we can then use that functionality in all the more specialized classes. The more specialized classes inherit the functionality from the generalized class.

Inheritance is the technique used to implement objects that are identified using the generalization technique. Visual Basic 5.0 doesn't provide a direct mechanism to support inheritance, but it's such an important concept that we need to provide it. As we discussed in Chapter 1, we can use containment and delegation together to simulate inheritance in Visual Basic.

To see how we can implement inheritance, let's use an example from our video rental store. Suppose the store decides to not only rent videos, but also to sell popcorn, candy bars and other snacks.

Looking for Common Ground

We've already considered that our Invoice object has LineItem objects to reflect the videos that are rented. Now we're saying that we've got two different types of line item - a video rental line item and a line item for snacks the customer wants to purchase.

Let's look at the possible properties and methods for the video rental line item, which we'll call RentalItem:

Properties | Methods
Title | CheckOut
Price | MarkAsPaid

Now let's look at the possible properties and methods we might find in the line item to purchase a snack (PurchaseItem):

Properties | Methods
Price | ReduceInventory
TaxableFlag | MarkAsPaid

We have a couple of indications that we may be able to come up with a more general class from these two. First off, we know they are both used in a similar way - they're both line items of an Invoice object. Secondly, in looking at their respective properties and methods, we can see that they share some interface elements.

Taking the common properties and methods from each, we derive the following list:

Properties | Methods
Price | MarkAsPaid

We can look at this as being a new, more general line item object, called LineItem. If we then use LineItem as a base from which to implement both the RentalItem and PurchaseItem objects,

we'll have a class hierarchy.

This diagram illustrates how each of our specialized line item classes, RentalItem and PurchaseItem, will inherit behaviors from the more general LineItem class.

Now let's implement this relationship using Visual Basic. To start with, we'll create our general class, LineItem. Once that's done, we'll use it as a base class to implement the RentalItem class. Once we've seen how to implement RentalItem, the implementation of the PurchaseItem class would be trivial, so we won't rehash the same material again.

LineItem Class

Our general LineItem class will implement the properties and methods that are common to all line items. In this case, we're talking about a Price property and a MarkAsPaid method. Let's take a look at the code for the LineItem class:

Option Explicit

Private dblPrice As Double
Private flgPaid As Boolean

Public Property Get Price() As Double
  Price = dblPrice
End Property

Public Property Let Price(dblValue As Double)
  dblPrice = dblValue
End Property

Public Sub MarkAsPaid()
  flgPaid = True
End Sub

Private Sub Class_Initialize()
  flgPaid = False
End Sub

This code is very straightforward. Our Price property merely stores and retrieves its value from the dblPrice variable, while the MarkAsPaid method simply sets the flgPaid variable to True. Obviously, any real business object would be more complex, but this should be enough code to demonstrate how inheritance works.

RentalItem Class

Now we're getting into the interesting part. Since Visual Basic doesn't directly support inheritance, we'll use a combination of containment and delegation to simulate it. We covered this briefly in Chapter 1, but we'll get a good taste for the process as we implement the RentalItem class.

Setting up the RentalItem Class

Before we inherit the behaviors of the LineItem class, let's set up the basics of our RentalItem class. We don't need to worry about the Price property or the MarkAsPaid method, since we'll inherit them from the LineItem class. We do need to implement the Title property and CheckOut method however:

Option Explicit

Private strTitle As String
Private flgCheckedOut As Boolean

Public Property Get Title() As String
  Title = strTitle
End Property

Public Property Let Title(strValue As String)
  strTitle = strValue
End Property

Public Sub CheckOut()
  flgCheckedOut = True
End Sub

Private Sub Class_Initialize()
  flgCheckedOut = False
End Sub

We've kept this code very simple as well. The Title property just stores and retrieves its value from the strTitle variable, with the CheckOut method setting the flgCheckedOut variable to True.

Containing a LineItem Object

With the basics of the class down, we can move on to inherit the LineItem class's functionality. The first thing we need to do is use the technique of containment to make our RentalItem object contain a LineItem object. This means declaring a Private variable to hold the reference to our LineItem object. Add the following line to the RentalItem class:

Option Explicit

Private strTitle As String

Private flgCheckedOut As Boolean

Private objLineItem As LineItem

We also need to create an instance of the LineItem class. The best place to do this is in the Class_Initialize routine, so the object is created right up front and is available any time we need it. Add this line to the RentalItem class:

Private Sub Class_Initialize()

  flgCheckedOut = False

  Set objLineItem = New LineItem

End Sub

Inheriting Behavior Using Delegation

Our RentalItem object now has access to its own private LineItem object. Rather than implementing its own Price property, it can rely on its LineItem object's Price property to do the work. The same goes for the MarkAsPaid method, where the RentalItem object can delegate the work to its private LineItem object.

Of course, the LineItem object is Private, so any client code working with our RentalItem object won't be able to get at it. In order to make a Price property and MarkAsPaid method available to the client code, our RentalItem object will need to implement them. The implementation is simple however, since we'll just delegate the work down to our private LineItem object.

Add this code to the RentalItem class:

Public Property Get Price() As Double
  Price = objLineItem.Price
End Property

Public Property Let Price(dblValue As Double)
  objLineItem.Price = dblValue
End Property

Public Sub MarkAsPaid()
  objLineItem.MarkAsPaid
End Sub

When the client code calls the MarkAsPaid method, for instance, our RentalItem code merely echoes the call to the LineItem object's MarkAsPaid method, rather than trying to do any work itself.

Trying It Out

Let's take a quick look at some client code that we might use to work with the RentalItem object at this point. Rather than creating a UI for such a simple example, let's look at how we might test this object in Visual Basic's Immediate debug window. With our two classes in place, we can enter the following into the Immediate window:

set x=New RentalItem

x.Price=1.99

print x.Price

 1.99

x.MarkAsPaid

x.CheckOut

x.Title="Video X"

print x.Title

 Video X

This example shows how we're able to work with the properties and methods from the RentalItem object itself, as well as those from the LineItem object. To the client code, there is no difference at all. This is exactly the behavior that we'd get using actual inheritance, were it available.

This would, of course, be a lot easier if Visual Basic directly supported inheritance; but, at the same time, this shows that we can do inheritance in Visual Basic - after a fashion. Since generalization is such an important part of object design, it's very important for us to be able to implement it in some manner.


Summary

In this chapter, we've discussed the analysis and design process for developing a logical business object model. Our discussion included:

  • Requirements use cases (or scenarios)

  • Functional use cases

  • How to identify objects from our use cases

  • How to determine the properties, methods and events for our objects

  • Object relationships and how to implement them using Visual Basic

Object-oriented analysis and design techniques provide us with very powerful and useful tools. These techniques can be useful whether we're going to implement our application using object technology or not, since they offer a general way to model our business entities and processes.

From here, we'll move on to look at implementing robust, real-world business objects and components using Visual Basic. Then, in Chapter 5, we'll start to tie everything together as we implement a full-blown example application based on the video rental store example that we studied in this chapter.


Professional Visual C++ 5 ActiveX/COM Control Programming

Published By
Wrox Press Ltd.
30 Lincoln Road, Olton, Birmingham, B27 6PA
Printed in Canada
Copyright © 1997 by Wrox Press

All rights reserved. No part of the contents of this book may be reproduced, stored in a retreival system or transmitted in any form or by any means – electronic electrostatic, mechanical, photocopying, recording or otherwise, without the prior written permission of the publisher except in the case of brief quotations embodied in critical articles or reviews.

The author and publisher have made every effort in the preparation of this book to ensure the accuracy of the information. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Wrox Press nor its dealers or distributors will be held liable for any damages caused or alleged to be caused either directly or indirectly by this book.

ISBN 1-861000-37-5

Credits:

Authors:

Sing Li

Technical Editors:

Julian Dobson

Technical Reviewers:

David Gardner
Zor Gorelov
Christian Gross
Richard Harrison
Matt Telles

Development Editors:

John Franklin

Cover/Design/Layout:

Andrew Guillaume
Graham Butler

Copy Edit/Index:

Simon Gilks
Dominic Shakeshaft


Distributed Objects With DCOM

We're about to experience one of the major benefits of designing component objects based on COM technology. Our objects will become network-ready without any extra work! We'll make this happen in this chapter. We'll also take a more in-depth look at the technology behind this magic: DCOM (or Distributed COM). We'll investigate how DCOM provides an object-based communications service to both DCOM-aware and legacy COM-only objects. We'll attempt to answer the question: why DCOM? We'll examine a couple of the nuts-and-bolts technologies beneath the DCOM covers that may affect us when creating DCOM based applications. And we'll briefly examine the question of security, just enough to understand some of the security fences that our ActiveX control has to jump over during a typical deployment situation, leaving most of the details for Chapter 9. Penultimately, we'll take a cold, hard look at some of the shortcomings of creating distributed component-based applications using today's technology. Coverage will be given to some upcoming technologies that Microsoft is working with to overcome some of these difficulties.

Putting some of our new knowledge to work, we will DCOM-enable our ATL based Dept object server. We'll walk through a step-by-step experiment configuring a network with three machines and testing our Aberdeen & Wilshire Calendar application with cooperating components running on the network of machines.


Distributed COM

DCOM is just COM with a longer wire.

This is a perfect way to describe how DCOM provides extension to basic COM that we have grown to know and love. By examining how COM operates when a client interacts with object instances served through a local .EXE server, we'll be able to see how DCOM actually does its job.


Obtaining DCOM

At the time of writing, DCOM is a standard part of the Windows NT 4.0 distribution, but is an add-on for Windows 95. DCOM for Windows 95 is expected to be included as a standard system component of the next major release of the Windows 95 operating system.

A copy of the released version of DCOM for Windows 95 is also included on the Visual C++ 5 Professional Edition CD.

You can download the latest version of DCOM for Windows 95 over the Internet at the following URL:

http://www.microsoft.com/oledev/olemkt/oledcom/dcom95.htm

A utility that you'll need to configure DCOM for Windows 95 is called the DCOM Configuration, and can also be downloaded from the same URL.

Even if you've installed Windows NT 4.0 release distribution, it's important that you have applied the latest Service Pack for the operating system. As of the time of writing, Service Pack 2 for Windows NT 4.0 is available. This Service Pack brings the DCOM component and related libraries up to the same level as the DCOM for Windows 95 release. You can find the Service Pack at the URL:

http://www.microsoft.com/ntserversupport/Default-SL.HTM

To bring your development environment up to the same level as these DCOM products, you'll need the Win32 SDK Update for Windows NT 4.0 Service Pack 2. This update is delivered as a standard part of the Microsoft Developer's Network subscription CD, the April 97 edition or later. It can also be downloaded at the following URL:

http://www.microsoft.com/msdownload/platformsdk/sp2sdk.htm

Once you have all the above pieces assembled together and installed on your system, you're ready to explore the capabilities of DCOM by following through the code samples. Let's first look at the installation process for DCOM for Windows 95.


Installing DCOM

Installing DCOM for Windows 95 is quite straightforward, simply click on the Dcom95.exe distribution executable and it will start (and complete) the installation on your system. You'll need to reboot the system once. During installation, DCOM for Windows 95 will backup all the system files that it replaces. Your system can be restored to the original condition should you wish to uninstall DCOM for Windows 95. This can be done through the Add/Remove Programs applet within the control panel. There exists no known reason why you may want to remove it. However, as with most system upgrades, it's possible that some yet untested application software may not work properly with it.

After installation, the only directly user accessible application is a configuration utility called Dcomcnfg.exe. You can try running it, but you may get the following message depending on how your computer is configured on the network.

Dcomcnfg.exe is coded in a way that it will not start if user authentication against a Windows NT Server is not configured for your system. This isn't actually a requirement for the proper operation of DCOM for Windows 95, and the restriction may be lifted in a future version of the Dcomcnfg.exe utility (watch the URL in the last section for the latest version).

DCOM for Windows 95 also installs two other system EXEs: Rpcss.exe and Dllhost.exe. Rpcss.exe is the DCOM enhanced System Control Manager; the Dllhost.exe utility surrogate which allows in-proc COM servers to be executed out-of-process or remotely across the network. We'll take a look at Dllhost.exe a little later on.


DCOM Enabling Our ActiveX Controls: A Preview

Before digging into the more technical aspects of DCOM, we'll now get our hands wet with a quick demonstration. We'll reserve a more complex scenario involving three machines for the last section of the chapter. Since our ActiveX control and the back-end objects are already communicating across processes (the Calendar control running inside the Visual Basic or Test Container process and the Finder object running inside Atlfinder.exe), it should be an easy matter to make them run across machine boundaries over the network. In fact, it's quite simple, with the help of DCOM.

First, we'll need machines connected over a TCP/IP network with DCOM enabled. The previous section has described how DCOM for Windows 95 may be obtained, as well as the installation procedure. In our example, we'll be using DCOM for Windows 95. This choice is made since Windows 95 machines are easier to locate in most cases than Windows NT based machines. However, all of what we cover should be equally applicable to Windows NT 4.0 systems as well.


Preparing the Network

The following are instructions to follow in preparing the network for DCOM testing. Depending on the type of network that the Windows 95 machines are on, the setup procedures are slightly different. If you have machines belonging to an NT Server administered domain, follow the For NT Server Administered Domains instructions. If you have Windows 95 machines connected peer-to-peer (or any other Windows 95 network configuration), follow the For Peer-to-Peer Windows 95 Networks instructions.


For NT Server Administered Domains

The following instructions are for readers who have machines connected to an NT Server Administered Domain. Readers with standalone peer-to-peer networks consisting of Windows 95 machines do not have to perform the following steps.

Assuming that DCOM for Windows 95 has been properly installed on both machines, we must ensure that both machines have printer and file sharing enabled (this allows the system to 'listen' for RPC requests), and that user based security is enabled with both machines authenticating against the same domain server. This can be done by clicking on the network icon in the control panel.

Select the Client for Microsoft Networks networking component, and click the Properties... button. You can now select the Log on to Windows NT domain option and enter in the name of the NT domain that you'll use. Of course, you need to ensure that your user ID and password have been setup with NT domain in order to log on for our experimentation.

After you make this change, the system may request the location of a Windows 95 CD, and finally the system will ask you to restart Windows 95.

The system will now authenticate against the NT domain when you log on to the system. However, we still need to enable user level sharing, and this is where we converge with the instructions for those without Windows NT Domains.


For Peer-to-Peer Windows 95 Networks

DCOM for Windows 95 (Release 1.0) defaults to relaxed security upon installation on peer-to-peer Windows 95 networks. This allows operation of DCOM without specifically configuring any security parameters.

Start Control Panel and click on the Network icon. Instead of selecting properties, click on the File and Print Sharing... button. You'll see this dialog:

If the I want to be able to give others access to my files option is already selected, you're all set. Just Cancel out of the operation and go on to the next step.

If it isn't yet selected, then select it and go through the Windows 95 CD-Rom check and reboot one more time. After this reboot, you'll be all setup for DCOM under Windows 95.

Now we must make sure that the machine can accept remote DCOM calls from clients. This is done by setting the following registry key:

\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\OLE\EnableRemoteConnect = ‘Y’

This key isn't set by default. You'll need to restart the machine again if you change the value of this key.

Another key you'll find at this location is:

\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\OLE\EnableDCOM = ‘Y’

This key is set to Y by default, and can be turned off if you should ever want to disable DCOM operations altogether on this machine; however, it would be cleaner to uninstall DCOM for Windows 95 from the machine if you want to do this permanently.

A somewhat easier way to change the value of the key is through the Object Viewer application. If you start Oleview.exe and select File | System Configuration, you'll get the following dialog box:

If you check or uncheck these boxes, the Oleview.exe program will handle the setting and resetting of the registry keys for you automatically. It will also remind you to reboot should you change any of these values. Object Viewer provides a much safer and friendlier way to the registry settings. If you should run Object Viewer under Windows NT, you'll find the ability to configure the default security blanket for the system as well when you select this option.

Repeat these steps on the other machine.


Installing the Software Components

Now, we're ready to move the Atldept.dll and Atlfinder.exe over to the remote machine that we'll call the server machine. On the 'server' machine, make sure that the network is prepared, including the activation of printer and file sharing, activation of the user level shares, and the enabling of DCOM connections if necessary. Having done this, create a C:\Test directory on the 'server' machine and copy the following files into the directory:

  • Atldept.dll

  • Atlfinder.exe

  • atl.dll (from the ATL 2.1 distribution or your system directory)

  • Regsvr32.exe (from DevStudio\vc\bin directory)

Now change directory to C:\Test and type in the following:

REGSVR32 ATL.DLL

REGSVR32 ATLDEPT.DLL

ATLFINDER /RegServer

Each of these operations should complete successfully. Now install Object Viewer and start it up. Check under the All Objects list and find ATLFinder1 Class and ATLDept1 class. Double-click each one to ensure that you can create an instance of each class. Release the instances.

Finally, on the 'server' machine, type in ATLFINDER /Server. This will start the Atlfinder.exe COM server listening for incoming COM requests. DCOM for Windows 95 doesn't support remote server launching. Therefore, all remote servers must be started manually. If you have a Windows NT 4.0 system, however, you'll not need to manually enter the above command. Atlfinder.exe will be automatically launched on-demand under Windows NT DCOM.

Now the 'server' machine is completely configured for operations. Go back to the 'client' machine. I assume that this machine has been properly configured as specified above, and that you've logged on with the same user ID as on the 'server' machine. Now, start up Object Viewer and click on the ATLFinder1 Class under All Objects. On the right-hand pane, you'll see a set of tabs. Click on the Implementation tab. Click on Local Server and you should see a path to the Atlfinder.exe on your system. Remove this path and leave the edit box empty. This effectively removes the content of the LocalServer32 key from the registry. Now, we need to tell the 'client' machine that the ATLFinder1 class is to be instantiated remotely. To do this, click on the Activation tab, click on the Launch As Interactive User and key in the IP address of the 'server' machine.

Now go back to the left-hand pane and click on the ATLFinder1 Class entry to instantiate an object. If this is successful (it may take a little while), you'll see the interfaces listed and you'll have successfully instantiated an object across the network.

Important note: DCOM for Windows 95 is supposed to automatically start a program named Rpcss.exe when a connection is made to the object. However, this doesn't always work. If you have problems connecting to the object try running Rpcss.exe on the 'server' before you attempt to connect.

To put the entire scenario to work now, open TestPage.htm from the last chapter. After a moment the calendar should start and display all the events. Try selecting a cell with events to get the details. Notice the response. Next, try changing months: notice again the significantly slower response. The ActiveX control is now working over the TCP/IP network via DCOM. The exact same interaction is happening across machine boundaries.

The significant note to make here is that we haven't specifically programmed any DCOM server or DCOM client. The binary code hasn't actually changed at all. It's the same in-proc Atldept.dll server, the same Atlfinder.exe local server, and the same ActiveX control. All we did was some external configuration, and our COM objects started to communicate with each other over the network. This is significant because it shows the fact that:

DCOM is COM

Almost all legacy COM applications (ones designed with no consideration for DCOM) can be configured to work with DCOM. Of course, new 'from-scratch' applications can take specific advantage of the new features available in DCOM by programming directly to it or—more importantly—being designed specifically for it. We'll have a further discussion on these topics in a later section.

The following diagram depicts the interaction between the ActiveX control and the remote DCOM server components.


Making Calls Across Processes

Let's now take a closer look at how the DCOM magic works. When a client creates an object instance supported by a local .EXE server, this is what happens:

  1. The client calls CoCreateInstance() on a CLSID supported by a local server.

  2. The DCOM runtime working together with the SCM (Service Control Manager), determines if the requested local server is already running and can be connected to. The SCM is a part of the OS on Windows NT.

  3. If the local server is already running and can be connected to, the client is provided with a pointer to an interface proxy (more on this later).

  4. If no connectable instance of the class requested exists, a new instance of the object is created by launching the local server if necessary.

  5. An interface proxy representing the requested interface from the newly created object is returned to the client.

The SCM is a process whose sole purpose in life is to locate implementations or specific instances of requested COM classes. Because of this, it's a close friend of the system registry where all the CLSIDs to implementation mappings are kept persistently. This SCM is typically named Rpcss.exe on Windows 95 and Windows NT systems.

Once a client is connected to an instance of a COM class, it holds an interface pointer to an interface proxy. With this interface proxy, the client may obtain other interface pointers (using QueryInterface()—recall that all interfaces must implement IUnknown::QueryInterface()) or call methods of the interface directly). When such methods are invoked, the COM runtime must send all the arguments for the method from the client process to the local server process. This requires the use of a type of interprocess communications supported by the underlying operating system. In the case of COM, the mechanism used is Distributed Computing Environment Remote Procedure Calls (DCE RPC). DCE RPC has several nice features:

  • It works in the same way across processes on the same machine or across machines on a network.

  • It handles procedure calls and parameter passing across two machines with dissimilar hardware architectures and operating systems.

  • It provides security support that allows the identity of the client process to be passed over to the server.

  • It's basically network transport protocol independent, being currently able to work over TCP, UDP, IPX/SPX or Named Pipes.

  • It's a time-tested mechanism widely deployed in both Windows NT system design and UNIX based network programming.

COM and DCOM leverage heavily on these capabilities.

Note: COM actually will attempt to use lightweight RPC (LRPC) whenever possible when the two communicating objects/components are residing on the same machine and OS. LRPC is a Microsoft proprietary variant of RPC which has the exact same calling syntax as the DCE RPC runtime (thus the seamless interchangeability) but is optimized to reduce data copying and eliminate access to the networking code altogether.

When we put the RPC layer into the picture during a method invocation, the situation becomes a bit more complex. Namely, the call is now 'cross-process' or 'cross-machine'. The actual call is delivered to an object within the current process called an Interface Proxy. The following diagram shows the activities that will occur:

  1. The client invokes the interface through the interface proxy.

  2. The interface proxy marshals the parameters into a buffer provided by a channel. The channel is supplied by the RPC runtime system and is an abstraction above the different transports. The channel can be operating on shared memory spanning two processes on the same machine, or could be operating over a network protocol stack across two machines. It can be viewed as a virtual tunnel connecting the two communicating processes (regardless of their location). Data going into the tunnel (marshaled) on Process A will come out the same way (unmarshaled) from the other end of the tunnel on Process B. Loosely speaking, marshaling means packing the data into the channel's buffer in a well-defined format (for DCE RPC, this well defined format is called the Network Data Representation or NDR). NDR is an agreed format to transmit different data types between dissimilar machines so that the same data can be reconstructed exactly at the destination machine).

  3. The channel forwards the data to the server.

  4. An interface stub on the server receives the buffer from the channel and unmarshals the data from the buffer .

  5. The interface stub calls the associated interface on the server object.

  6. The interface stub sends return values back to the interface proxy.

  7. The interface proxy returns the values to the client.

Note that there are many more subtleties associated with the marshaling action than the simple description implies. For example, if a pointer to a data item is marshaled across two processes, just sending the value of the pointer itself is useless since the receiver lives in a totally separate process/addressing space. Instead, the actual data item itself must be sent across the wire and a pointer to it reconstructed within the receiver process in order for the marshaling to work.

Now is a good time to find out what the interface proxies and stubs are. Under the COM model, they are actually small COM objects. The code for these objects was created automatically when we ran our original IDL (interface description language) through the MIDL compiler. Based on the data type of the method arguments and return values, the MIDL compiler creates these marshalling proxy/stub pairs for communicating between processes. One pair of proxy/stub classes is created for each interface. If you look back in Chapter 7 where we created a local server called ATLFinder.EXE, you'll find a file called ATLFINDERPS.MK. You can actually perform:

NMAKE –F ATLFINDERPS.MK

in a DOS box to create and register the proxy/stub for marshalling of the interfaces supported by the ATLFinder.EXE local server. To satisfy your curiosity, look in your Chapter 7 working directory for the ATLFinder.EXE project and find the ATLFinder_p.c file to see what MIDL generated proxy/stub code physically looks like.

So far we've focused on the conceptual view of providing proxy and stub code for interface method invocation across processes, and indeed it was the way that it was done for a period of time. With DCOM, though, Microsoft has introduced automatic type-library based marshaling. Automatic type-library marshaling actually builds these proxy and stub objects on the fly from type library information obtained on an interface. What we see here is actually a reuse of the marshaling technology Microsoft had been using for OLE Automation. As long as the data type that needs to be marshaled is compatible with OLE Automation (which essentially means all the data types that a VARIANT can hold), you don't even need the MIDL generated proxy/stub code.


Calling COM Methods Between Machines

Now that we have a basic understanding of how an interface method call is transmitted across two communicating processes on the same machine or across a network, the next logical question is: how is the object on the destination created in the first place? Simply put, the SCM will locate or create the object instance across the network whenever necessary (if there are no object instances which can service the call, the SCM will launch the required object server on the remote machine, subjected to security constraints detailed in the next chapter). In the case where the caller and called party are both on the same machine under the same OS, the channel is typically working over shared memory and LRPC will be used. In the case where the caller and called party are on different machines over a network, the channel uses the DCE RPC over a layer of network software called the protocol stack (usually TCP/IP but IPX and NETBEUI are also supported by RPC on Windows platforms).

Of course, there's low level sanity checking and connection management work that DCOM must do because of the added network connection. All this work, though, is completely hidden from you as an application developer and you can develop components without worrying about where they may be executed.


The Premise of DCOM: Why DCOM?

Now, back to discussions on a higher level. We, as technical professionals, are often asked to justify or support strategic decisions through analysis and/or case studies. If such a decision involves the question 'Why DCOM?', we have a dilemma. DCOM, being an extremely young technology, will not have too many substantial or particularly compelling case studies that one can draw from. Instead, the decision may have to be made based on initial testing and more casual 'feature-set' analysis.

Here we'll provide a list of salient 'selling points' for DCOM that can be used when building a case for DCOM. Beware, portions of what follows may read more like sales pitches than sound technical punches.


The Object-Oriented Hype

The object-oriented gold rush of the early 1990s has been eclipsed by the painful discovery of the difficulties involved when putting the methods into practice. Full adoption of the object-oriented culture entails revolutionary changes throughout the life cycle of a product, from OO Analysis/Planning to OO Design, to OO Programming, through to OO environment for software maintenance and support. The exposure to such culture, for most people, is limited to the utilization of object-oriented programming languages and development environments. The C++ language has became the most popular ambassador of object-oriented design and development techniques. Unfortunately, much of its popularity comes from the fact that it attempts to be backward compatible with the most popular system (and microcomputer applications) programming language of all time: C. This compatibility has provided many 'backdoors' in programming which break all rules of object-oriented software design. While discipline in software design works well in small object-oriented projects working in C++, industry case studies have shown that the practice inevitably breaks down in very large projects involving multiple programming teams. One key problem cited is the inability to reuse C++ classes effectively across projects without having access to the underlying source code. Many of the promises made by the object-oriented pundits remained undeliverable in the era of object-oriented C++ software development.

Another very real limitation is that unless the entire system is object-oriented, it becomes very difficult to manage the 'boundaries' between the pieces on a system that are and aren't object-oriented. After all, a simple computing language can only give an 'illusion' of object-orientation without an underlying object-oriented runtime operating system, or data access support. Without these supports, some sort of 'mapping' has to be performed to work with the underlying subsystem if it isn't object-oriented. Because an alternative method of access is available which isn't object-oriented, such a system typically requires very involved maintenance and is subjected to easy corruption. There were valiant attempts in the industry to address this problem by making the complete system object-oriented, most notable are the Next operating system, the SmallTalk programming language and programming/runtime environment, and many of the object databases. In each case, by having total control over the entire operating environment, some of the higher level benefits of object-oriented design based on inheritance and polymorphism can be realized. While there are a significant number of people following and adopting the approaches of these systems, their 'revolutionary' nature has kept them from many of the largest corporate installations.


Dream of Code Reuse

As we've said before, the promise of code reuse is somewhat realized in reuse of class libraries within in-house small projects. In the larger 'shrink wrap' software market, the postulated 'uninhibited world-wide' market for class libraries never materialized. It turns out that class libraries delivered without source code are very hard to use and debug. Delivering class libraries with source code still gives the seriously business-minded software vendor heartaches because of the potential for unauthorized plagiarism.

While the leading edge fraction of the industry are trying various 'silver bullet' object-oriented solutions, and failing, various standards organization and industry groups (i.e. OMG, DCE, Open Group, the COM camp within Microsoft) are taking a step-by-step approach by evolving the current operating system and supporting services towards an object-oriented style of computing. The current result from these working groups is a computing model based on distributed objects, or components. Almost all of the abstract concepts preached by the object-oriented evangelists are deployed in creating reusable software components that can interoperate over heterogeneous networks of computers.

Regardless of what architectural appeal a new methodology may have, the business community will not usually commit to it wholesale unless there exists compelling business reasons for them to do so. The overwhelming driving force towards a component model of computing is simple economics. In the ideal world of component based applications building, pieces of prefabricated and tested code can be assembled together to create applications. The same pieces of code can be reused in other contexts to build alternative applications. In this world, software components only have to be written once, drastically saving the time and effort required for 'reinventing the wheel' for each individual project. If it sounds familiar, this is the exact same promise almost a decade back that had brought the object-oriented design and development into vogue. What, then, makes it different ten years later?

The key here is the slow emergence and development of a simple yet effective supporting infrastructure that can actually make it work. One term that is often used to describe these 'piece of functionality' components is a Software IC or Software Integrated Circuit. The analogy drawn upon here is the silicon chip in hardware design that encapsulates a fixed piece of functionality and works with other chips to create functional electronic devices. Take the example of a microprocessor chip. By itself, it's not very interesting, and it can do very little for the end-user. However, when it's put into a design where it can communicate via a bus to memory chips and input/output chips, we end up with a highly functional computer system. It's this 'bus' which has been missing for our 10-year-old Software ICs. In the hardware example, the electronic bus interconnecting the microprocessor, memory, and input/output chips has well defined electrical and timing specifications, and documented protocols for how the connected devices will be interoperating. In our software example, COM and DCOM (and also CORBA, dominant in the UNIX world) provides such a bus for object cooperation.


Compelling Business Reasons

Some other compelling business reasons why an infrastructure like COM and DCOM may finally be able to deliver on the component based computing promise include:

(disclaimer: intense sales pitch coming up)

  • Preservation of existing investment: current software can be 'wrapped' and participate in the DCOM component computing world regardless of its current implementation platform.

  • Autonomy through distributed business-rule components, allowing components encapsulating business-rules to reside and be maintained by the authority group which decides and implements them.

  • Efficient and effective use of low-cost commodity computing resources; many mainframe or mini-class solutions can be migrated to fault-tolerant clusters of inexpensive microcomputers, greatly reducing deployment and maintenance costs.

  • Potentially unlimited scalability for server solutions: ongoing developments in DCOM and related support services will soon enable very large server systems which can handle very large databases (up to a terabyte) and very high transaction rates (up to a million transactions per day). In a recent industry summit, Microsoft's Bill Gates has made a commitment to deliver this in 1997.

  • Enable distributed workflow computing: a choice of scripting languages is available for creating workflow applications orchestrating the action of various business-rule components over a network using DCOM.

  • Lower cost of development and support: the platform and language independence provided by DCOM enable an organization to use existing programming and design expertise, say in COBOL or BASIC, to develop and maintain reusable software components. (MicroFocus COBOL can create native COM components!)

  • Delivery over diverse heterogeneous environments: the support of DCOM over both TCP/IP, IPX/SPX and many other transports, coupled with the implementation of DCOM on UNIX, Macs, and other systems allows deployment of DCOM over existing network and computing resources without additional investment.

  • 'Riding the power curve': the ability to leverage the currently available commodity microprocessor based technologies and apply them to new or legacy problems that formerly required very expensive dedicated or niche computing equipment.

  • DCOM can work securely through an intranet, the public Internet, or over virtual private networks enabled through the Internet today making objects location-independent.


Dealing with Legacy Applications

We've seen how regular COM objects can be made to work across a network using DCOM simply by altering a registry configuration. This is done by design to allow the crop of existing COM components to participate in the DCOM world immediately. You don't even need to recompile these components to make them work across the network. Specifically, the following registry keys are essential in letting the SCM know that the server is located on another machine:

\HKEY_CLASSES_ROOT\AppID\<the object’s CLSID>\RemoteServerName = “ip address of server node”

\HKEY_CLASSES_ROOT\AppID\<the object’s CLSID>\RunAs = “Interactive User”

These settings will make the SCM look for the remote class on the server node, and run with the security principal and identity of the current user

In addition, we need to make sure that the following keys in the registry are removed:

\HKEY_CLASSES_ROOT\CLSID\<the object’s CLSID>\InProcServer32

\HKEY_CLASSES_ROOT\CLSID\<the object’s CLSID>\LocalServer32

otherwise the SCM will use either the in-proc or local server versions of the object to create the instance of the object, as the mechanism is more efficient.

This approach works well when only one externally created class can be created by the remote executable. In the case where there are multiple classes corresponding to the same remote executable, we'll need to create a registry key called AppID at each \HKEY_CLASSES_ROOT\CLSID\<the object's CLSID> entry. The AppID key should contain a GUID that refers back to the entry under \HKEY_CLASSES_ROOT\AppID\<GUID>. For example:

\HKEY_CLASSES_ROOT\CLSID\{A023F6A0-F4C1-11CE-8EAF-00608C85E107}\AppID = {208D2C60-3AEA-1069-A2D7-08002B30309D}

\HKEY_CLASSES_ROOT\CLSID\{B10CBD8E-F9B6-11CF-9B38-0080AD11B667}\AppID = {208D2C60-3AEA-1069-A2D7-08002B30309D}

\HKEY_CLASSES_ROOT\AppID\{208D2C60-3AEA-1069-A2D7-08002B30309D}\RemoteServerName = “192.168.0.1”

\HKEY_CLASSES_ROOT\AppID\{208D2C60-3AEA-1069-A2D7-08002B30309D}\RunAs = “Interactive User”

In this example, we can see that objects with CLSIDs {A023F6A0-F4C1-11CE-8EAF-00608C85E107} and {B10CBD8E-F9B6-11CF-9B38-0080AD11B667} can both be created using a server on the node 192.168.0.1. The SCM will use this information to decide where to go and create instances for these two classes.

We're running as the Interactive User (under DCOM for Windows 95, there exists really no other choice) in these cases in order to avoid complications due to security constraints on Windows NT based systems. Interactive User means the user who is currently logged on at the particular PC. Under Windows 95, all currently active processes 'belong' to the system that is currently logged on. Adopting this simple approach to security allows us to focus our effort on development and testing. As long as we're logged on as the same user on all machines containing clients and servers to be tested, DCOM will be able to connect and execute. For production, however, we must plan our security strategy carefully using DCOM's security blanket mechanism. This will be the subject matter of Chapter 9.


Some Changes for DCOM-Aware objects

While 'normal' COM objects can plug-and-play in the DCOM world, it's certainly possible to create DCOM-aware objects that can take advantage of some new features that DCOM offers. We'll now take a look at some of the new APIs and security oriented COM interfaces available for DCOM and how we may be able to take advantage of them.


CoCreateInstanceEx()

First, we have a CoCreateInstanceEx() call to specifically create an object instance on a remote machine, and also optimize on the performance of the QueryInterface() mechanism across the network by handling multiple QueryInterface() operations during the CoCreateInstanceEx() call (utilizing only one network round-trip). The prototype is shown here:

HRESULT CoCreateInstanceEx( REFCLSID  rclsid,
                            IUnknown *  punkOuter,
                            DWORD * dwClsCtx,
                            COSERVERINFO* pServerInfo,
                            ULONG  cmq,
                            MULTI_QI  rgmqResults   );

The parameters are explained below:

Function Parameters | Description
REFCLSID rclsid | The CLSID of the COM class from which the object instance is to be created.
IUnknown * punkOuter | If NULL, indicate that the object is not being aggregated. If non-NULL, the object being created is part of an aggregate. Note that out-of-process aggregation isn’t supported in the currently available version of DCOM. CoCreateInstanceEx() is guaranteed to return CLASS_E_NOAGGREGATION if a non-NULL value is passed, and the instance to be created is remote.
DWORD * dwClsCtx A value from the CLSCTX (execution context) enumeration, it can be:

CLSCTX_INPROC_SERVER,

CLSCTX_INPROC_HANDLER,

CLSCTX_LOCAL_SERVER,

CLSCTX_REMOTE_SERVER,

CLSCTX_SERVER or

CLSCTX_ALL

Note that the last three values include CLTSCTX_REMOTE_SERVER, which means that they will also allow creation of a remote instance. Recall that it’s still possible to create a remote server by external registry configuration, potentially overriding the use of this parameter by the client.
COSERVERINFO * pServerInfo Pointer to a COSERVERINFO structure that specifies the machine on which to create the object instance. The COSERVERINFO structure is defined to be:

typedef struct _COSERVERINFO

{

DWORD dwReserved1;

LPWSTR pwszName;

COAUTHINFO __RPC_FAR *pAuthInfo;

DWORD dwReserved2;

} COSERVERINFO;

The pwszName should be pointing to a wide string containing the server’s name. When using DCOM over TCP/IP, this can contain the IP address of the node, or a DNS name such as WWW2.ABC.COM. Otherwise, a UNC name such as \\MACHINE is allowed. The COAUTHINFO member contains a security blanket definition (see Chapter 9) structure:

typedef struct _COAUTHINFO

{

DWORD dwAuthnSvc;

DWORD dwAuthzSvc;

LPWSTR pwszServerPrincName;

DWORD dwAuthnLevel;

DWORD dwImpersonationLevel;

COAUTHIDENTITY __RPC_FAR *pAuthIdentityData;

DWORD dwCapabilities;

} COAUTHINFO;

In most cases, when an NT Domain server is used to authenticate a user (using NTLMSSP), this member of the COSERVERINFO should be set to NULL rather than a pointer to COAUTHINFO. We'll cover security in more detail in Chapter 9.

If this parameter, COSERVERINFO, is NULL, the SCM will be asked to do its job to determine how to create an instance of the class by examining the registry.
ULONG cmq | The number of the MULTI_QI structures being passed in as the rgmqResults parameter. Combined with rgmqResults, this parameter allows the DCOM runtime to save network round-trips by specifying multiple QueryInterface() calls to perform on the object immediately after creation. This must be at least 1 (i.e. you must get at least one interface for the remote object, otherwise why create it?).
MULTI_QI rgmqResults Contains an array of MUTLI_QI structures, which is defined as:

typedef struct _MULTI_QI

{

const IID* pIID;

IUnknown * pItf;

HRESULT hr;

} MULTI_QI;

Upon calling, the pIID portion contains a pointer to the IID of each interface to QueryInterface() for. Upon call return, the pItf member will contain the interface pointer requested if the hr member is S_OK. If the hr member for a specified interface is E_NOINTERFACE, the pItf member is invalid, and the object doesn't support the interface requested.

The possible return values from CoCreateInstanceEx() are:

Return Code | Description
S_OK | The object instance was created successfully.
E_INVALIDARG | One or more arguments passed in were invalid.
E_NOINTERFACE | None of the specified interfaces in the rqmqResults was available from the object.
CO_S_NOTALLINTERFACES | Not all of the specified interfaces in the rqmqResults were available, but at least one entry contains a valid interface.


CoInitializeSecurity()

A new CoInitializeSecurity() call allows a process to set a default security blanket. A security blanket is a suite of security parameters specifying the level of security required. A negotiation process takes place when a client connects to a server, when the system matches the security levels specified by the client to make sure it is at the same level or higher than those specified in the server's security blanket. More discussion of security and the CoInitializeSecurity() will be presented in Chapter 9. Typically, the CoInitializeSecurity() call is made after a call to CoInitializeEx(). If a process doesn't call CoInitializeSecurity(), COM will actually make the call on the process's behalf, using the defaults found in the registry (under the AppID key). This is how legacy non-DCOM aware components get to play along with DCOM-aware components, without having to worry about security.

Again, we'll discuss other DCOM related security interfaces, including the IClientSecurity, IServerSecurity, and IAccessControl interfaces (together with their associated helper functions) in Chapter 9.


New CoRegisterPSClsid() API

This API function is used to temporarily register the CLSID of a proxy/stub class server associated with a specified interface. The normal behavior for COM is to read the proxy/stub server information from the

HKEY_CLASSES_ROOT\Interface\{interface ID} registry key. However, in certain cases, the object wishing to replace the proxy may not have write-access to the registry. This is frequently true under the Windows NT where the access security level of the running object may not be sufficient to modify the registry. CoRegisterPSClsid() is useful in these situations. In addition, it is an all around preferred way to specify a specific proxy/stub code for an interface without affecting the registry settings. For example, if you would like the system to use a network-optimized, custom-coded marshalling scheme rather than the system default automatic type library marshaling.

The scope of the temporary assignment will last until the termination of the process or until CoRegisterPSClsid() is called again. If the proxy/stub objects aren't yet registered with the system, CoRegisterClassObject() should be called to register the CLSIDs before calling CoRegisterPSClsid().

The CoRegisterPSClsid() has the following prototype:

WINOLEAPI CoRegisterPSClsid( REFIID riid,
                             REFCLSID rclsid );

It has the following parameters:

Parameter Name | Description
REFIID riid | A pointer to the interface ID of the interface that the proxy/stub code is to be associated with.
REFCLSID rclsid | A pointer to the CLSID representing the proxy/stub code for marshaling the riid interface.

CoRegisterPSClsid() may return:

Return Value | Description
S_OK | Mapping of proxy/stub code to interface successful.
E_INVALIDARG | A parameter is invalid.
E_OUTOFMEMORY | The runtime ran out of memory while executing the API call.

This concludes our coverage of new interfaces and APIs associated with DCOM. None of these new features are required for COM objects to participate in a distributed environment. But DCOM-aware objects that make use of these features can gain better control over the way DCOM performs remote instantiation, client and server security checking, as well as marshaling.


Remote Activation (Server Launching)

One major difference between DCOM for Windows 95 and Windows NT DCOM is the ability to automatically launch (or activate) a remote server to create object instances. Providing that the client has the required security blanket, server launching/activation is automatic on Windows NT. On Windows 95, it isn't possible to automatically launch a server. Instead, COM servers must be launched manually in Windows 95 before they may be used remotely.

This difference can be attributed to the lack of support for network or multiuser security under Windows 95. Under Windows 95, every process runs as the current user and there's no way to circumvent this. The only authentication level accepted is 'connect' under Windows 95 (once again, more on this in Chapter 9), this means that separation between accessible resources for concurrently executing servers is nonexistent and that impersonation isn't possible. From a security perspective, all currently running processes on a Windows 95 system are executing under a single user account. The bottom line is that COM servers running concurrently on Windows 95 can't provide privacy or resource protection features. This is a rather sad affair, and renders Windows NT the preferred server platform for DCOM objects.


Typelib and Registrations

Registration of type library information is mandatory for classes that are to be used by automation clients. You should register your type library if you want the object that you've created, whether it's local or remote, to be usable by VBScript and JAVAScript in Internet Explorer, and Visual Basic as well as Java.

Recall from Chapter 2 that type libraries are maintained in .TLB files generated by the MIDL compiler from the project's .IDL file. But, ATL also combines the type library information with the generated executable (either .EXE or .DLL). When we do a regsvr32 <name>.DLL or a <name>.EXE /RegServer, the type library registration is also performed automatically.

Let's look into the registry and see where these type libraries are actually located. In general, a type library can be located under:

\HKEY_CLASSES_ROOT\TYPELIB\{Type library’s UUID}

Taking our ATLFinder local server as an example, we find the following registry values:

Matching this with the Atlfinder.idl that we've created in Chapter 7, we can see the correspondence.

[
   uuid(721B4F00-4D27-11D0-8564-004005263AF7),
   version(1.0),
   helpstring("ATLFinder 1.0 Type Library")
]
library ATLFINDERLib
{
... skipped interface and coclass definitions
};

The GUID of the type library is used as the main key under \HKEY_CLASSES_ROOT\TYPELIB. Then a subkey containing <MajorVersion.MinorVersion> has a default value that is the name or 'helpstring' attribute in the IDL file. Under this subkey, there can be a FLAGS subkey. This subkey can contain a unary ORed combination of the following values:

FLAGS value | Description
1 | Restricted—no user tools should display or use this type library.
2 | Control—the type library contains descriptions of controls. Tools which examine nonvisual objects should skip this library.
4 | Hidden—this library should not be displayed to the users, but can be used by controls.

The actual location of the type library information, in this case the Atlfinder.exe file with the TLB information merged in as a custom resource, can be found under a subkey with 0\win32. The 0 subkey is there mainly for historical reasons. Older OLE implementations had no means of deleting a type library entry programmatically, so the numeric index is used to keep track of older versions of the type library.

The HELPDIR subkey specifies the path to the .HLP file associated with this type library. If a type library is published for commercial or production purposes, a context index can be associated with a help file for each item in the type library to give more information to the end user as they browse the type library.


Shipping Components Across the Intranet

Building applications out of reusable distributed components is a great idea. Keeping such an application up-to-date with respect of new versions of the application as a whole, and to the varying new versions of the components used in the application isn't a trivial task. One new technology introduced by Microsoft along with its ActiveX push, which addresses this problem, is a system for automated code download and installation.

Conventional software upgrades produce major headaches for MIS departments, involving coordinated install, update schedules and user training sessions. It's easy to see the tremendous effort and cost associated with each build of a widely used piece of software.

It simply will not work in the componentized world.

Imagine an average component-based application using directly and indirectly about 50 to 60 classes of component objects. Just imagine two revisions for each component class each year, and you end up with 120 software upgrades a year or approximately 10 each month. We must look for an automated and reliable way of upgrading components.


Automated Application Download and Install

Though not explicitly designed for COM or DCOM, the automated code download feature solves our problem nicely. This features allows ActiveX controls (COM servers) to be downloaded from a centralized location, installed, and activated on an end-user machine when a web page containing the control is reached. As an additional feature, if the ActiveX control is already installed on the user's machine, but is of an older version, the support system will download and replace the control with the newer version. This behavior is almost exactly what we want. Paired with the code download technology is digital signature/code signing technology from Microsoft. This allows the receiver of the downloaded component to be sure of the integrity of the code during the download process, and the authenticity of the original source of the code. However, this technology is less relevant in the isolated intranet environment.

In a typical usage on a web page, an ActiveX control object is embedded in the following manner with the <OBJECT> tag.

<OBJECT
   ID="MyCalendar"
   CLASSID="clsid:99B42120-6EC7-11CF-A6C7-00AA00A57DD2"
   CODEBASE="http://objectstore.aw.com/controls/vbsamp/vbcalctl.ocx#Version=1,0,0,121"
   TYPE="application/x-oleobject"
   WIDTH=150
   HEIGHT=60
   VSPACE=0
   ALIGN=left
>

The key attributes for version control and download are the CLASSID and CODEBASE entries. The CLASSID entry contains the CLSID of the control to download, and the CODEBASE entry contains the location for downloading the code if necessary, as well as a version number consisting of four parts. The first two parts are the two words making up the most significant double word of the version number (typically the major version number), and the last two parts are two words making up the lower double word of the version number (usually the minor version number). The new control will be downloaded if it hasn't been installed in the local machine and/or if the version number of the code on the local machine is older than the one required.

Under the hood, the browser (in this case IE3.0) uses a new API call to accomplish its task. The API is generally available, and we may use it to create centrally stored, version controlled component code for automatic distribution and version management. The name of this new API function is CoGetClassObjectFromURL(), and is equivalent to CoGetClassObject(),which relies solely on the SCM to check the information in the local registry.


CoGetClassObjectFromURL()

Like CoGetClassObject(), this function will return a class factory interface for a specified CLSID. The object instance will be created if it's already installed locally. If necessary, this routine will also download and install the code from the specified URL. If the URL points to an uncompressed .OCX, .DLL, (.CLASS for Java) or .EXE file, the component is registered locally after download and the object factory instance will be created. If the URL points to a .INF file, the instruction within the .INF file will be followed to either download more URL based files, or install and download more components. If the URL is a compressed .CAB file, the file is decompressed and authenticated (via call to WinVerifyTrust()) with its embedded digital signature; the decompressed contents are then treated as if they are freshly downloaded COM servers or an .INF file as described above. This API is server agnostic, it will work with any web servers, not just the Microsoft IIS.

It's also possible to call without a specified CLSID, in this case, the content type parameter determines the CLSID of the executable to use in interpreting or displaying the specific MIME type. The MIME type to CLSID mapping is kept in the local registry directly under the key:

\HKEY_CLASSES_ROOT\MIME\Database\ContentType

Using the MIME type feature is a way for the system to associate a specific default executable to deal with a specific multimedia data type. For example, if the content is a music file in MIDI .MID format, the associated executable may be the Media Player applet.

The call prototype for CoGetClassObjectFromURL() is:

STDAPI CoGetClassObjectFromURL( REFCLSID rclsid,
                                LPCWSTR szCodeURL,
                                DWORD dwFileVersionMS,
                                DWORD dwFileVersionLS,
                                LPCWSTR szContentTYPE,
                                LPBINDCTX pBindCtx,
                                DWORD dwClsContext,
                                LPVOID pvReserved,
                                REFIID riid,
                                VOID **ppv );
Parameter | Description
rclsid | A pointer to the CLSID of the object instance to be created. It can contain NULL, in which case the szContentTYPE will be used to determine what to instantiate.
szCodeURL | A wide-string indicating the URL of the code for the COM object. As mentioned above, it can be a .CAB, .INF, .OCX, .DLL, .CLASS or .EXE file.
dwFileVersionMS | The most significant word of the version number (usually the major version number).
dwFileVersionLS | The least significant word of the version number (usually the minor version number).
szContentType | A wide-string containing the MIME content type description string. It's used to determine the CLSID to instantiate in cases where the rclsid parameter contains NULL. For example, szContentType may contain audio/x-wav which will cause the sound recorder applet to be instantiated.
pBindCtx | Pointer to a bind context that is passed in. Note that if the client is interested in receiving status information during the download (i.e. if the link is slow), a callback should be registered with the bind context. We'll explain what a bind context is later on in this section.
dwClsContext | A value from the CLSCTX enumeration specifying the execution context of the object (i.e. CLSCTX_INPROC_SERVER, CLSCTX_INPROC_HANDLER, or CLSCTX_LOCAL_SERVER).
pvReserved | Set this to NULL.
riid | Pointer to the IID of the initial interface to ask for, almost always IClassFactory.
ppv | An output value, a pointer to an interface pointer which will be set to point at the requested interface from the new object instance.

The possible return values from CoGetClassObjectFromURL() are:

Return Value | Description
S_OK | The ppv return value now contains the requested interface.
E_NOINTERFACE | Cannot obtain the requested interface.
REGDB_E_READREGDB | Error encountered when accessing the registry.
CO_E_ERRORINDLL | The .DLL or .EXE image has problems.
CO_E_APPDIDNTREG | The local server was started but it did not register any class objects.
E_ACCESSDENIED | General access failure.
MK_S_ASYNCHRONOUS | Asynchronous completion. If the client has registered a callback, it will be called to update status.

Asynchronous and URL Monikers

We'll now explain the pBindCtx parameter and the E_PENDING return value. These parameters are supporting the asynchronous nature of the CoGetClassObjectFromURL() call. Under the hood, the CoGetClassObjectFromURL() call makes use of a COM facility called a URL Moniker to do its magic. The URL Moniker is actually an instance of a concept known as an Asynchronous Moniker defined by the Internet Addendum to the OC96 specifications. The following is the conceptual inheritance tree.

First, we must give a quick explanation of what a Moniker is. Monikers are essentially 'smart name objects' in COM. Any COM object that implements the IMoniker interface is deemed a Moniker. The unique property of a Moniker (usually a relatively small object) is that it knows how to find the object that it names. It also completely hides from the client how it goes about finding the object. By 'binding' to a Moniker, we magically get the object that it names. Monikers provide a level of indirection between the client of an object and the object itself. This level of indirection provides unlimited extensibility because the Moniker isn't obliged to follow any standard rules or protocols when it goes about its binding operation. In the old days, Monikers were used almost exclusively for OLE linking operations. In this context, embedding means to embed an entire object into a document while linking means embedding a Moniker (much smaller object) into the document. It's easy to throw Monikers into compound documents (or any arbitrary file) because IMoniker is derived from IPersistStream, which supports serialization of the Moniker to memory, disk, or even across a network.

Asynchronous Monikers are a relatively new breed introduced to solve the problems produced by operations over a slow Internet link. Unlike its age-old parent, the binding operation on an Asynchronous Moniker can take as long as it wants. Meanwhile, the client invoking the binding operation gets freed to do other things right away. If the client registers a callback function with the Asynchronous Moniker, the Moniker will report back status information as it's performing the binding operation.

A URL Moniker is a specific implementation of an Asynchronous Moniker. We know how it does its magic. Since a URL Moniker is a name (URL in this case) that knows how to find the object that it represents (i.e. an ActiveX control stored on the web server), we can deduce that the URL Moniker will be using HTTP to fetch the object over the network. And this is indeed how it's done. Of course, if the URL should be ftp://ftp.abwil.com/objects/atldept.dll, then alternative protocols would be used (again, transparent to the client since the Moniker performs all the magic). Since the going rate for asynchronous modems is 28.8Kbps, we can quickly understand why a URL Moniker must be asynchronous. We certainly don't want to hold the client (and thus the application) in a frozen blocked state while a URL Moniker is finishing its magical binding effort.

The current implementation of URL Monikers uses standard Win32 Internet APIs (formerly called WinINET) to access the actual resources, this allows security proxies to work transparently underneath.

It's quite easy to see that our CoGetClassObjectFromURL() call is using URL Monikers underneath to do its dirty work. This is where the BindCtx requirement comes from. A bind context is just a small COM object that's passed into a Moniker during a bind operation. It functions as a client-supplied instance-identification 'scratch pad'. While not always necessary, you may want to receive status feedback during the asynchronous process, and then there's substantially more work to be done. You can easily create a bind context by making the CreateBindCtx() call. The interface that you grab a bind context object by is, not surprisingly, called IBindCtx. The IBindCtx interface comprises 13 methods altogether, including the three required by IUnknown. We aren't actually interested in any of these methods. There exists a (new!) helper function, called RegisterBindStatusCallback(), which will do our callback registration for us. The usage of RegisterBindStatusCallback() is:

HRESULT RegisterBindStatusCallback( IBindCtx *pbc,
                                    IBindStatusCallback *pbsc,
                                    IBindStatusCallback **ppbscPrevious,
                                    DWORD dwReserved );

The parameters required are:

Parameter | Description
pbc | Pointer to the IBindCtx of our bind context object.
pbsc | Pointer to an IBindStatusCallback of a bind status callback object to be registered. We'll discuss how to create this later.
PpbscPrevious | An output parameter which will be filled with a pointer to the previously registered IBindStatusCallback instance.
dwReserved | Set this to 0.

The possible return values for RegisterBindStatusCallback() are:

Return Value | Description
S_OK | The operation has completed successfully.
E_INVALIDARG | An argument passed was invalid.
E_OUTOFMEMORY | The operation can't be performed because memory was exhausted during its execution.

The use of the function is straightforward, except for the IBindStatusCallback pointer that we need. Typically, the ppbscPrevious returned will be NULL since it will be the first IBindStatusCallback that we register for this bind context. Once we've registered an IBindStatusCallback with a bind context, it would last until it's revoked by RevokeBindStatusCallback(), displaced by a new one through another RegisterBindStatusCallback() call, or the bind context itself is destroyed.

The IBindStatusCallback pointer should be pointing to an interface that the client (or an object that the client controls) would implement. The IBindStatusCallback interface consists of a total of 11 methods, including the three IUnknown methods. These methods are:

IBindStatusCallBack Method Name | Description
QueryInterface() | Standard IUnknown requirement. Make sure that a query for ICodeInstall will return the client’s ICodeInstall interface (see below).
AddRef() | Standard IUnknown requirement.
Release() | Standard IUnknown requirement.
GetBindInfo() | Called by the Moniker to get binding information.
OnStartBinding() | A chance for the client to tell the Moniker which events/callback the client is interested in. Also provides the client with an IBinding interface for the client to control downloading.
GetPriority() | Obtains the priority of this binding operation.
OnProgress() | Reports progress to the client.
OnDataAvailable() | Reports the incremental availability of data.
OnObjectAvailable() | The requested object interface pointer is passed to the client.
OnLowResource() | The Moniker reports a low resource condition during the bind.
OnStopBinding() | Notification that the binding operation has completed, and returns error code if any.

It's safe to return E_NOTIMPL for the GetPriority(), and OnDataAvailable() members in most cases.

If we're supporting automatic installation of a component (i.e. through registry update or an .INF file), we must also implement the ICodeInstall interface. The Moniker will do a QueryInterface() on IBindStatusCallback for ICodeInstall. The ICodeInstall interface has a total of only 5 methods. The client must implement this interface.

ICodeInstall Method Name | Description
QueryInterface() | Standard IUnknown requirement.
AddRef() | Standard IUnknown requirement.
Release() | Standard IUnknown requirement.
NeedVerificationUI() | Called when the server needs to display a verification window. The client should supply a parent window in this case.
OnCodeInstallProblem() | Called when server encounters code installation problem (i.e. out of disk space). The client may attempt to correct the situation by prompting the user accordingly.

By supporting a callback in our bind context for the asynchronous (URL) moniker, we gain a lot of control over the display of status during the binding process, as well as the ability to customize system-displayed UI and handle error conditions during the code installation process.

There exists also an asynchronous moniker specific function, called CreateAsyncBindCtx() which combines the function of RegisterBindStatusCallback() and CreateBindCtx() into one convenient call.

HRESULT CreateAsyncBindCtx( DWORD dwReserved,
                            IBindStatusCallback *pbsc,
                            IEnumFORMATETC *penumfmtetc,
                            IBindCtx **ppbc );

The parameters required are:

Parameter | Description
dwReserved | Must be zero.
pbsc | Pointer to an IBindStatusCallback of a bind status callback object to be registered.
penumfmtetc | Pointer to an IEnumFORMATETC of an enumerator object for format negotiation during binding, should be set to NULL for URL moniker binding through the CoGetClassObjectFromURL() call.
ppbc | Pointer to hold the IBindCtx pointer for the newly created bind context object.

The possible return values for CreateAsyncBindCtx() are:

Return Value | Description
S_OK | The operation has completed successfully.
E_INVALIDARG | An argument passed was invalid.
E_OUTOFMEMORY | The operation can't be performed because memory was exhausted during its execution.


An Applied Example: The 'Downloader' Control

Let's put some of these interfaces and APIs to work. We'll create an ActiveX control that you can use to download and install components over the intranet. We'll be encapsulating in an ATL based component all of the APIs and interfaces we've described above. Through COM based code reuse, we can avoid the required elaborate coding each time we need the download and install functionality. The implementation of the control is skeletal in nature, in order to keep things simple. The reader can easily modify and enhance the control for specific situations.

The ILoadlate Interface

Our completed component will support an ILoadlate interface. This is a dual interface that we've defined. The methods for this interface are:

ILoadlate Method Name | Description
QueryInterface() | Standard IUnknown requirement.
AddRef() | Standard IUnknown requirement.
Release() | Standard IUnknown requirement.
GetFactory([out, retval] LPUNKNOWN* itf) | Called after the download and installation is completed in order to obtain the IClassFactory interface from the newly downloaded (and instantiated) object.
GetLatest([in] BSTR bstrCLSID, [in] BSTR bstrURL) | Called to start the code download, install, and object creation process. bstrCLSID should contain the string representation of the CLSID of the object; bstrURL should contain the URL to download the object from.
PollResult([out] long * current, [out] long * max, [out,retval] BSTR * bstrStatus) | Called periodically during download to get the current status of the download. bstrStatus is a status message indicating the current progress. current may contain the number of bytes downloaded and max may contain the number of bytes of the object being downloaded (this depends on the server and the URL used).

This is the only interface that our ActiveX control will provide. Essentially, it encapsulates all the calls necessary for code download and installation, and it also changes the 'callback' model that the CoGetClassObjectFromURL() calls to an easier-to-use polling model for the client.

Coding the Downloader Control

Use the Visual C++5 ATL COM AppWizard to create a new DLL based project. In the project, use the ATL Object Wizard to put a Simple COM object into the project. Call this object CLoadlate, call the interface ILoadlate, select Apartment model threading and create dual interfaces.

When the basic code generation is completed, add the three methods according to the ILoadlate interface description earlier in this section.

Next, add the implementation for these methods:

STDMETHODIMP CLoadlate::PollResult( long * current, long * max, BSTR * bstrStatus)
{
   *bstrStatus = SysAllocString(m_bstrStatus);
   *current = m_lLoaded;
   *max = m_lTotal;
   return S_OK;
}

The implementation of ILoadlate::PollResult() simply copies the value of a set of member variables to the return values. We'll see how these variables get updated later.

The ILoadlate::GetFactory() implementation is:

STDMETHODIMP CLoadlate::GetFactory(LPUNKNOWN * ift)
{
   *ift = m_myFactory;
   return S_OK;
}

Again, we assume that the caller will be calling after the download/binding is completed. We simply copy the member variable's value to the return value.

Most of the work is done in the ILoadlate::GetLatest() implementation:

STDMETHODIMP CLoadlate::GetLatest(BSTR bstrCLSID, BSTR bstrURL)
{
   HRESULT hr;

   hr = CLSIDFromString(bstrCLSID, &m_clsid);
   if (FAILED(hr))
      return S_FALSE;

Here, we recover the CLSID from its string representation. This will be used later in the CoGetClassObjectFromURL() call. We'll store it in the m_clsid member variable for now.

   m_bstrUrl = bstrURL;
   m_pbsc = new CBindStatusCallback(this);
   if (m_pbsc == NULL)
      return S_FALSE;

We then save the value of the URL in the m_bstrUrl variable, and create a CBindStatusCallback() object (which implements the IBindStatusCallback interface), storing a pointer to it in the m_pbsc member variable.

   hr = CreateAsyncBindCtx(0, m_pbsc,NULL, &m_pbc);
   if (FAILED(hr))
      return S_FALSE;

Next, we create a bind context object by calling CreateAsyncBindCtx() using the CBindStatusCallback object we created earlier. The resulting bind context is assigned to a m_pbc member variable (of the point-to-IBindCtx type). Finally, we're ready to call CoGetClassObjectFromURL().

   LPUNKNOWN tpInf;

   hr = CoGetClassObjectFromURL(m_clsid,
      m_bstrUrl,
      0xffffffff,
      0xffffffff,
      NULL,
      m_pbc,
      CLSCTX_INPROC_SERVER,
      NULL,IID_IClassFactory,
      (void * *)&tpInf);

   if (!FAILED(hr))
      m_myFactory = tpInf;

   return hr;
}

Note that we used 0xfffffff for both version numbers to ensure that the control will download the latest version of the requested object. The reader may customize this for actual deployment. When called successfully, this call will return MK_S_ASYNCHRONOUS which indicates that the download and install will be taking place asynchronously.

We used a few member variables in the code above without declaring them. Now we must add these variables to the CLoadlate class. Add the following variables and make them all public.

   WCHAR                m_bstrStatus[100];
   CComBSTR             m_bstrUrl;
   IUnknown*            m_myFactory;
   long                 m_lLoaded;
   long                 m_lTotal;
   uuid_t               m_clsid;
   IBindCtx*            m_pbc;
   IBindStatusCallback* m_pbsc;

This completes our CLoadlate class. However, we see above that we must also define the CBindStatusCallback class. This class is defined in the .H file as:

class CLoadlate;

class CBindStatusCallback : public IBindStatusCallback
{
public:
    // IUnknown methods
    STDMETHODIMP    QueryInterface(REFIID riid,void ** ppv);
    STDMETHODIMP_(ULONG)    AddRef()    { return m_cRef++; }
    STDMETHODIMP_(ULONG)    Release()   { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; }

    // IBindStatusCallback methods
    STDMETHODIMP    OnStartBinding(DWORD dwReserved, IBinding* pbinding);
    STDMETHODIMP    GetPriority(LONG* pnPriority);
    STDMETHODIMP    OnLowResource(DWORD dwReserved);
    STDMETHODIMP    OnProgress(ULONG ulProgress, ULONG ulProgressMax, ULONG ulStatusCode,
                        LPCWSTR pwzStatusText);
    STDMETHODIMP    OnStopBinding(HRESULT hrResult, LPCWSTR szError);
    STDMETHODIMP    GetBindInfo(DWORD* pgrfBINDF, BINDINFO* pbindinfo);
    STDMETHODIMP    OnDataAvailable(DWORD grfBSCF, DWORD dwSize, FORMATETC *pfmtetc,
                        STGMEDIUM* pstgmed);
    STDMETHODIMP    OnObjectAvailable(REFIID riid, IUnknown* punk);

    // constructors/destructors
    CBindStatusCallback(CLoadlate * pLate);
    ~CBindStatusCallback();

    // data members
    CLoadlate      * m_ptrLate;
    DWORD           m_cRef;
    IBinding*       m_pbinding;
    CCodeInstall    * m_CodeInstall;
};

Note: If you use the New Class... menu option to add this and the following class, then you need to add a generic class derived from the interface. Then in the .CPP file you'll find a #define for new to DEBUG_NEW. This needs to be removed as it's for MFC projects only.

We can see that this class implements the IBindStatusCallback interface through its member functions. It also has a member which holds a pointer to the CLoadlate class which instantiates it (in order to update the member variables of the CLoadlate class during status call back). It also has the m_pbinding member to hold an IBinding interface during the binding process. Since we need to support installation, there's also a CCodeInstall object reference (we'll describe this class later).

The implementation of this class (in the .CPP file) is simple. The constructor initializes member variables, and the destructor does nothing.

CBindStatusCallback::CBindStatusCallback(CLoadlate * pLate):m_ptrLate(pLate)
{
   m_ptrLate->m_lLoaded = 0;
   m_ptrLate->m_lTotal = 0;
   m_ptrLate->m_myFactory = NULL;
   m_cRef = 1;
   m_pbinding = NULL;
}  // CBindStatusCallback
CBindStatusCallback::~CBindStatusCallback()
{
}  // ~CBindSt

Several functions return trivially with E_NOTIMPL:

STDMETHODIMP CBindStatusCallback::GetPriority(LONG* pnPriority)
{
   return E_NOTIMPL;
}
STDMETHODIMP CBindStatusCallback::OnLowResource(DWORD dwReserved)
{
   return E_NOTIMPL;
}
STDMETHODIMP CBindStatusCallback::OnDataAvailable(DWORD grfBSCF, DWORD dwSize, FORMATETC* pfmtetc, STGMEDIUM* pstgmed)
{
   return E_NOTIMPL;
}

In QueryInterface(), we handle request for our own IBindStatusCallback, as well as ICodeInstall since we support installation.

STDMETHODIMP CBindStatusCallback::QueryInterface(REFIID riid, void** ppv)
{
   *ppv = NULL;
   if (riid==IID_ICodeInstall)
   {
      m_CodeInstall = new CCodeInstall();
      *ppv = (ICodeInstall *) m_CodeInstall;
      m_CodeInstall->AddRef();
      return S_OK;
   }
   if (riid==IID_IUnknown || riid==IID_IBindStatusCallback)
   {
      *ppv = this;
      AddRef();
      return S_OK;
   }
   return E_NOINTERFACE;
}

Notice that above we create a new CCodeInstall object if the query interface for ICodeInstall is called.

Before download (binding) begins, the GetBin() member will be called by the system to determine how to bind. We set the flags to indicate an asynchronous bind/download, and that the newest version should be fetched.

STDMETHODIMP CBindStatusCallback::GetBindInfo(DWORD* pgrfBINDF, BINDINFO* pbindInfo)
{
   *pgrfBINDF = BINDF_ASYNCHRONOUS
      | BINDF_ASYNCSTORAGE |BINDF_GETNEWESTVERSION ;
   return S_OK;
}

When download (binding) begins, the OnStartBinding() member will be called by the system. We're obliged to grab hold of a binding object at this time:

STDMETHODIMP CBindStatusCallback::OnStartBinding(DWORD dwReserved, IBinding* pbinding)
{
   if (m_pbinding != NULL)
      m_pbinding->Release();
   m_pbinding = pbinding;
   if (m_pbinding != NULL)
   {
      m_pbinding->AddRef();
   }
   return S_OK;
}

During the binding/download, the OnProgress() member will be called regularly. We take this opportunity to update the status variables of the CLoadlate class.

STDMETHODIMP CBindStatusCallback::OnProgress(ULONG ulProgress, ULONG ulProgressMax, ULONG ulStatusCode, LPCWSTR szStatusText)
{
   wcscpy(m_ptrLate->m_bstrStatus, szStatusText);
   m_ptrLate->m_lLoaded = ulProgress;
   m_ptrLate->m_lTotal = ulProgressMax;
   return S_OK;
}

When the binding/download is completed, the OnStopBinding() method is called. Here we release the binding object we seized earlier in OnStartBinding(). We also signify to our client that the binding is completed by setting the m_bstrStatus of the CLateload member to “DONE!”.

STDMETHODIMP CBindStatusCallback::OnStopBinding(HRESULT hrStatus, LPCWSTR pszError)
{
   wcscpy(m_ptrLate->m_bstrStatus, L"DONE!");
   if (m_pbinding)
   {
      m_pbinding->Release();
      m_pbinding = NULL;
   }

   return S_OK;
}

When installation is completed and the requested IClassFactory interface is available, the OnObjectAvailable() member will be called. Here, we simply assign the pointer to our member variable, ready for the client to fetch.

STDMETHODIMP CBindStatusCallback::OnObjectAvailable(REFIID riid, IUnknown* punk)
{
   m_ptrLate->m_myFactory = punk;
   return S_OK;
}

This is all that's required for the CBindStatusCallback class to implement the IBindStatusCallback. Next, we'll examine the ICodeInstall interface (and the CCodeInstall class) that we'll need. The CCodeInstall class is defined in the .H file as:

class CCodeInstall:public ICodeInstall
{
public:
   // IUnknown methods
   STDMETHODIMP    QueryInterface(REFIID riid,void ** ppv);
   STDMETHODIMP_(ULONG)    AddRef()    { return m_cRef++; }
   STDMETHODIMP_(ULONG)    Release()   { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; }
   STDMETHODIMP    GetWindow(REFGUID ab, HWND *pwnd);
   STDMETHODIMP    OnCodeInstallProblem(ULONG ulStatus, LPCWSTR szDest, LPCWSTR szSrc, DWORD dwRes);
   CCodeInstall();
   ~CCodeInstall();
protected:
   DWORD           m_cRef;
};

ICodeInstall only has two new member functions. In the .CPP file, constructor and destructor are implemented trivially.

CCodeInstall::CCodeInstall():m_cRef(1)
{
}

CCodeInstall::~CCodeInstall()
{
}

ICodeInstall::QueryInterface() is also implemented in a standard fashion.

STDMETHODIMP CCodeInstall::QueryInterface(REFIID riid, void** ppv)
{
   *ppv = NULL;
   if (riid==IID_IUnknown || riid==IID_ICodeInstall)
   {
      *ppv = this;
      AddRef();
      return S_OK;
   }
   return E_NOINTERFACE;
}

ICodeInstall::GetWindow() asks for a parent window to display some user interface. We return NULL which indicates that the desktop window should be used.

STDMETHODIMP CCodeInstall::GetWindow(REFGUID ab, HWND *pwnd)
{
   *pwnd = NULL;
   return S_OK;
}

To implement ICodeInstall::OnCodeInstallProblem(), we return S_OK for all the reported problems which we would ignore, and return E_ABORT for all other problems (e.g. out of disk space).

STDMETHODIMP CCodeInstall::OnCodeInstallProblem(ULONG ulStatus, LPCWSTR szDest, LPCWSTR szSrc, DWORD dwRes)
{
   switch(ulStatus)
   {
   case CIP_OLDER_VERSION_EXISTS:
   case CIP_NAME_CONFLICT:
   case CIP_TRUST_VERIFICATION_COMPONENT_MISSING:
   case CIP_NEWER_VERSION_EXISTS:
      return S_OK;
   default:
      return E_ABORT;
   }
}

Finally, we need to include the definitions for the two interfaces we're implementing as well as linking in the relevant library. The best place to add the required header is in StdAfx.h:

...
#include <atlbase.h>
//You may derive a class from CComModule and use it if you want to override
//something, but do not change the name of _Module
extern CComModule _Module;
#include <atlcom.h>
#include <urlmon.h>
...

In the Link tab of the Project Setting dialog, you need to add Urlmon.lib to the list of library modules:

This completes our implementation of the 'downloader' control. You can now compile, link, and register the control.

Coding the Test Client

Typically, a client of this object will:

  1. Call ILoadlate::GetLatest() with information for the object to download and install. This call will return immediately while the download and installation occurs asynchronously.

  2. Handle user interface or perform other tasks, while intermittently calling the ILoadlate::PollResult() looking for a status string of “DONE!”. It can also update the user on the progress of the download if desired.

  3. After ILoadlate::PollResult() returns “DONE!” for status, call ILoadlate::GetFactory() to obtain the class factory interface of the object.

  4. Use the class factory to create the object, and release the class factory.

  5. Proceed to use the newly created object.

Let's follow these five steps and quickly code our test client. First, create a new Win32 application project in Visual C++ 5. We called ours 'lotestw'. Create a blank C++ source file in the project and start entering the code:

#include <assert.h>
#include <comdef.h>
#include "resource.h"

#import "codeload.tlb" no_namespace
#import "atldept.tlb" no_namespace

Here, we're using the native COM support of the Visual C++ 5 compiler again. In the two #import directives, we obtain the type information (and smart pointer definitions) for our Atldept.dll, as well as the type information from our new 'downloader' ActiveX control. Next, we have a few constant declarations.

const char szURLofObject[] = "http://PENTIUM1/ATLDEPT.DLL";
const unsigned TIMER_DURATION = 200; // refresh every 200 ms
const int TIMER_ID = 1001;

Since the ILoadlate interface supports polling, we'll be creating a Windows timer to activate our polling during the download. TIMER_DURATION is the interval time between each pool, the TIMER_ID is used to identify the timer in Win32 API calls. Next, we define a custom Windows message. This message is posted by the timer handler after receiving “DONE!” from the control; the handler for this message will get the IClassFactory interface and create an object from it.

#define   WM_CUSTOM1   WM_USER+100

The next section is the COM exception handling required by the native COM support. We aren't going to implement the details here, but you're free to do so.

void dump_com_error(_com_error &e)
{
// handle error intelligently in here
}

The WinMain() function is quite straightforward:

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR szCmdLine, int nCmdShow)
{
   HRESULT  hr;
   hr = CoInitialize(NULL);
   if (FAILED(hr))
      return hr;
   DialogBox(hinst, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, (FARPROC)&DialogProc);
   CoUninitialize();
   return 0;
}

In the above code, we simply call CoInitialize(), create a dialog box, then call CoUninitialize() and quit. Making the desktop window the parent for the dialog box means we don't need to create any window in WinMain() (however, CoInitialize()will actually create a hidden window).

Let's take a look at the dialog box; create this using the resource editor in Visual C++ 5 and name it IDD_DIALOG1 (the default).

Use the default dialog generated by the resource editor, and change the text of the OK button to Install Object Asynchronously and the Cancel button to Exit. Create a read-only edit and name it IDC_EDIT1.

Now, we're ready to examine our dialog procedure, where all of the testing logic resides. If you're keying in the code, make sure this goes before the WinMain() in the file.

BOOL CALLBACK DialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
   static ILoadlatePtr  pIC;
   long current, max;
   BSTR bstrStatus;

We declare a smart pointer to our ILoadlate interface as static, so we would not recreate the object on every message. Current, max, and bstrStatus are to hold the results from ILoadlate::PollResult() for reporting to the end user. Next, we're into the message decoding loop portion:

   switch(message)
   {
   case WM_INITDIALOG:
      try
      {
         pIC.CreateInstance("Loadlate.Loadlate.1");
      }
      catch (_com_error &e)
      {
         dump_com_error(e);
      }
      break;

In the WM_INITDIALOG message (sent only once after the creation of dialog box), we instantiate a 'downloader' control, and obtain the ILoadlate interface through the smart pointer using the CreateInstance() member.

   case WM_TIMER:
      {
         bstrStatus =  pIC->PollResult(&current, &max);
         char    sz[255];
         if(bstrStatus!=NULL)
            WideCharToMultiByte(CP_ACP, 0, bstrStatus,-1, sz, 255, 0,0);
         char    msg[100];
         wsprintf(msg,"Loading: %s %d of %d ", sz, current, max);
         SetWindowText(GetDlgItem(hwndDlg,IDC_EDIT1), msg);
         if (wcscmp(bstrStatus, L"DONE!") == 0)
         {
            KillTimer(hwndDlg, TIMER_ID);
            PostMessage(hwndDlg, WM_CUSTOM1, 0, 0);
         }
       }
       break;

The next message handled here is the timer message. We call the ILoadlate::PollResult() method through the smart pointer pIC and then fill the edit box in the dialog with the status. We convert the string from wide character BSTR to MBCS in order to format the output using wsprintf(). If the download is completed, we stop the timer messages and then post our custom message WM_CUSTOM1.

   case WM_CUSTOM1:
      {
         HRESULT hr;
         IClassFactory * tpF = (IClassFactory *) pIC->GetFactory();
         IUnknown * tp2;
         tpF->CreateInstance(NULL, IID_IUnknown, (void **) &tp2);
         if (!FAILED(hr))
            SetWindowText(GetDlgItem(hwndDlg, IDC_EDIT1), "Object Created!");
         else
            SetWindowText(GetDlgItem(hwndDlg, IDC_EDIT1), "Cannot create obj!");
         tpF->Release();
         tp2->Release();
      }
      break;

Handling of the WM_CUSTOM1 message is straightforward. At this time, the download has been completed. We obtain the IClassFactory interface by calling ILoadlate::GetFactory() through the smart pointer. We then use IClassFactory::CreateInstance() to create an instance of the ATLDept object. If this is successful, we print the message Object Created! in the dialog box. In any case, we release the class factory and the object before returning.

We come now to the button handling messages:

   case WM_COMMAND:
      {
         switch (LOWORD(wParam))
         {
         case IDOK:
            {
               _bstr_t bstrUrl(szURLofObject);
               _bstr_t bstrStatus;
               try
               {
                  LPOLESTR  ab;
                  CLSID myID = __uuidof(ATLDept1);

                  StringFromCLSID(myID, &ab);
                  _bstr_t bstrProgID(ab);

                  pIC->GetLatest(bstrProgID, bstrUrl);
               }
               catch (_com_error &e)
               {
                  dump_com_error(e);
               };
               SetTimer(hwndDlg, TIMER_ID, TIMER_DURATION, NULL);
            }
            break;

When the Install Object Asynchronously button (OK button) is pressed, we call ILoadlate::GetLatest(). This is done by calling StringFromCLSID() on the CLSID of the ATLDept object that we want to create to get a BSTR parameter. We also start the timer for refreshing the status edit box during the download.

         case IDCANCEL:
            pIC.Release();
            EndDialog(hwndDlg,0);
            return 1;
         }
         break;
      }
   }
   return 0;
}

Finally, if the user presses the Exit button, we release the interface encapsulated within our smart pointer and exit from the application.

This is all the code required to download and install the ATLDept object. Thanks to the 'downloader' control and its ILoadlate interface, the code for this client is very simple and uncluttered.

You can now compile the test program. Make sure that you have updated the URL embedded in the test program so that it points to your own web server.

Testing the Downloader Control

The following is the procedure for testing the 'downloader' control with the test program.

  1. Unregister the current ATLDept1 object by going to the directory where Atldept.dll resides, and perform a REGSVR32 /U ATLDEPT.DLL.

  2. Use Oleview.exe to ensure that the ATLDept1 object no longer shows up (i.e. removed from the registry).

  3. Copy Atldept.dll to the location of the URL. In our case, we have it at C:\Webshare\Wwwroot\Atldept.dll. The Personal Web Server for Windows 95 is set to point to the directory \Webshare\Wwwroot as the root directory. Our URL is http://PENTIUM1/ATLDEPT.DLL.

  4. Make sure your web server is working properly.

  5. Start the test program, Lotestw.exe.

  6. Press the Install Object Asynchronously button.

If everything is working properly, you should start to see the status appearing in the read-only edit of the dialog box. You should also see or hear some disk activity. It will report the download progress (but this probably won't be for too long since Atldept.dll is so small in size). After the download, the following security dialog will pop up asking if it's all right to install a component:

If you click Yes, the installation will complete, and you should see the Object Created! message in the dialog. This indicates a successful object creation using CoGetClassObjectFromURL() call.

To convince yourself, use the Oleview.exe program to see that now the ATLDept1 class is registered again. Click on the class entry to create an instance and notice that it is now working.

If you like to repeat the test, you must first remove the newly registered DLL.

  1. Go to the \Windows\Occache directory.

  2. Do a REGSVR32 /U ATLDEPT.DLL to remove the registry entries.

  3. Delete Atldept.dll to remove the downloaded DLL.

  4. Restart the test program and try download/install again.

The 'downloader' ActiveX control substantially simplifies the calls to perform automated code download and install. It can also be called from C++ or other clients (i.e. Visual Basic). While the Internet Explorer itself provides automation capability similar to the 'downloader' control, the control provides the function with a significantly smaller code and runtime memory footprint.


Problems Still Unsolved: The Missing Links

Despite all the flexibility that distributed component computing will bring us, there are vital missing pieces in the COM/DCOM puzzle that will prevent the immediate widespread deployment of the technology. This has little impact on self-contained project development environments, such as our Aberdeen & Wilshire intranet. However, these shortcomings do surface as problems that we must solve in an ad hoc manner on a per-project basis unless they are addressed by the system vendor. We'll examine several of these 'missing links' in this section.


Making it Simple: Hide the Details

The discussion we had in the previous section on the CoGetObjectClassFromURL() call, leading to URL Monikers, bind contexts and callback objects certainly wasn't for the faint of heart. And as long as the building of COM/DCOM based applications requires a high level of expertise, there will continue to be very few applications in circulation. The requirement here is to hide the details and make things as simple as possible. Witness that there are very few truly OLE-compliant applications on the general market despite the fact that OLE technology has been available for the past 6 years. The complexity and difficulties in dealing with the technology have created an artificial barrier to adoption.

A high priority for Microsoft's ActiveX initiative is the goal of making it simple for the developer. This is accomplished by hiding the complexity as much as possible from the end user and general programmers. With support of COM/DCOM by Visual Basic, VBA, VBScript and Java available (mainly through OLE automation), users and programmers can start experimenting with DCOM without having to go through the steep learning curves. The first generation of these COM/DCOM capable tools is just becoming available. In the future, we can look forward to enhanced ease of use/programming that ought to put things like IDL files, type library registrations, and direct registry editing behind us once and for all.

On the server end, the new Active Server Platform architecture will eventually allow for the creation of powerful, high performance, scalable server applications without being an operating system guru.


Finding Objects: Location Service

In a world full of objects, locating the object that you need to get a job done could be a tough problem. Depending on the application, the need to locate a specific instance of an object in a department or even throughout the enterprise may frequently arise. The need to find all the departments that have events posted for a given month is a good example. We bypassed the problem by creating our own EventsFinder class.

If the underlying system offers an object location service, however, the problem can be solved easily. Having a global location service will also enable effective pooling of computing resources. In this case, a component based application would consult the location service each time it needs to instantiate a server object to do some work, and the service can pick the component instance which is running on the 'most available' machine or based on certain pre-set criteria.

Part of this problem can be solved today using File Monikers and their BindToObject() capability (as with URL Monikers that we examined earlier, you can use File Monikers to reunite an object instance with its persistent data, assuming that the data is saved on disk as a file). Another part will be resolved with the introduction of a global 'class store' in future releases of Windows NT. The availability of a global directory service in the future will further unify the location and naming of all network and local resources, providing yet another step in the right direction.

What is still missing, though, is the ability to support the equivalence of a 'global running objects table' across an administrative domain, an intranet, or the entire Internet. The problem is very complex and requires tracking of a very large set of highly dynamic data items. Until a general solution becomes available, each project requiring such a service must reinvent the wheel in an ad hoc manner.


Transforming Chaos into Order: Transaction Service

A typical distributed application consists of computations, each requiring coordination of software components functioning across a network of computers. Each computation being coordinated may require a different mix of components residing on different machines. The pure software coordination problem is by itself a daunting task. Even if we manage to write perfect code and all the components reside on the same machine, the combination and permutation of lower level software failures, system hardware failures, reaching resource limits, etc. can create a very large number of 'error states' that our computing system can fall foul of. Now compound the effect with a distributed computing system where network failure, delays, and system availability contribute further to increase the possible error conditions.

Even though you have probably voiced this complaint to yourself already while walking through the Aberdeen & Wilshire code samples, I'll just repeat it here. There's no error handling code! Besides the normal excuse of 'the error handling code complicates the sample', I must admit that writing error handling code for DCOM applications is a science in its own right.

The problem is that the very same component may be running either locally or remotely, and the computational requirement for each case of reuse is not predictable at component design time (i.e. does this component work by itself, or does it feed into other components at every input/output junction?). Even if you know that your application spans, say, 6 computers in a network and uses a total of 30 components during its lifetime, writing a good application requires you to identify and recover from a tremendous combination of complex failure states. It's very hard to be sure that a system will detect or catch all software/hardware failure scenarios. It's easy to predict, though, that a distributed system consisting of a large number of software components will occasionally fail.

Learning from the Internet phenomenon (users on the Internet are notoriously famous for tolerating alpha or beta-software which contains many errors) maybe we should just ignore network errors and go on. While acceptable when casually surfing for interesting information on the web, it's definitely not an acceptable practice in the business world. What we need is a facility, or a service, which will enable us to greatly simplify the handling of errors, and greatly reduce the permutation of failure possibilities.

One implementation of such a service is called a transaction service. Microsoft's version of it is called the Microsoft Transaction Server. A transaction service for distributed components will allow the application writer to adopt a very simple view of error handling. A piece of work performed either by a component in isolation or by a large network of distributed components, can have only one of two outcomes. It either completed or failed. If the operation has failed, the state of the system is guaranteed to be the same as before the attempt to perform the piece of work. While the distributed system is carrying out its piece of work, the intermediate changes to the state of the system aren't visible to other concurrently computing system(s) until a success or failure state for the entire piece of work is reached. Note how this greatly simplifies failure cases. There may have been a million different reasons why a piece of work failed, but the application designer doesn't have to deal with them.

With this simple premise, the work of application design is reduced to transforming the application logic into an orchestration of a set of concurrent transactions. Each transaction can contain multiple computational steps involving many distributed components, and it can also include other nested transactions. A successful transaction is completed by a commit action, and an unsuccessful transaction is completed by a rollback action. A commit sends the system of distributed objects on to the next predictable state while a rollback reverts the system back to a previously stable state. In between transactions, while the system of distributed objects is computing, it may go through an unpredictable series of state changes including partial or complete software/network failure. None of these state changes, however, will be visible outside of the system. The other concurrently executing systems will only see a state change when a successful commit is performed on a transaction.

Before the availability of a robust and reliable transaction service, design presentations for component based distributed applications are full of frantic hand-waving and crossed fingers among the architects and engineers. Notice that the design of an application using a transactional model of computing is significantly different from designing, say, for a C++ or Java implementation. We require a new way of thinking about the problem.


Working in an Asynchronous World: Queuing Transport

We live in a highly asynchronous world. Just when you start to make that all-important business call to your supplier in Switzerland, he left the office and took the family out to dinner. Meanwhile, you end up with his voice-mail, so you leave him a message which he'll answer when he's next at his desk.

This scenario repeats itself in all shapes and forms millions of times per day around the world. Many business procedures involve the flow of work from one individual to another, each processing or adding value to the work in progress. Unfortunately, not every individual involved is connected to the networked system all the time. Therefore, there's a need to reliably queue the work while it's being relayed from one processing stop to another.

The flow of work from one agent to another is called workflow and many business procedures can be automated with workflow based systems. Fitting into the distributed computing perspective, the work performed by each agent can be considered a subtransaction, while the entire workflow itself can be considered as a single large transaction. In cases where a workflow consists of many subworkflows, this can be nested even further.

Now, without the availability of a reliable queuing mechanism, asynchronous workflow can't participate in the transaction world. This is because the typical delay between the processing steps of the workflow may be in terms of hours, days, or even weeks. Any mechanism of network sanity detection would consider such time delays as a network failure, and will cause the transaction-in-progress to fail, resulting in a rollback.

With the introduction of reliable queuing mechanisms, you can introduce the concept of a very long transaction in terms of duration, and computational integrity can be guaranteed. This is an absolute necessity in the handling of business procedure workflow.

One side benefit of having a reliable queuing mechanism in a distributed system is the ability to use it as an alternative transport. For example, imagine a world where DCOM is transmitted over a reliable queuing transport. This means that a link between inter-operating components can be broken, yet the request will be persistently queued at the source until the link is recovered minutes, hours, or even days later. At that time, the interaction between the components can continue. With the highly interactive nature of DCOM today (freshly evolved from OLE-centric, UI-focused COM), it's hard to imagine the practical implication of this ability. However, given time, as more and more transactional applications are delivered based on DCOM technology, the reliable queuing mechanism will become indispensable.

The Microsoft version of this queuing mechanism is code-named 'Falcon' and will become a fundamental enabler for the future world of robust DCOM based component computing.


Ensuring Uptime: Clustering of Redundant Inexpensive Computers

Transaction services allow us to focus on the business problem and be more independent of failure scenarios. Reliable queuing allows component interactions to survive network and hardware failures.

The assumption in these statements is that the system will occasionally fail. To address the issue of general system reliability, to enable the high-availability, 24 hours-a-day, 7 days-a-week operation required of the distributed computing world, one promising technology is the clustering of inexpensive computers.

By pooling together the resources of a cluster of 'identical' computers, connected via some high bandwidth (i.e. fibre channel, ATM, etc.) means one can construct a 'virtual' system where the components (hardware and software) are fault tolerant. In fact, all hardware components will fail eventually. However, by making available duplicate sets of hardware components in this 'clustered' environment, a hardware failure can be switched, or failed over, to a backup piece of hardware transparently to the client. By classifying running software processes as another failable resource, it's possible to fail these over as well. In this manner, fault tolerance can be achieved in this cluster (which looks to the rest of the network as one computing machine).

As work performed by computing systems become more and more componentized, this clustering of computing resources can also be used in the scaling of computational throughput. For example, the same fail over for software processes can be used in load balancing where the executing process is transferred on to a machine within a cluster that is the least busy. The ultimate objective of such a load balancer would be to ensure that every computing resource on the cluster is loaded evenly. This results in high throughput for the cluster.

Traditionally, high availability, fault tolerance, transactional service, reliable queuing and unrestricted throughput scaling are the domain of mainframe or highly proprietary minicomputers. With the amazing computational power afforded by commodity microprocessors, the ability to productively apply these advanced concepts to inexpensive and widely available hardware presents a very interesting and potentially lucrative business opportunity.


Hands on DCOM: Wiring Components Across Three Machines

Let's take our DCOM experimentation with our ActiveX Calendar application to the final stage and distribute the computing load over three (or more) machines. This will allow us to get our hands wet working with the DCOM facilities provided by Windows 95 and the Windows NT system. The same technique can easily apply to any multitiered intranet applications that you may build employing DCOM technology.

While this book has dealt mainly with software development and techniques thus far, the following section will feel more like a description of a laboratory exercise. In general, any description for testing of DCOM applications will involve describing the network topology, machine profiles and the staging scenario.

What we'll be attempting to do is set out in this diagram:

There are three machines in this configuration. They are all connected via a TCP/IP Ethernet LAN. Machine A is our client machine running the Calendar control embedded on a web page. Our web server is residing on machine B. Machine B is also where the ATLFinder COM object is run. Machine C is the host for a departmental server. The ATLDept object on machine C supports the IATLDept, ICorpEvents, and ICorpEvent interfaces with the object (and interaction) model that we covered in the last chapter.

All our clients and servers are running under Windows95 in the hope that the configuration can be easily duplicated by the reader for experimentation purposes. Another reason is the relatively simple security model that DCOM for Windows 95 supports. This allows us to focus on setting up DCOM without getting caught up in security issues. Remember that since a built-in, operating system based security infrastructure is nonexistent in Windows 95, DCOM based servers all run as the currently logged on interactive user. What this means is that provided we're logged on to all the machines as the same user when we're doing the staging, there'll be no security issues whatsoever. In Chapter 9, we'll be examining the finer-grained control over DCOM security that a Windows NT platform can offer.

We'll assume all four machines are setup for DCOM as described in the DCOM Enabling our ActiveX Control: A Preview section of this chapter. The following will only cover setup procedures which are customized for each machine involved in the experiment.


Setting up Machine A (the Client Machine)

Machine A is the client machine with the following software configuration:

  • Windows 95 with Service Pack 1

  • DCOM for Windows 95

  • Internet Explorer 3.01

On this machine, we'll set up the Aberdeen & Wilshire Calendar ActiveX control. This will require the pre-installation of the Vccalc1.ocx control and required associated files on the client station.

To pre-install the Vccalc1.ocx file on machine A, you may use the following procedure:

  1. Copy the Regsvr32.exe program over to \Windows\System directory if it's not already there.

  2. Copy the Vccalc1.ocx file to the \Windows\System directory.

  3. Copy the support files for Vccalc1.ocx to the \Windows\System directory (these files can be determined by running DUMPBIN/IMPORTS VCCALC.OCX); you'll also need Mfc42d.dll, Mfco42d.dll, and Msvcrtd.dll.

  4. Run regsvr32 VCCALC1.OCX on the control.

The next thing that you need to do on machine A is set up DCOM class linkage. We know that Vccalc1.ocx will instantiate an ATLFinder object during its operation, therefore we'll need to pre-wire this class (through its class ID) to a remote machine. Recall that we could have indeed hard-coded this into ATLFinder.exe using CoCreateInstanceEx(), but we'll take advantage of DCOM support for legacy applications in our experiment.

The easiest way to do this, again, is with the assistance of the indispensable Oleview.exe utility. Follow these steps:

  1. Copy the ATL support DLL, atl.dll, over to \Windows\System directory.

  2. Run regsvr32 atl.dll.

  3. Copy the ATLFinder.exe file over to a temporary directory.

  4. Run ATLFinder /RegServer to create the registry entries.

  5. Remove the ATLFinder.exe.

Running ATLFinder.exe will prepare the registry with information about the ATLFinder object. The actual ATLFinder.exe isn't required on this node, so we remove it after it's completed the job of setting up the registry entries. While ATLFinder adjusted the registry, it also inserted the location of the .EXE file under the \HKEY_CLASSES_ROOT\<clsid>\LocalServer32 key. We need to remove this and point it at the remote machine B. Start up Oleview.exe. Select All Objects on the left pane, and find the ATLFinder1 Class entry and select it. On the right pane, select the Implementation tab. Select the Local Server subtab, and clear out the Path to Implementation edit. This will clear up the associated registry key. Next, select the Activation tab. The pane will look like this:

Select the Launch as Interactive User check box and enter in the IP address of machine B in the Remote Machine Name edit. This tells the DCOM runtime to find a server for the ATLFinder1 class on the remote machine. When DCOM tries to launch the server remotely, the identity used will be that of the currently logged on user in Windows 95.

The Enable ‘At Storage’ Activation setting is specifically used in ActiveX document and/or Moniker binding scenarios where the desired operation is to 'start the object server on the node wherever the persistent data file of the object instance is stored'. This can be a powerful, albeit confusing, mechanism for starting object servers remotely. Essentially, it will allow for the activation of a DCOM object server remotely if the client has a pathname (e.g. \\System5\User\Doe\Mper.dat) to a file containing the persistent data used by the class (in this case, the server will be started on the \\System5 machine). The network filesystem is used as a type of persistent object instance store, and a UNC pathname is used to access and reactivate persisted object instances.

In actual deployment and production scenarios, performing the above setup on every client machine would be impossible. The ideal situation is to have the entire procedure automated when the user reaches a web page containing the Vccal1.ocx. Our earlier discussion of automated code download and installation is a perfect solution for this problem. Thankfully, Microsoft has built the call to the CoGetClassObjectFromURL() helper API into the Internet Explorer, saving us a tremendous amount pain. To deploy using automatic code download and installation, in this case, boils down to the creation of a .CAB file bundling everything together.

A .CAB file contains a compressed archive of one or more files. If the archives contains a .INF file, it will be used as the setup script to install the software component after it's downloaded. The WinVerifyTrust() facility is automatically called to allow the user to deny installation.

Bear in mind, however, even if a .CAB file is used, that DCOM for Windows 95 will still need to be installed at each client workstation before our Vccal1.ocx page would work properly.


Setting up Machine B (the Web Server and Finder Service machine)

This is the server machine, and it has the following software configuration:

  • Windows 95 with Service Pack 1

  • DCOM for Windows 95

  • Microsoft Personal Web Server for Windows 95

On this machine, machine B, we'll set up the ATLFinder.exe COM server class, and we will set pointers to the ATLDept.dll class that will be residing on machine C. To prepare the machine for serving ATLFinder objects, perform the following steps:

  1. Copy Regsvr32.exe into \Windows\System directory if it's not already there.

  2. Copy the ATL support DLL, atl.dll, over to \Windows\System directory.

  3. Run regsvr32 atl.dll.

  4. Copy the ATLFinder.exe file over to a working directory.

  5. Run ATLFinder /RegServer to create the registry entries.

This is all that's needed for ATLFinder. In order to create the link to the required ATLDept objects, we need to do the following:

  1. Copy the ATLDept.dll file over to the working directory.

  2. Run regsvr32 ATLDept.DLL.

  3. Remove the ATLDept.dll file.

  4. Start Oleview.exe, select ATLDept1 Class on the left pane, and the Implementation tab on the right pane.

  5. Select the Inproc Server tab and remove the Path to Implementation content.

  6. Select the Activation tab on the right pane, click the Launch as Interactive User check box, and enter in the IP address of machine C into the Remote Machine Name edit.

This machine is now ready to support calls to ATLFinder1 classes and remote calls to ATLDept1 classes. Before moving on to set up machine C, it's a good idea to test the setup between machine A and machine B. To do this, we again acquire the assistance of Oleview.exe:

  1. Start the ATLFinder server on machine B by typing ATLFinder /Server at the command line.

  2. Start Oleview.exe on machine A.

  3. Find the ATLFinder1 Class entry on the left pane and double-click on it.

If everything is setup correctly, after a little delay, you should see ATLFinder being expanded on the left pane with all its interfaces exposed. The ATLFinder1 Class entry is also in bold to indicate that an instance has been created successfully.

Using Object Viewer, you can then select the Object menu and Release Instance to release the selected instance.

This concludes and confirms the setup of machine B.

In actual deployment, under certain design circumstances, one may need to install the middle tier server components (such as ATLFinder) repeatedly on different systems. In these cases, of course, the Setup toolkit that comes with Visual C++ 5.0 may be used to create a setup script which can perform all of the above for us when the server object is installed on the system.


Setting up Machine C, the Departmental Server

This machine represents a departmental server, and it has the following software configuration:

  • Windows 95 with Service Pack 1

  • DCOM for Windows 95

Setting up this machine is rather similar to the setup of machine B. However, since the ATLDept.dll is a DLL, we can't simply start it listening for calls as we did with ATLFinder.exe on machine B.

To install ATLDept.dll on this machine, follow these steps:

  1. Copy Regsvr32.exe into \Windows\System directory if it's not already there.

  2. Copy the ATL support DLL, atl.dll, over to \Windows\System directory.

  3. Run regsvr32 atl.dll.

  4. Copy the ATLDept.dll file over to the working directory.

  5. Run regsvr32 ATLDept.dll.

This is all that's necessary for this machine since it isn't accessing any remote server.

To make sure everything is running okay, start Object Viewer on machine C. In the left-hand pane, select ATLDept1 Class, double-click it to instantiate it. If you have a problem instantiating ATLDept locally, backtrack over the steps and correct any errors.

Once you can instantiate it locally, it's time to try it on machine B. First, however, since Windows 95 offers no remote launching facility (due to security issues), you need to start the server.


Running an In-Proc Server Remotely

We can't just run ATLDept since it's a DLL. Instead we use a surrogate provided by DCOM. This is a program called Dllhost.exe and is installed by default in your \Windows\System directory during installation of DCOM for Windows 95. The operation of the surrogate process is quite straightforward: it simply creates a process space for the DLL server to execute in. It aggregates all of the DLL's interfaces and exposes them to any remote clients.

The command line syntax for invoking the surrogate is:

DLLHOST <clsid>

For example, we can host the ATLDept.dll by specifying the class id of the ATLDept class. (Note: one easy way to get the class id is to use Object Viewer, select the class and choose the Object menu, selecting the Copy CLSID to the clipboard option).

DLLHOST {D2A1CBC6-4C5E-11D0-8564-004005263AF7}

This will start the surrogate process waiting for DCOM calls on behalf of the ATLDept.dll server. If you examine the Implementation tab of the right pane of the Object Viewer display with the Inproc Server tab selected, you'll also see a space for specifying the surrogate process:

The entry here is rather useless under Windows 95. Under Windows NT, if you specify an entry here, the 'automatic launching' service can launch the Dllhost.exe surrogate with the ATLDept.dll whenever a client call is received remotely. On the other hand, starting such a surrogate must be performed manually on Windows 95. What's more interesting, if you were to instantiate ATLDept from Object Viewer, it would simply call the in-proc server directly without going through the expensive surrogate process.


Testing the Remote Instantiation of ATLDept

It would be a good time now to test the remote ATLDept class instantiation from machine B. To do this, perform the following steps:

  1. Run DLLHOST {D2A1CBC6-4C5E-11D0-8564-004005263AF7} on machine C.

  2. Use Pview95.exe (supplied with Visual C++ 5.0) to verify that Dllhost is running.

  3. Start Object Viewer on machine B, select ATLDept1 class on the left pane, double-click on the entry to create an instance on machine C.

Once this is successful, you have completed the setup required for machine C. Now we are ready to attempt testing the three machines together.

As in the case of machine B, if you find that you need to repeatedly perform server installation during actual deployment, it would be wise to spend the extra effort on creating a setup script for the server.


Testing the Three Networked Machines

Just to make sure that everything is still in order:

  • Run Pview95.exe on machine B to ensure that the Atlfinder.exe server is still running. If it isn't, restart with ATLFinder /Server.

  • Run Pview95.exe on machine C to ensure that the Dllhost.exe surrogate is still running. If it isn't, restart with DLLHOST {D2A1CBC6-4C5E-11D0-8564-004005263AF7}.

Finally, start Internet Explorer 3.01 on machine A, and enter in the following URL:

http://<Machine B’s name or IP>/TestPage.htm

This, of course, assumes that you've placed the TestPage.htm file at the root of the web supported by machine B's Personal Web Server.

As the web page loads, you'll see the Calendar control activate (albeit quite slowly). Try clicking on a cell to select the event list and get the event details. Remember that the control coding has to initialize the ATLDept objects on machine C from machine A with dummy data across the network.

Notice how the entire application still functions as it did when it was standalone. The only difference is the slower network based operation.


Testing DCOM with Windows NT 4.0

If you'll be carrying out testing on Windows NT 4.0 systems rather than Windows 95 systems, the following will be helpful in isolating some of the differences between the DCOM support on the two platforms. Some major differences between the two support platforms are:

  • Windows NT 4.0 has full launch and access security support (see Chapter 9 for more discussion on this topic).

  • Windows NT 4.0 offers automatic launch service.

  • Besides in-proc and local servers, Windows NT 4.0 also supports COM servers executing in the form of a native NT Service.

Make sure that you have Service Pack 2 installed on your system.

Dcomcnfg.exe on Windows NT 4.0 will allow you to adjust the registry entries and security permissions, etc. You can continue to use Oleview.exe in the Windows NT environment.

For our casual testing, we would like to disable the security and permission checking support. You can do this by setting the default values:

Default Authentication Level = None

Default Impersonation Level = Anonymous

This puts the system at the same level of accessibility as the Windows 95 machines.

One would never operate production systems in this state. However, a viable security policy requires careful planning before deployment. Again, see Chapter 9 for an in-depth discussion of security issues when working with Windows NT security in general, and DCOM security in particular.


Summary

In this chapter, we've introduced DCOM as COM with a longer wire. We looked at DCOM on Windows NT 4.0 and DCOM for Windows 95, and covered how we can obtain the software as well as the installation process. We demonstrated how ordinary COM objects can be made to interact over the network with simple registry changes.

Lifting the covers, we took a brief look at how DCOM works. We also covered several changes and additions in APIs that are associated with DCOM. To satisfy situations where we may need to justify DCOM, we provided a top 10 of reasons for deploying DCOM.

Not all is rosy, though. DCOM had some definite limitations, and we discussed how Microsoft intends to address them.

Distributed component systems are of no use if there's no way to distribute the components' code when necessary. We looked at the automated code download and installation technology provided through the complex CoGetClassObjectFromURL() call. We saw how this technology can help in maintaining an updated version of a component based application. To send the concept home, we've even created a 'downloader' ActiveX control to encapsulate the complex call.

Finally, in the last part of the chapter, we went back to the Aberdeen and Wilshire Calendar control example and step by step showed how to distribute the application over three machines.

All the while, we've avoided the complex issue of security in our network and software setup. This is a very important topic and it's the subject of the next chapter.


Security Overview

We've gone through most of this book looking at ActiveX, COM, DCOM and component software, all the time avoiding the issue of security. This is not because security is a less important topic. Security is of paramount importance, and can't be treated lightly in a 'by the way' fashion within sections embedded in other chapters. With this is mind, we've opted to focus on it in its own independent chapter and thereby do it some justice.

Moreover, the sheer complexity of security concerns means they don't mix easily with other technical subjects that require their own focus. And the implementation of security measures in a distributed computing environment isn't simple. The situation is further aggravated by the set of new terminologies and jargon that the security industry employs. In this chapter, we'll demystify many of the concepts, and discover the available APIs, system objects, and built-in system features which facilitate the implementation of secure distributed computing systems.

On the other side of the coin, we'll see the roadblocks, harassment, and trepidation that an ActiveX component has to live and deal with in order to carry out its chores in a secured environment.

The branches of security that we'll be focusing on are those related to distributed component computing (i.e. COM and DCOM security), as well as web server extension security (i.e. IIS and ISAPI security).


The Need for Network Security

From the beginning in the history of multiuser computing, there was always a need to protect and secure information or resources of one user from access by another. Security violations can be intentional or unintentional, vicious or benign; in all cases, it's highly undesirable and best prevented, if at all possible. Now that networking machines and sharing of resources and information becomes enterprise-wide (in the intranet) and global (in the Internet), the need for sound and robust security measures is more important than ever before.

The security industry is a relatively 'fear' driven one, and deservedly so. The sensational cases that occasionally surface, with employees or demonic individuals bringing down entire enterprises through electronic attack or espionage, is enough to drive any executive into committing major resources to prevention measures.

There are many reasons why you should secure a web server or distributed software components in an intranet environment. Most of them revolve around the previously mentioned need to protect the information and/or resources belonging to one party against access or tampering from other unauthorized parties. In order to carry this out, one typically has to provide means for:

  • Restricting access

  • Providing positive identification and authentication

  • Ensuring data integrity (i.e. against embedding of a virus into a program, or data tampering during network transmission)

  • Ensuring privacy (i.e. against 'secret peeks' into unauthorized data)

We'll briefly explain these terminologies and see how they all fit together.


Restricting Access

Let's try to clarify the definition of access. Access is the ability of the accessing entity of an object to interact with the object. Object in this sense doesn't refer to an ActiveX object or a COM object, but rather any logical or physical entity in the computing environment. The accessing entity isn't necessarily a person sat at a keyboard, but could quite easily be another object (for example, an ActiveX component running as an unattended service).

There are different types of access too. Broadly speaking, they are: view, read, change, delete and execute. Viewing means that the accessing entity is aware of the object in question, but can't do anything with it. Reading allows the accessing entity to view the contents of the objects, but not change that content. Change means that the contents can be updated, while delete means the object can be deleted by the accessing entity. Finally, there's execute. Execute is possibly the most 'dangerous' as the object may have access to other objects that the accessing entity normally has no access rights to.

Access rights

There's a need to assign different levels of access to objects in a system. Access rights is the mechanism by which security-conscious operating systems (for example, Windows NT) follow in order to achieve this. Access rights specify what the accessing entity has the right to do with the object. Objects in the system have an associated list of access rights that specifies which accessing entities have which rights.

This brings us to the issue of how to ensure that the accessing entity is who they claim to be, so that they can be allowed to exercise the rights they may have. That is, the accessing entity must be authenticated.

Identification and Authentication

In most computer systems that support security mechanisms, the notion of a user or account associates the accessing entity with certain access rights to the objects managed by the system. Any programs, then, that run on such systems, run 'on behalf of', or 'in the context of' a user or an account.

The security issue that arises immediately is that of ensuring that when an entity presents to the system its credentials purporting that it is user 'A', there's a mechanism to indeed verify that it is, or it is not, 'A'.

Authentication is the mechanism of ensuring or verifying the identity of the entities requiring access to a computer system and is based on the secure exchange of identity information.


Integrity

Integrity effectively means ensuring that data or communications are 'unaltered'. In the context of security, integrity doesn't cover alteration of data or interactions because of nonmalicious, unintentional errors. Taking care of those is the job of error recovery on a local or network scale. Instead, integrity means ensuring that data or communications aren't tampered with. For example:

  • A user has accessed certain system resources. Preserving integrity means that they can't claim that they didn't. History can't be rewritten. Support for auditing the exercising of access rights ensures this.

  • A user has sent a message. Preserving integrity means that they can't claim it wasn't them; actions can be uniquely attributed to the entity that initiated them. Signatures are valid. The technical name for this issue is nonrepudiation.

  • A user sends data, that gets modified en route to the receiving party. Preserving integrity means that the modification can't go unnoticed. You can't change somebody else's letter with impunity! 'Message tampering' activities are detected.

In this context, even authentication can be construed as an aspect of integrity. It preserves the integrity of presenting an identity.


Privacy

Let's define privacy: it's the right to disallow access to one's private data or to one's communications with another party.

If there are no 'third parties', i.e. in a totally isolated system, this requirement is vacuously true.

In reality, however, the world is much more interesting than that and does consist of more than single entities. In addition to setting access rights for known users, the need to use insecure communications channels may mean that you have eavesdroppers who can gain access to the information passing between trusted users without the need to impersonate one of the users.

The fundamental mechanism that provides solutions to the issue of privacy is encryption, a process of transforming the data to be protected into some form that only the trusted parties have the ability to interpret.


Fitting the Pieces Together

The following table shows all these elements fit together in a typical security implementation.

Security Issue | Security mechanism
Controlled access | Access rights, identities
Right for privacy | Encryption
Integrity | Authentication

In the following pages, we'll be examining the security implementation of Windows NT and Windows 95 in the context of ActiveX components and distributed computing in an intranet environment. We'll be revisiting each of the above security elements as appropriate and showing how you can implement and reuse them.

ActiveX and Windows NT Server Network

Thankfully, when dealing with ActiveX based technology in a Windows NT Server network, there is a lot that can be done. The Windows NT Server product is designed from the conceptual stage to provide a secure computing environment.

Security isn't something that can be added to an operating system as an afterthought. It has to be designed into the core of the system from day one. In particular, it was designed to meet the so-called C2 security guidelines set out by the U.S. government. Even though being certified for compliance with the C2 guidelines may be required only for deployment in government organizations, the enhanced security that compliant systems offer is very important for businesses at large as well.

The main requirements for C2 compliance are:

  • User identification and authentication. The system requires the users to prove their identity before they are allowed access.

  • Auditing. User actions, and object access can be logged by the system.

  • Discretionary access control. Objects on the system have owners who can grant or restrict at various levels access to the resources.

  • Object reuse. The system guarantees that discarded or deleted objects are not accessible by other entities. This holds true, for example, for deleted files, deallocated memory, etc.

  • System integrity. The system protects resources that belong to one entity from being read or written to by other entities. For example, the memory that's been allocated to a process isn't accessible by other processes.

The C2 guidelines refer to standalone systems only, and are published in an orange book aptly called the Orange Book. Guidelines for networking aspects of security are covered in the Red Book.

NT 3.51 received C2 certification in September 1996. NT 4.0 is undergoing networking and C2 certification at the time of writing.

With such robust security support in place, network software, application software, and distributed components (really networked application software pieces) can leverage off these system features to extend the secure computing environment. We'll see how this is done throughout the rest of the chapter.


Where We Need Security in an Intranet

Other than controlling and administering access to the individual machines in an intranet (this is the function of a network operating system), we may want to restrict access to certain intranet-accessible information to only a selected group of users. For example, we may want everybody to be able to access the general Accounting Department web server, but we only want people in the Accounting Department to access a draft annual report which is linked to from the home page.


Controlled Access to Web Pages

In many ways, this type of protection is very similar to the access control that's supplied by the network operating system like Windows NT Server. What's different, though, is that the user may not be currently 'logged in' to the network serving the page. He or she may be totally remote, may not be using an ActiveX-enabled browser, and may even be running on a Macintosh computer. Yet, we still want to be able to enforce the same access control.

It's easy enough to protect specific web pages or other files from casual access. Simply use the file manager and put an explicit access control attribute on it (assuming you're in a secured installation using the NTFS file system). This, however, will prevent all users from accessing the protected pages, including the users who you have explicitly given permissions to access.

Thinking through the situation, whatever the solution may be, the web server must somehow obtain the capability to identify who the user is at the other end of the connection (sitting behind the browser) in order for it to work. This, of course, boils down to the ability for the server to authenticate a remote user accessing the web server. How would such authentication be performed, though? Even if we obtain the true identity of the user somehow, what if the network hosting the web server doesn't know about the user at all (i.e. there's no account information on the user)? In our example, consider the case where Joe Belmont of Accounting is trying to access the secured draft report (which he has access rights to) from Italy over a modem using a Unix machine at a trade show. We'll take a look at ways in which this authentication can be done today in the next section. This will give us an insight into the security problems faced by the IIS and ISAPI applications.


Secure Intranet Interactions

Ensuring orderly controlled access, data integrity and user privacy in an Intranet environment requires careful analysis of the access scenarios and access patterns that a typical user of an intranet application (or Active web site) may undergo. Designed too tightly, the user may end up with an impression of a highly restrictive, and rather frustrating (i.e. repeated requests for password and user IDs) experience. Designed too loosely, the system/application may be subjected to security abuse and vicious attacks.

Before we look at how to secure IIS, ISAPI and DCOM, let's look at the state-of-the-art with respect to security in the 'normal' web page access scenario. This access pattern is fundamental to our later discussions.

Basically, we're talking about the scenario we've described in the previous section. We need to authenticate a remote user accessing a web server in order to restrict access to certain web pages. We'll look at three specific cases and see how the problem is solved. The first case will be the general case where both the browser and the server may not be a Microsoft product. The second and third cases feature a Microsoft browser and a Microsoft server, but one over the public Internet and the other over a private intranet. As we'll soon realize, even though everything looks very similar above the surface, major differences in security implementation exist under the covers.


A Non-Microsoft Scenario

If you've surfed the Internet at all, you've probably encountered sites or web pages which require a password and user ID. When such a page is reached, the browser would pop up a little password entry dialog box. The interesting thing here is that there seems to be something built into the browser-server combination that provides this authentication capability. Yet, if you were to look into the documentation for the browser (and sometimes even from the server), you may be surprised to find that this capability is often not well described. At any rate, there seems to be no standard way to follow for authenticating a remote user using a random browser and server combination. How exactly is this done?

The secret, it turns out, lies in the transmitted HTTP packet's header information.

The scenario which leads to the triggering of the pop-up on the browser is as follow:

  1. The client clicked on a link on the browser display which attempts to access some protected resource

  2. The browser, having no way to tell a requested resource is protected, formats a normal HTTP request packet and transmit it to the server asking for the resource

  3. The server attempts to access the resource and faces an 'access denied' situation

  4. The server sends an access denied HTTP response packet back to the client, but in the header indicates the authentication schemes that the server will support in the order of preference

  5. The browser receives the 'access denied' packet, and looks into the header to find the authentication schemes supported by the server and matches it against what it would support

  6. If the lowest common denominator between the browser and server authentication support is basic authentication, the browser will pop-up the dialog box asking the user for a user ID and password

  7. The user keys in the user ID and password

  8. The browser retries the request packet, this time including the desired authentication method, the user ID, and Base64-encoded (totally unsecured) password in the header

  9. The server receives the request packet with authentication information, impersonates the client if authentication is successful, and accesses the protected resource on behalf of the remote user

  10. The server sends the contents of the requested resource back to the browser which promptly displays it

We should notice a few distinguishing points about the above scenario:

  • Nothing about it is specific to Microsoft or ActiveX: the situation can be handled with any browser supporting basic authentication, and any server running on any operating system which supports authentication capabilities

  • If the user were to access a series of protected pages, the above scenario would repeat itself, asking the user to repeatedly enter his or her user ID and password

  • The password of the user is sent over the wire with minimal encoding, leaving it vulnerable to potential interception

The form of authentication detailed above is called basic authentication and is the most widely used one over the Internet, since most browser, web server, and operating system combinations will support it.


Internet Explorer with IIS over an Intranet

Now let's switch some details of the previous scenario. We're on a private intranet that is controlled under one Windows NT domain (see the next section coverage of Windows NT security); the IIS 3.0 server is running on a departmental server machine. We're accessing the IIS server from a Windows 95 client logged on as an Accounting department user, and running Internet Explorer 3.01.

Picking up from Step 4 above, we now have a greatly improved situation:

  1. The server sends an access denied HTTP response packet back to the client, but in the header indicates the authentication schemes that the server will support, in the order of preference. At the top of the list is NTLM challenge/response authentication (see next section for details).

  2. IE 3.01 receives the 'access denied' packet, and looks into the header to find the authentication schemes supported by the server and matches it against what it would support. It likes the NTLM challenge/response authentication because it supports it.

  3. IE 3.01 will actually initiate the challenge/response authentication process by informing the NTLMSP. The user will not be asked for the user ID and password, because IE 3.01 can cause it to be passed to NTLMSP (the user ID and password is cached by the system as long as the user remains logged on).

  4. NTLM-based challenge/response authentication takes place over the network across the two nodes using a proprietary (non-HTTP) protocol. See the next section for details of how NTLM challenge/response works.

  5. The challenge/response sequence terminates with the server impersonating the client if authentication is successful, and retries the access to the protected resource on behalf of the remote user.

  6. The server sends the contents of the requested resource back to the browser which promptly displays it.

Note the great improvement of this scenario over the previous one:

  • The password of the user is never transmitted over the network; there's no risk of password capture.

  • The user is never prompted for user ID and password. To the user, the protected web pages were accessible just like unprotected pages.

This improvement certainly makes intranet surfing (and intranet resource access control) much simpler to manage. However, there's one potential drawback: everything is from Microsoft, from the desktop OS to the network OS, from the desktop browser to the web server.


Internet Explorer with IIS over the Internet

Finally, let's take a look at the situation if the previous scenario took place over the Internet instead. Here, we still have everything from Microsoft. However, since the user logged on is authorized against a domain that's separate and unconnected from the server machine. One can't enjoy the cached user ID and password capability.

In this case, the IE 3.0 and IIS combination will still try the NTLM challenge/response sequence first using the cached user ID and password at the client end, but this attempt will fail (because the user is unknown to the server domain). IE 3.0 will then pop up a dialog box prompting the user for an ID and password. Next, IE 3.0 will reattempt the NTLM challenge/response authentication using this new set of user ID and password.

Since the user will enter a user ID and password which can be authenticated in the IIS's domain, this will succeed and the server will reply with the protected data. From this point on, if the server requests any further authentication, IE 3.0 will use the user-entered user ID and password to authenticate the request.

The end result is somewhere between the transparent, all-Microsoft intranet situation, and the any-browser/any-server situation:

  • The user's password is never transmitted over the wire

  • The user only has to enter his or her user ID and password once for the entire session, accessing multiple protected objects


Best Method for Secured Intranet

We see above a gamut of ways in which a web browser and server combination would react in a secured environment. It's interesting to observe that a fully homogeneous Microsoft installation in the intranet scenario can make security control completely transparent to the end user. This is obviously done by design rather than by accident, and is often a consideration when IS departments consider new intranet implementations.


Basic Windows NT Security

Before bravely diving into the specifics of IIS, ISAPI and DCOM security, it's essential that we give a brief coverage to basic Windows NT security. This is the security framework upon which all IIS, ISAPI and DCOM security implementations are built. An understanding of the basic framework will go along way in ensuring that our later discussions can proceed without hindrance.


The Security Model

The security model prescribed by the Windows (and Active) platform is a client-server one. The model is pervasive in that a network need not be involved; we can be talking about client and server processes running on the same machine. In this situation, the partitioning is between the application space that the client is running in and the (usually) kernel mode protected space that the server is running in.

Under this model, clients should only access objects through servers. They should never access objects directly. This means that access to files, devices, other servers, etc., must be arbitrated and 'passed through' by an intermediary server.

Servers, on the other hand, must manage the access of the object and enforce access control. This can be done by the server through assistance from the operating system, or the server may decide to 'roll its own' access control mechanism. Specifically, under Windows NT, it's typical that servers don't actually maintain and verify access rights directly. To make life simpler (and more secure) for server writers, servers are requested to impersonate the identity of the client, and attempt access to objects under such impersonation. In this way, unauthorized access would be prevented by the operating system without the server maintaining elaborate access verification schemes.

Windows NT provides such an impersonation capability for server applications through Win32 APIs.


The Elements of Security: Domains, Users, SIDs, and SDs

From the operating system point of view, everything creatable on Windows NT is securable. By 'creatable' we mean any object (excuse our overloaded usage of the word object here again) that either the system can create or the programmer may create using programmatic means. This includes the standard files and devices, but also applies to processes, threads, and even more mundane things like a semaphore, a piece of shared memory, or a registry key. By 'securable' we mean that specific access rights can be associated with it, and those rights can be verified by the OS when the object is accessed.

The implementation for such security handling is buried deep, deep in the kernel space. Essentially, every new object created gets a handle which can have a security descriptor associated with it. We'll talk about the composition of this security descriptor in a section coming up soon.

Domains

A domain is an administrative grouping of networked machines (and all the users plus resources associated with these machines). The domain controller machine in a domain stores and authenticates all users in the domain for network resource access. This means that the user and password database is stored and managed by the domain controller machine. Windows NT domain implementation allows for a Primary Domain Controller and a Backup Domain Controller. The Backup Domain Controller sits on the network and gets a replication of all modifications and changes to the Primary Domain Controller. If and when the Primary Domain Controller should go out of commission, the Backup Domain Controller can take over as the Primary Domain Controller, ensuring continued network operation. Note that it isn't currently possible to perform any administrative operations on the Backup Domain Controller while the Primary Domain Controller is still alive and operating.

For proper secured operations, every machine within a domain should authenticate user IDs and passwords against its domain controller.

For every large installation, it's possible to organize multiple domains in one or more 'trusted relationship'. This basically allows all users in one domain to access resources on another (access rights are still checked, of course). Operationally, a domain controller will authenticate a user against a trusted domain should it fail to authenticate the user locally. This allows for a degree of separation between the functions of user-account administration and shared resource management.

Authentication Authority

Each and every time a user on a secured Windows network is authenticated, three pieces of information are required. The authentication authority (i.e. the domain which the user belongs to), the user ID, and the password. For example:

UserID: WRXDOMAIN\JULIAN

Password: xyzzy

indicates that the authentication authority is the domain controller of WRXDOMAIN, the user ID is JULIAN, and the password is xyzzy.

If the authentication authority isn't explicitly named, it will always default to the local authentication authority. This means the local registration database on a non-domain controller Windows machine, and the domain registration database on a domain controller machine.

Users and Groups

To make administration and assignments of access rights somewhat easier, Windows NT Server-based networking allows the assignment of users to groups. A user is considered a 'member' of a group if the group is defined to be containing the user. Access rights can then be assigned to the object, allowing or denying access to a group instead of spelling out all its members.

Groups which contain purely users from a single domain are called global groups; these groups are assigned and tracked on the Domain Controller. Groups which are administered local to a machine are called local groups and can contain users on the local machine, users from trusted domains, or global groups.

Security Identifier

Each user on a secured Windows network has an account, the account contains information such as password, groups that the user belongs to, profile-specific items, hours allowed to logon, etc. It also contains something called a Security ID (or SID). An SID uniquely identifies the user in both 'space and time'. What this means is that no two SIDs generated will ever be the same due to the generation algorithm. It contains information which can be used to uniquely locate the user's account information on a registration database. Notice that a group can also have its own SID.

Security Descriptors

As mentioned earlier, every elementary operating system object created by the operating system can be associated with a Security Descriptor. This descriptor contains the following important pieces:

Information Contained | Meaning
Owner | Owner of the object, qualified by authentication authority.
Group | Group that the owner belongs to, qualified by authentication authority.
Discretionary Access Control List (DACL) | A set of access rights set by owner or administrator.
System Access Control List | A set of access rights set by the system for system operations.

The discretionary ACL can be used to specifically allow or deny access to the object by certain user and/or groups. The OS provides set and query operations to this descriptor. It also provides APIs to assist in manipulation of access lists, perform access checking, or assist in generating operating system based audits.

Let's now take a look at what Access Control Lists (ACLs) are.

Access Control Lists

An access control list consists of zero or more Access Control Entries (ACEs). Each ACE will contain:

  • The type of entry—either an ACCESS_ALLOWED_ACE_TYPE or an ACCESS_DENY_ACE_TYPE

  • The access rights to control (i.e. Read, Write, Read-Write, etc.)

  • The user or group (global or local) as represented by its SID

The access control list within a security descriptor that can be changed by applications or users programmatically is called the discretionary access control list (DACL). All SDs and the associated ACLs are stored in compact binary form.

It's interesting to note that an object which has an SD with a DACL containing no ACE entry is deemed to be explicitly inaccessible to everyone, while an object that has an SD with no DACL (i.e. set to NULL) is accessible by everyone.

Historically, programmatic modification of ACLs has been a very complex procedure requiring painstakingly careful coding (and testing since you can easily lock up a machine or a domain with faulty ACL modification code). The recent release of Windows NT 4.0 Service Pack 2 has fixed this problem through the implementation of a well-tested body of code in the form of a reusable COM based software component! This provides an interface called IAccessControl which ActiveX/COM programmers can readily reuse in situations requiring modifications to ACLs.

Security Access Tokens

ACLs are associated with static objects such as files, devices, and users. For dynamic objects in the operating system (things that come and go relatively quickly), such as processes and threads, they have associated with them something called a security access token. It's this security access token which enables a server to impersonate a client after authentication. Among other information, the security access token contains the following very important information:

  • The user SID, used in matching against ACLs.

  • The SID of the groups that the user belongs to, used in matching against ACLs.

  • The default ACL for creating new objects.

  • Default owner SID for creating new objects.

  • Special token privileges, such as reboot, settime, logout, etc. These are used in very special cases only.

Since it's always servers (or threads) which actually access protected resources, it's important that threads can carry with them a context for security. The security access token is such a context.

A thread of execution assumes an alternative personality (i.e. impersonates others) by switching security access tokens. The same thread can later revert back to itself by switching back to the base process's security access token.

Client Credential

Client credential refers to the set of user ID and password information maintained by a server on behalf of a calling client. This is often required, in addition to an impersonation token, to access networked resources on other networked nodes.

SSPI and NTLMSP Authentication

We've been talking about authentication thus far as if it just magically happens between the client seeking authentication and the server performing the authentication. If the client simply sends the textual user ID and password over the network to the server, the system can't be totally secure because other nodes on the network can easily capture the user ID and password, and wreak havoc on the server with this information.

Since so much of the Windows security implementation rests on dependably robust and secure authentication, the above simple scenario can't be depended upon for proper authentication.

Instead, Microsoft has designed an elaborate architecture for handling authentication. This architecture allows the actual method of authentication to be abstracted away from the user using the authentication service. As a side benefit, it allows arbitrary extensibility through the 'plug-in' of new authentication providers. This architecture is frequently referred to as the Security Service Provider Interface (SSPI).

The following diagram shows how the SSPI fits in with the rest of the security implementation.

The architecture clearly separates the user of the security services from the provider of the services, and allows newer service providers to 'plug-in' and function with existing application using the SSPI services.

While the architecture is indeed elegant, the only easily usable default security provider for Windows NT 4.0 is the NTLM security provider (or NTLMSP). As a bonus, the client portion of the NTLM security provider is also implemented on Windows 95 so that Windows 95 client workstation can participate in the network.

Since this is all we've got today, it may be worthwhile to understand how NTLM works. The key element here is that the user's password is never transmitted across the wire during authentication. This is both NTLM's strong point (for security) and its weakness. We'll cover the weakness part later, but first let's see how authentication can be performed without sending the password over the wire. When a client application supplies the authentication authority, user ID, and password triplet to the client portion of the NTLMSP, the following happens:

  1. The client NTLMSP sends the domain, user, and machine information to the server NTLMSP

  2. The server NTLMSP uses this information to generate a unique challenge sequence (of binary bits)

  3. The client NTLMSP receives this sequence and encrypts the sequence using the user's password as a key, this forms the response

  4. The client NTLMSP sends the encrypted response to the server NTLMSP

  5. The server NTLMSP calls the authentication authority with the user ID, the challenge sequence and the response sequence

  6. The authentication authority uses the password associated with the user ID to encrypt the challenge sequence and verify it against the response sequence, authenticating the user in the process

  7. If successful, the authentication is completed, and the server NTLMSP will allow the server to impersonate the client

So far so good: only an encrypted response is sent across the wire, but not the actual password. However, since the server actually never receives the password, the server will have no way of using the password to access other resources that user would have access to. Specifically, these may be network resources (i.e. files, databases, etc.) that the user would normally be able to access. This is one weakness of NTLMSP, significantly reducing the possible tasks which the server can perform on behalf of the client.

It's anticipated that the Kerberos security provider will be available with Windows NT 5.0 in the future. It's based on a mature public standard originated from MIT called MIT Kerberos V5 and endorsed by the Internet Engineering Task Force for interoperability. This new security provider is expected to provide improved performance, scalability and flexibility over the NTLM implementation. Most important of all, it will eliminate the problem we've cited with the NTLM provider.

NTFS

The NT File System format (called NTFS), although not new, is an essential piece of the Windows NT security puzzle. While NT supports alternative file format for hard disk, such as FAT and HPFS, one must make use of NTFS to enable a fully secured system. Besides offering higher performance and less fragmentation with large disks, it is the only NT-supported disk format which allows ACLs to be associated and stored with files and folders. This capability is vital to the proper operation of a secured Windows network.

As a bonus, NTFS also supports optional journalizing of all disk transactions, allowing the file system to be totally recoverable against disk failures.

Authenticated RPC

Take a careful look at the diagram in the previous section, and you'll notice a box called 'Authenticated RPC' under the 'DCOM Security' box. This implies that authenticated RPC, whatever it is, is the foundation upon which DCOM security is built on. It would be wise, then, for us to take a look at what authenticated RPC is about.

You'll frequently see the mention of Secure RPC as well. They actually refer to the same thing. Secure RPC can be viewed as where authenticated RPC is heading in the near future.

We've learned that DCOM (or COM for that matter) is based on RPC (or LRPC) as the interprocess communications mechanism. We also learnt that RPC can operate over a variety of network transports, including TCP/IP, IPX and named pipes. The named pipe protocol has provided authenticated connections across networks long before the arrival of RPC. Unfortunately, other TCP/IP or IPX based protocols don't offer such features.

By building in code which will call the SSPI service, and bundle additional security information 'on-the-wire' in the runtime implementation of RPC, Microsoft has created authenticated RPC which can operate over all transports supported by the raw RPC implementation.

This can be viewed as a layer of software on top of RPC (but since it's implemented in the RPC runtime code, it can be treated just as a variant of RPC) which provides a secured channel for intermachine procedure calls. The specification for Secure RPC has five levels of security:

Security Level | Meaning
NONE (1) | This is regular RPC with no security ramification.
CONNECT (2) | Authenticate the client connection during the connection phase (i.e. when the initial TCP/IP socket is established under TCP/IP).
CALL (3) | Authenticate the request for each and every interface call.
INTEGRITY (4) | Authenticate and verify that the request packets received have not been modified.
PRIVACY (5) | Perform all of the above and encrypt the data packets for transmission of the wire.

Obviously, level 5 security would be very CPU-intensive when performed on a packet level. For performance reasons only the CONNECT level of security is implemented on Windows 95 clients. The symmetric connection nature of RPC requires both ends of an RPC connection to be at the same security level. This means that most applications or software components must work on the CONNECT level of security, or below, if it is ever to accommodate Windows 95 clients.

So far, Windows NT appears to be more secure than Windows 95. We have always been saying '...but Windows 95 cannot do this...' or '...except for Windows 95 because...'. Is Windows 95 really a security 'wimp'? You bet your CDROMs! Let's take a good look at what's missing.


IIS and ISAPI Security

This section focuses on the security issues that surround the usage of IIS and the implementation of ISAPI based extensions, of both filters and applications types. The rules are the same whether the extensions are part of the system (i.e. Active Server Pages, the Internet Data Connector) or ones that you and I write (i.e. our samples in the Server Extensions chapter).

IIS is a process running on the Windows NT operating system, and we learnt earlier that every process has a Security Access Token containing the security context under which the threads in the process would run by default. For the IIS, this is a very powerful system user. This particular user has access to almost everything on the local system, but no access whatsoever to networked resources.

Due to the tremendous power that the system user has, we couldn't possibly expect the IIS to use the system user's context when serving web pages or running ISAPI extensions. And, of course, it doesn't. Instead, when servicing a user request to access web pages and other server resources, the working thread(s) are operating under impersonation (via a change of the Security Access Token) in the security context of a local user account. This account could be a 'default' one that can be set during IIS setup, or a user account specified by the requesting browser when submitting the request.

All the security problems that arise during access through IIS revolve around the proper matching of the access rights of the requesting user, the user account under which IIS executes the request, and the rights associated with the resources that are accessed during the processing of the request.

The exact security context that will be used depends on the HTTP requests that the IIS receives and the setting of the Authentication Options in the Internet Service Manager configuration. The WWW service of IIS can be set to one or more of the following authentication options:

  • Allow Anonymous

  • Basic

  • Windows NT Challenge/Response

Let's look at each of these options.


Anonymous Authentication

If the Allow Anonymous option is set, IIS will choose to use Anonymous Access wherever the headers of the HTTP request being served doesn't contain user ID and password information.

Under Anonymous Access, all access to web pages and resources are done under impersonation of the default anonymous user account. This account is created and setup during IIS installation.

The account, by default, is named after the server machine as follows:

IUSR_<machinename>

It's given a random password and permissions to 'log on locally'. This allows the IIS threads executing requests of an anonymous user to access resources on the local server machine, but not venture out into the local network. This user will also be made a member of the local Guest group. This setting allows the anonymous account to access most of the 'public' contents on a server machine.

Note that, if IIS is installed on a primary domain controller, then the anonymous user account is added to the domain account database. You can use this technique to extend the visibility of the anonymous account. For example, all other servers in the Windows NT domain can now 'publish' information which can be accessed by the IIS anonymous user.

If you have a very large domain with multiple IISs installed on several domain controllers, it's often more convenient to change all the anonymous access accounts to use the same user ID and passwords. This makes it simple to set the ACLs for anonymous serving of 'public' contents throughout the entire enterprise. One can change the anonymous access account information on the Internet Service Manager configuration utility at each IIS installation.


Basic Authentication

If the Basic authentication option is set on the Internet Service Manager configuration, the IIS will honor authenticated access for HTTP requests that contain user ID and password information.

As we've discussed before, most browsers don't automatically include user ID and password information with HTTP requests. Instead, this will only happen if access is denied when the server attempts to access a page or resource. At this time, a request denied HTTP response is sent back to the browser, with a list of authentication methods supported by the server in preferred order. Now, if the server indicated that it would prefer Basic HTTP Authentication, the browser (if capable) will honor it by popping up a dialog box requesting the user ID and password from the user. Only after the user has entered the user ID and password will the browser actually send an HTTP request that contains user ID and password information in its header.

Basic HTTP Authentication involves sending the user ID information (potentially containing an authentication authority—WRDOMAIN\JULIAN), and the password over the wire using simple Base64 encoding. This is equivalent to sending the password in clear text, and is inherently insecure. Ironically, this is also (currently) the most powerful form of authentication available for the IIS. It's powerful because the server actually obtains full credentials including the password of the user. With this information, the server can access all the resources that the original user can access, including network based resources which require passwords for access.


Windows NT Challenge/Response Authentication

If the Windows NT Challenge/Response authentication option is set on the Internet Service Manager configuration, the IIS will honor authenticated browser-initiated NTLM-based authentication.

We've covered this scenario in the Secure Intranet Interaction section before.

Recall that the NTLM protocol is secure in that the user's password is never transmitted over the network. This also makes the authentication significantly weaker because the server doesn't have the complete credentials of the user. It can only access local resources under impersonation with NTLM authentication.

Regardless of the authentication method you use, the appropriate permissions must be carefully set to all directories or files accessible through IIS, taking care to protect any sensitive contents. For complete safety, if IIS is to be used for disseminating public information, no sensitive data should be stored on the same machine. This is particularly important if you can't guarantee the correct behavior (from a security standpoint) of any and all extensions and scripts running on the system. Recall that IIS by default runs under the system user security context with complete access to all local resources. A badly coded server extension may accidentally allow access to local files which the requester would otherwise have no access to. However, the extension will not be able to accidentally access nonlocal resources. To prevent this from happening, an ISAPI extension must take certain steps in order to ensure that it doesn't grant requesting users more rights than they are entitled to.


ISAPI Applications Security Issues

IIS offers FTP, Gopher and HTTP server capabilities, each running as an NT service. An NT service is a running process that is, potentially, started at system startup and may be running while users log on and off the machine. It appears to be part of the system services. As a process, it must run in the security context of some NT user account. Depending on the type of the service, the account may be a generic, privileged system account, or a specific account with particular rights. We know from the previous section that IIS runs under the almighty system user account. For other services, like some of the Microsoft BackOffice servers (Exchange or SQL), it's recommended that they are installed and run under a separate 'service' account to which the 'log on as service' privilege is added. In general, this is a good idea for these other servers since you then have finer control over the access rights of your services by just adding or removing membership to appropriate groups.

Unlike the other BackOffice servers, the 'normal mode' of operation of the IIS involves the access of server-based, operating-system-managed resources on behalf of remote client requests. We already know that the IIS accomplishes this securely through impersonation.

Let's now take a look at how this impersonation happens and what APIs are available under NT in order to exercise finer control on the thread's access rights.

By default, under NT, a newly created thread inherits the security context of its parent process. Access rights to resources accessed by the thread are checked against the process account. In a typical server like the IIS, the server process may have created a pool of worker threads to which it parcels out each request for servicing. Each worker thread services a request, perhaps producing a response to be sent back to the client and then is ready to process the next client request that has been queued. Now, each client request may require access to data (files or objects) that are accessible only to its corresponding user account. Since the server must access data on behalf of a variety of users, with different access rights, two avenues are open:

  • The server process runs as a super-privileged account (with full access rights to all the user's resources)

  • The server process 'impersonates' each client before accessing any resources on its behalf

The first method is very dangerous! A bug in the server process, or a rogue request (depending on how general the request is and what checks and balances have been implemented) may allow an unauthorized user access to somebody else's private data. This method is also inconsistent with the Windows NT security model that we have discussed previously.

Fortunately, Win32 provides a number of API calls that support the second method allowing us much finer control over who accesses what.

The basic principle at work here, one that system components of NT adhere to, too, is this corollary of the general security model:

Before accessing system resources on behalf of a user, assume the identity of that user.

In this way, the server thread has exactly the same access rights as a process started by the logged on client (well, almost, since the impersonated user may not have access to networked resources). These rights are stored with the thread specific Security Access Token. These rights are checked against the ACL for the resources in question by the NT security reference monitor and access is granted or denied. Thus security decisions are kept centralized and protected within the NT executive, deep inside the kernel. The access model is also simplified since all access is reduced to the level of a process instantiated by a logged on user.

Let's look at some of the relevant functions from the security API before we go through an example of how we can use this from within our ISAPI extensions. The security API is large and complex and outside the scope of this chapter—the following subset, however, is sufficient to solve the problem at hand.

BOOL OpenThreadToken( HANDLE ThreadHandle,
                      DWORD DesiredAccess,
                      BOOL OpenAsSelf,
                      PHANDLE pTokenHandle );

OpenThreadToken() opens the Security Access Token associated with the thread identified by the thread handle. As we've discussed before, this access token encapsulates the security context of the current thread, or its parent process, and can be potentially modified and then used in order to change the security context of a running thread—either the same one, or another one. This token is the key to 'impersonation'. The function returns TRUE on success.

Let's take a look at the parameters:

Parameter | Meaning
ThreadHandle This is a handle to the thread for which we want to get the security information. This implies that the calling thread must have permissions to inquire about the ThreadHandle thread.

Thread handles are returned by the CreateThread() call that creates a new thread. A running thread may get at its handle by invoking GetCurrentThread() .

(Actually GetCurrentThread() returns a 'pseudohandle' that the thread can use to specify itself wherever a thread handle is required. If a proper thread handle is required, e.g. to pass to other threads to refer to the current thread, then DuplicateHandle() must be invoked on the pseudohandle.)
DesiredAccess | This specifies an access mask with the requested types of access to the thread access token. As any other object in the system, the access token has a discretionary access control list (DACL) against which the DesiredAccess is checked.
OpenAsSelf This is an important parameter. If set to TRUE, the access check to decide whether access to the thread token will be granted or not, is done against the security context of the calling process. If the value is FALSE, the access check is against the calling thread security context.

In other words, if the thread belongs to a server process running under a privileged account, passing TRUE will perform the check against the privileged account, while a FALSE will result in a check against the current security context of the thread. If the thread is already impersonating a user account with fewer privileges, the check may fail.
PTokenHandle This is a pointer to a handle. If the call is successful, it will be set to a handle to an open access token to the thread. When the token is no longer necessary, the handle should be closed with CloseHandle().

The access rights implied by the token depend on the DesiredAccess parameter; that is, it may not encapsulate the complete set of rights for the thread.

Here are some of the most important values for the DesiredAccess parameter

DesiredAccess Value | Meaning
TOKEN_ADJUST_PRIVILEGES | Required to change the privileges specified in the access token.
TOKEN_DUPLICATE | Required in order to duplicate an access token.
TOKEN_QUERY | Required in order to inquire about the contents of an access token.
TOKEN_IMPERSONATE | Required in order to get an access token that can then be used by a process or thread to impersonate the user with the rights that the access token represents.

An almost identical function is available in order to capture the access token of a process:

BOOL OpenProcessToken( HANDLE ProcessHandle,
                       DWORD DesiredAccess,
                       PHANDLE pTokenHandle );

If we now want to modify the privileges associated with the token, for example, in order to enable a the thread or process to perform a system shutdown, we must use the AdjustTokenPrivileges() function. Note that the function can't add new privileges to the access token but only enable or disable existing privileges.

The main parameters are the token handle in question, an array of the new privileges we want (we can specify whether we want them enabled or disabled), and a pointer to an array to set to the current set of privileges (so that we can revert to it after we're done).

BOOL AdjustTokenPrivileges( HANDLE TokenHandle,
                            BOOL DisableAllPrivileges,
                            PTOKEN_PRIVILEGES NewPriv,
                            DWORD PreviousBufferLen,
                            PTOKEN_PRIVILEGES PreviousPriv,
                            PDWORD RequirdPreviousBufferLen );
Parameter | Meaning
TokenHandle | Handle to the access token to be modified.
DisableAllPrivileges | If set to TRUE, all privileges are revoked.
NewPriv | Pointer to a TOKEN_PRIVILEGES structure. This contains a counter and an array of LUID_AND_ATTRIBUTES structures. Each one of these contains just two members: a locally unique identifier (LUID) for the privilege and a Boolean flag to be set to TRUE if the privilege should be enabled and to FALSE otherwise.
PreviousBufferLen | The size of the buffer passed in the PreviousPriv argument. The calling code must have allocated an array large enough to hold the PreviousPriv TOKEN_PRIVILEGES structure, if we want to keep hold of the current privileges before they get modified. If the size isn't sufficient, the last argument is set to the required buffer size and the call fails.
PreviousPriv | If not set to NULL, it should point to an allocated buffer of size PreviousBufferLen
RequirdPreviousBufferLen | Set to the size of the required buffer pointed to by PreviousPriv that can hold the current privileges.

The function will return TRUE if it has managed to modify at least some of the requested privileges.

Let's go now through an example where the thread in question must set the system time. Since this is a privileged operation, the thread must first attempt to adjust its privileges to include the SE_SYSTEMTIME_NAME privilege, change the time and then revert back to disabling the privilege.

HANDLE      hToken;      // handle to thread token
TOKEN_PRIVILEGES   tp;      // structure to hold the privileges array

// Get the current thread access token
if ( !OpenThreadToken( GetCurrentThread(),      // current thread handle
         TOKEN_ADJUST_PRIVILEGES   // we want to modify the access token privileges
         |  TOKEN_QUERY,      // ask for the existing privileges
         TRUE,  // access check against the process
         & hToken) )
   // error handling

We now need to get the LUID for the system time change privilege. A privilege has a well-known name, an LUID (which is unique on one machine while the system is up—but not necessarily between reboots) and a display string that is meaningful to the end user (in this case, for example, it could be 'Change system time'). We need to look up the LUID for this session, given the text string representing the well-known privilege name. The LUID is assigned to the first LUID_AND_ATTRIBUTES array of the token privilege structure.

LookupPrivilegeValue( NULL,      // system name – NULL for the local system
   SE_SHUTDOWN_NAME,   // string with privilege in question
   & tp.Privileges[0].Luid );      // pointer to 64-bit LUID

Enable the privilege in the token privilege structure.

tp.PrivilegeCount = 1;
tp.Privileges [0].Attributes = SE_PRIVILEGE_ENABLED;

We're now ready to attempt to adjust our thread privileges. For this example, we won't be interested in the current privilege set.

AdjustTokenPrivileges( hToken,   // token handle
      FALSE,      // do not disable all
      & tp,      // new token privilege structure
      0,      // not interested in current structure
      NULL,      // same here
      NULL );      // same here

Now we check whether all requested modifications have taken place. Checking whether the return value is TRUE isn't enough, since the function will succeed even for partial modifications. We need to invoke GetLastError() which will return ERROR_SUCCESS if all modifications succeeded and ERROR_NOT_ALL_ASSIGNED otherwise.

if ( GetLastError() != ERROR_SUCCESS )
   // handle error

The coast is clear! Let's change the system time!

SYSTEMTIME   t;

GetSystemTime( &t );
t.wHour += 1;   // add one hour to current time!
if ( ! SetSystemTime( &t ) )
   // handle error

We now need to disable the change system time privilege to bring things back to the default state.

tp.Privileges[0].Attributes =  0;   // disable it
AdjustTokenPrivileges( hToken,   // token handle
      FALSE,      // do not disable all
      & tp,      // token privilege structure reverting to original state
      0,      // not interested in current structure
      NULL,      // same here
      NULL );      // same here

if ( GetLastError() != ERROR_SUCCESS )
   // handle error

Let's move on now to the core subject of impersonation.


Impersonation

IIS takes certain steps towards security by handling most of the requirements for simple ISAPI applications. Before a request thread is used to invoke an ISAPI application, IIS sets the thread to impersonate the client that submitted the request. Remember that the client must been locally authenticated, either against client-supplied credentials or the anonymous access account. Impersonating the client does two things: it protects resources that the client doesn't have the right to access and potentially allows for network access (by default, the system account is a local account and can't access network resources—network access is only possible when HTTP Basic authentication is used, not when NTLM authentication is used).

However, not all of the extensions are invoked through an impersonated thread. Here's a table showing which entry points to an ISAPI application or filter are called in which security context:

Exported functions | Impersonated (client) | Non-impersonated (system)
GetExtensionVersion() |  | ü
HttpExtensionProc() | ü |
TerminateExtension() |  | ü
GetFilterVersion() |  | ü
HttpFilterProc() (except for some notifications) |  | ü
TerminateFilter() |  | ü

It's obvious that if we want protected access to resources, we must in some way 'cache' the access token from an impersonated call (from the above table HttpExtensionProc() is the prime candidate) and use it when necessary, let's say from TerminateExtension().

Even invoking HttpExtensionProc() by an impersonating thread may not be sufficient. Consider an ISAPI application that must handle requests that do take some amount of time to complete. In this case, the extension must not take up a request thread by staying in HttpExtensionProc() until the call completes. Request threads are scarce resources. Instead, the right architecture calls for setting up a pool of worker threads that are dealt stored requests from a queue. The main purpose of HttpExtensionProc() in this case is to queue the incoming request (that is the extension control block) and return with status HSE_STATUS_PENDING. When a worker thread finishes with the request later, it returns through the control block's ServerSupportFunction() with HSE_REQ_DONE_WITH_SESSION so that IIS can free the resources tied up to the request.

The big problem from the standpoint of security is: how can the worker thread acquire the security context of the client? The request thread is long gone by the time the worker thread gets ready to handle the request.

The answer lies in the access token of the request thread. HttpExtensionProc() is invoked by the request thread. Along with storing the extension control block with the incoming request, it can acquire the access token of the calling thread and store it with the request. When the request thread gets to handle the request, it can use the access token in order to impersonate the client.

Let's first examine the relevant Win32 API functions before we delve into the code.

BOOL ImpersonateLoggedOnUser( HANDLE hToken );

where hToken is an access token that represents the logged-on user. The token may have been returned by a call to, among others, LogonUser(), OpenProcessToken() or OpenThreadToken(). It could be either a primary token (produced by the NT executive) or an impersonation token, acquired from an impersonating thread.

The calling thread needs no special privileges in order to successfully invoke this function. Upon success, with a nonzero return code, the calling thread will continue running in the security context of the access token hToken. The impersonation lasts until either the thread exits or it invokes the function RevertToSelf().

BOOL RevertToSelf( VOID );

Upon success, the function returns TRUE and terminates the impersonation of a client.

Let's go back, now, to the worker pool example we mentioned before. We need to keep track of an extension control block along with an impersonation token for each request. We'll skip the code for storing each request's data to an appropriate data structure as well as the synchronization code necessary to allow multithreaded access to it.

struct  Request
{
   EXTENSION_CONTROL_BLOCK *   lpEcb;      // from request
   HANDLE            hImpToken;   // impersonation token of calling thread
};

// add a request to a list
BOOL AddRequest( const Request& req );

// get a request from the list
Request* GetRequest( void );

For simplicity, in the code snippets below we'll deal with one worker thread.

// worker thread handle
HANDLE   worker;

In the worker pool situation we mentioned above, the first entry point in the extension DLL is DllMain(). It's a good place to create our worker threads, but not as good a place to terminate them. The threads should be terminated in the TerminateExtension() call. Since the threads are created before any requests have arrived, they inherit the security context of the parent process—that's the local system account that IIS is running under.

BOOL WINAPI DllMain( HANDLE hInst,  ULONG reason_for_call,  LPVOID  reserved)
{
   switch (reason_for_call)
   {
      DWORD   threadId;

      case DLL_PROCESS_ATTACH:
         worker = CreateThread (
               NULL,   // the thread gets the default security descriptor
               0,      // default stack size – same as main thread
               (LPTHREAD_START_ROUTINE ) DoWork,  // thread function ptr
               NULL,      // argument to thread function
               0,      // creation flags – 0 means thread runs immediately
               &threadId );   // thread identifier
         break;

      case DLL_PROCESS_DETACH:
         // cleanup
         break;
   }
}

When HttpExtensionProc() is invoked it should acquire the access token of the calling thread and store it in the list along with the control block:

DWORD HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK lpEcb )
{
   Request*  pReq = new Request;
   if (pReq)
   {
      // get the thread access token
      if ( ! OpenThreadToken( GetCurrentThread() ,
            TOKEN_QUERY | TOKEN_IMPERSONATE,
            TRUE,
            & pReq->hImpToken ) )
      {
         // handle error
      }
      pReq->lpEcb = lpEcb;

      // add to list
      AddRequest( *pReq );

      // instruct IIS to hold on to request resources
      return HSE_STATUS_PENDING;
   }
   else
      // handle error

}

The worker thread would effectively wait on a synchronization object to signal that work was added to the list. It would then get a request from the list, impersonate the request client, do the actual work and revert back to its previous security context. We're now done with the request, and we can inform IIS that this is the case.

We should also not forget to close the handle to the impersonation token: otherwise, the resource will remain open until the process exits. Perhaps this doesn't seem a terrible waste and might not be a great concern if your process takes a few milliseconds to run. Memory leaks, however, are of considerable concern for the stability of IIS (under which the extension runs) which in a production environment may stay up for days on end, processing millions of requests. You should employ every means at your disposal to ensure that IIS extensions are as free from memory-related bugs as possible.

DWORD  WINAPI   DoWork( LPVOID parm )
{
   // wait until there is work to do
   ...
   // get a request from the queue
   Request*   pReq = GetRequest();
   if ( pReq )
   {
      // impersonate client
      ImpersonateLoggedOnUser( pReq->hImpToken );

      // do the actual work
      ...

      // revert to previous security context
      RevertToSelf();

      // notify IIS we are done
      pReq->lpEcb->ServerSupportFunction( pReq->lpEcb->ConnID,
               HSE_REQ_DONE_WITH_SESSION,
               NULL,
               NULL,
               NULL );
      // close token handle
      CloseHandle( pReq->hImpToken );

      // free request
      delete pReq;
   }
   ...
}

This concludes our coverage of security issues and techniques related to IIS and ISAPI applications. The basic principals are very straightforward and simple; incorporating them into the design and coding them requires careful planning and meticulous attention to details.


DCOM Security

Now let's look at security issues which are specific to DCOM operations and programming.

We've seen in the 'Authenticated RPC' section that the DCOM security implementation is based on Authenticated RPC. Authenticated RPC uses the security support providers that are available through the Win32 Security Support Provider Interface (SSPI). At this time, Authenticated RPC uses the NTLMSSP exclusively.

Security can be configured externally, that is, without either the client or the server having to include security-specific code. This is suitable for both legacy and simple COM applications. If the security needs of the application are more sophisticated, a variety of functions and interfaces are available to both clients and servers to configure security programmatically, as we'll see shortly.


DCOM Security Blankets

The set of services and parameters that can be configured in order to specify the security settings for a DCOM component is called the 'security blanket'. This is essentially a packaging of security parameters. By bundling security parameters in a package, the negotiation process between the client and the server can be made 'network efficient'. Instead of negotiating each parameter across the network with multiple round trips, only a single round trip is required to complete the negotiation. We'll see how this is possible in the next section.

The parameters contained in the security blanket include:

Parameter | Meaning
Authentication service | The SSPI to use for authentication, e.g. NTLM.
Authentication level | The degree to which to authenticate the access to the object and ensure the privacy of the communications over the connection. These are identical to the security levels of Authenticated RPC.
Server principle | The user security context under which the COM server is running.
Impersonation level | The degree of ‘similarity’ to the client being impersonated which the COM server is allowed during its execution. For example, it is possible to restrict the COM server to only query rights to security information, but not actually impersonate the client.
Authentication identity | The identification of the client, as seen by the server. Usually a user name.


Security Blanket Negotiation

During a security blanket negotiation, the client supplies a security blanket which indicates the maximum level of security that the client can support. When the server receives this security blanket, it matches it against what it would accept. The server decides the minimum level of security which it will accept. If the client's security blanket levels are all above the ones expected by the client, the negotiation succeeds, otherwise negotiation fails.

This approach reduces network traffic down to a single round trip. This low overhead allows security negotiation on a very fine-grain level. For example, security negotiation can be performed on a 'per remote interface instance' level.


DCOM Security Categories

The following security categories are important in DCOM:

Category | Meaning
Access security | Specifying which clients have the right to connect with a running object. Clients might not have the right to launch a server but might be allowed to connect to one if it is already running.
Launch (activation) security | Specifying which clients can start the execution of the server process on the remote machine.
Call security | This essentially allows for security blanket negotiation on a 'per interface instance' level. More precisely, it enables the client to set the security blanket for each server interface proxy object which allows the server to check the security blanket per call when it arrives. It is important to have call level security in DCOM. After a successful launch, the client might pass a remote interface pointer (actually a proxy) to another unauthorized client. If security isn't provided on a per call level, the system can be compromised.


Security Level Configuration

Let's see now how we can configure the system for launch and call security. These activities can be performed through the Dcomcnfg.exe tool that provides a user-friendly interface to modifying the corresponding registry entries. If, like me, editing ACLs in binary in the registry editor isn't your idea of fun, then Dcomcnfg is indispensable!


Launch Security

Launch security is automatically applied by the COM runtime when a server application is started due to a remote object creation request. After the request has been received from a client, the COM runtime obtains all the necessary security parameters from the registry if the creating process didn't specifically specify security parameters.

There are two default activation settings in the registry that have machine-wide effect. They are both named values under the same key.

HKEY_LOCAL_MACHINE\Software\Microsoft\OLE

   EnableDCOM = <value>

The value is a string (REG_SZ) and can be a Y or an N. A value of Y enables remote activation of COM servers on this machine, any other value disables it. Even when remote activation is disabled, local activation is still allowed and is governed by the specific permissions in the LaunchPermission key of each class and the default settings in the DefaultLaunchPermission key.

HKEY_LOCAL_MACHINE\Software\Microsoft\OLE

   DefaultLaunchPermission = <value>

The DefaultLaunchPermission value is of type REG_BINARY and consists of a binary ACL of the principals (accounts or groups) that can have launch classes on this system. The value can be overridden by specifying a LaunchPermission value in the registry for a particular COM object. By default, the following principals are given 'allow launch' permissions:

  • Administrators—the administrator group

  • System—the local system-privileged account

  • Interactive—corresponding to the user currently logged in at the console

On a per-class basis, security configuration settings are stored as a set of named values under the following key:

HKEY_LOCAL_MACHINE\Software\Classes\AppID\

   {AppID_value}\

      <named_value> = <value>

For a class, the AppID_value is a GUID that appears, as a string, under the AppID named value under the CLSID key of the class. The string {AppID_value} is used as a subkey under ...\Classes\AppID.

For an executable, the AppID_value is the name of the module (e.g. myapp.exe). Under the {myapp.exe} key there's a REG_SZ named value AppID with the AppID associated with the executable.

The launch permissions are set in the named value LaunchPermission. The type and content of the value is the same as in the DefaultLaunchPermission. Other named values under the same key are:

  • AccessPermission specifying permissions to access running instances of the class. It's used only if the client doesn't call CoInitializeSecurity().

  • RunAs specifying that the server should run with the security context of the specified user.

  • LocalService specifying that the server is a Windows NT service.

  • ServiceParameters specifying the parameters to be passed to the service on invocation.

  • RemoteServerName specifying the remote machine on which the server will be activated by default, if the client hasn't programmatically requested otherwise (by specifying, for example, a COSERVERINFO parameter to CoCreateInstanceEx()).

You can change the default settings through the Dcomcnfg utility.

The procedure is similar for setting the security parameters of a specific application.

Other than configuring DCOM security parameters through registry editing, it's also possible to have fine-grain control over these parameters using programmatic means. Let's examine some of the Win32 APIs and COM object interfaces associated with DCOM security control.


DCOM Security API

We've covered the CoCreateInstanceEx() in Chapter 8. The new COSERVERINFO parameter allows us to specify a remote machine on which to create the new object instance. The support for multiple QueryInterface() calls in a single round trip keeps everything fast and efficient.


CoCreateInstanceEx() and Client Security Blanket

If we take a look at the COSERVERINFO structure again, there's a member of the structure that we didn't cover at any length. It was the COAUTHINFO structure which is the actual client security blanket.

typedef struct _COSERVERINFO
{
   DWORD   dwReserverd1;
   LPWSTR   pwszName;
   COAUTHINFO*   pAuthInfo;
   DWORD   dwReserved2;
} COSERVERINFO;

pAuthInfo is the security blanket which gets passed through the COM runtime to the server for negotiation. It's instructive to look at the details of the structure since some fields will be identical to the one for the CoInitializeSecurity() call that we'll be covering later. You can find this definition in Wtypes.h:

typedef struct  _COAUTHINFO
{
   DWORD       dwAuthnSvc;
   DWORD       dwAuthzSvc;
   LPWSTR       pwszServerPrincName;
   DWORD       dwAuthnLevel;
   DWORD       dwImpersonationLevel;
   COAUTHIDENTITY *   pAuthIdentityData;
   DWORD       dwCapabilities;
} COAUTHINFO;

dwAuthnSvc signifies the authentication service. It's a value from the enumeration RPC_C_AUTHN_xxx:

dwAuthnSvc Value | Meaning
RPC_C_AUTHN_DCE_PRIVATE | DCE private key authentication.
RPC_C_AUTHN_DCE_PUBLIC | DCE public key authentication.
RPC_C_AUTHN_DEC_PUBLIC | DEC public key authentication.
RPC_C_AUTHN_DEFAULT | The system default authentication service. NT 4.0 defaults to DCE private key authentication.
RPC_C_AUTHN_WINNT | The NTLM Security Support Provider. Except for the default and this one, these services do not currently have native NT 4.0 support.
RPC_C_AUTHN_NONE | No authentication.

dwAuthzSvc signifies the authorization service. In other words, what should the server use in order to check the access rights it should have on behalf of the client. The values are from the RPC_C_AUTHZ_xxx enumeration:

dwAuthzSvr Value | Meaning
RPC_C_AUTHZ_NONE | Server performs no authorization.
RPC_C_AUTHZ_NAME | Server performs authorization using the client’s name.
RPC_C_AUTHZ_DCE | Server performs authorization using the client’s DCE privileges.

pwszServerPrincName points to a wide character string indicating the principal name to use on the server with the authentication name. If the service chosen is RPC_C_AUTHN_WINNT, the value should be NULL.

dwAuthnLevel specifies the level of authentication required ranging from once when connecting, to packet-level authentication and encryption.

DwAuthnLevel Value | Meaning
RPC_C_AUTHN_LEVEL_NONE | No authentication.
RPC_C_AUTHN_LEVEL_CONNECT | Authenticates only when client establishes a connection.
RPC_C_AUTHN_LEVEL_CALL | Authenticates at the beginning of each remote procedure call.
RPC_C_AUTHN_LEVEL_PKT | Authenticates origin of all data. Used by datagram transports.
RPC_C_AUTHN_LEVEL_PKT_INTEGRITY | Authenticates origin and integrity of data.
RPC_C_AUTHN_LEVEL_PKT_PRIVACY | Authenticates origin and integrity of data and encrypts remote procedure call arguments.

dwImpersonationLevel specifies the impersonation level. Corresponds to the levels of impersonation specified in NT 4.0

DwImpersonationLevel Value | Meaning
RPC_C_IMP_LEVEL_ANONYMOUS | The server doesn't get any information about the client identification and doesn't attempt to impersonate the client.
RPC_C_IMP_LEVEL_IDENTIFY | The server can get security information about the client—for example, security identifiers and privileges—but it can't impersonate the client. The significance of this is that the server can make decisions about whether the client has the right to access resources, however, it can't use system resources or access objects 'as the client'.
RPC_C_IMP_LEVEL_IMPERSONATE | The server can impersonate the client’s security context. Note that this is valid only on the server's local systems. It is not supported on remote systems. In other words, the server can't access resources over the network as if it were the client.
RPC_C_IMP_LEVEL_DELEGATE | This level allows the server to impersonate the client over a network. It isn't currently supported by the default SSPs on NT 4.0. It will, however, be supported by the Kerberos SSP which will be an integral part of the Distributed Security Services in the next major release of NT.

pAuthIdentityData: specific to the authentication service. Usually left as NULL.

dwCapabilities: extra capabilities to be defined.


The CoIntializeSecurity() Call

If the client or server calls CoInitializeSecurity(), the default security blanket for the process is set. It has be called only once per process. If a DCOM client or server doesn't call CoInitializeSecurity(), the COM runtime will use the configured default security blanket in the registry. The call is typically made immediately after a call to CoInitializeEx(). The security blanket parameter for the COSERVERINFO parameter of the CoCreateInstanceEx() call can be used to override the CoInitializeSecurity() settings on a per-class basis. Let's examine the calling syntax of the CoInitializeSecurity() API:

HRESULT CoInitializeSecurity( PSECURITY_DESCRIPTOR pSecDesc,
                              LONG cAuthSvc,
                              SOLE_AUTHENTICATION_SERVICE* asAuthSvc,
                              void* pReserved1,
                              DWORD dwAuthnLevel,
                              DWORD dwImpLevel,
                              void* pReserved2,
                              DWORD dwCapabilities,
                              void* pReserved3 );
Parameter | Meaning
PsecDesc | The pointer to the SECURITY_DESCRIPTOR In the first argument, if not NULL—in which case all callers are allowed—specifies which principals are allowed to communicate with the process and which are disallowed.
CauthSvc | This is the size of the array asAuthSvc. If 0 is passed, no authentication service is registered. -1 signals COM to choose on its own which service to use.
AsAuthSvc | Specifies an array of structures each containing a principal name as well as an authentication and authorization service ID. These are the initial values used. The actual services to be used for each connection are negotiated and may be different.
dwAuthnLevel | Default authentication level for proxies. The available values are as in the CoCreateInstanceEx() call.
dwImpLevel | Default impersonation level for proxies. The available values are as in the CoCreateInstanceEx() call.


IClientSecurity for Call Security Control

If the client calls CoInitializeSecurity(), the security values for authentication and authorization for the process are set. If a fine-grain security control is required on the calls to individual interfaces, the client can achieve this by invoking security functions on each of the interface proxies. The main security interface to achieve this on the client side is IClientSecurity. You can obtain it by doing a QueryInterface() on an interface from the remote object for IID_IClientSecurity. In reality, since every remote object method invocation goes through a proxy managed by a proxy manager (who actually aggregates the proxy object), the proxy manager will intercept and provide this interface.

Once the IClientSecurity interface is obtained, the client can change the security blanket for this single proxy instance by first using the IClientSecurity::CopyProxy() method to create a new instance of the proxy, and then using the IClientSecurity::SetBlanket() method of the interface to alter the blanket. It's important to make a copy of the proxy before setting its security blanket to avoid contention with other portions of the process that may be setting the security blanket to other values.

There are wrapper functions available which call the IClientSecurity methods, these include CoSetProxyBlanket(), CoQueryProxyBlanket(), and CoCopyProxy().

Clients can call CoQueryProxyBlanket() to inquire about the existing security blanket on a proxy, CoSetProxyBlanket() to set a new security blanket on a proxy, and CoCopyProxy() to obtain a proxy copy on which a security blanket can be set without contention.


IServerSecurity Interface

On the server side, the server is to obtain the parameters contained in the client's security blanket for this interface and perform manipulation based on these parameters (such as impersonating the client). The main interface for this purpose is the IServerSecurity. This interface also provides control over the security level while executing on behalf of the client. To obtain this interface, which is actually provided by a call context object inside the stub, the server must make a CoGetCallContext() call.

Once the IServerSecurity interface is obtained, the server has the option to:

  • Get specific security level information from the blanket by making the IServerSecurity::QueryBlanket() method call

  • Impersonate the client's security level and identification while carrying out resource access by making the IServerSecurity::ImpersonateClient() and IServerSecurity::RevertToSelf() method calls

  • Determine if the server thread is currently impersonating the client using the IServerSecurity::IsImpersonating() method call

As with the IClientSecurity interface, there are wrapper APIs including CoQueryClientBlanket(), CoImpersonateClient() and CoRevertToSelf(), making the use of the IServerSecurity interface somewhat simpler.


Client-Side Proxy Copying

Occasionally, a client may want to ensure a specific security blanket is set on a particular interface when presented to the server for security negotiation. Directly changing the security blanket on the default proxy (returned by initial QueryInterface() call) will affect all other users of the proxy who may have different security requirements. The way out of the predicament is to make a private copy of the proxy on which to set the desired security blanket.

Beware, however: proxy copies are special in that a QueryInterface() on a proxy copy will return a pointer to an interface on the original proxy, with the original's security blanket.

CoCopyProxy() encapsulates several steps. It does a QueryInterface() on the original proxy for IID_IClientSecurity, invoking IClientSecurity::CoCopyProxy() on it and then releasing it.

HRESULT CoCopyProxy( IUnknown* pProxy, // original
                     IUnknown** ppCopy ); // pointer to copy pointer

The client can now set the security blanket by doing, for example:

IUnknown pCopy;  // copy proxy

CoCopyProxy( pProxy, &pCopy );
CoSetProxyBlanket(  pCopy,
          RPC_C_AUTHN_WINNT,
          RPC_C_AUTHZ_NONE,
          L”HOST”,
       RPC_C_AUTHN_LEVEL_CONNECT,
           RPC_C_IMP_LEVEL_IMPERSONATE,
         NULL,
           0);
// use the proxy
...
pCopy->Release();

Here's the exact declaration:

HRESULT CoSetProxyBlanket( IUnknown* pProxy,
                           DWORD dwAuthnSvc,
                           DWORD dwAuthzSvc,
                           OLECHAR* pServerPrincName,
                           DWORD dwAuthnLevel,
                           DWORD dwImpLevel,
                           RPC_AUTH_IDENTITY_HANDLE* pAuthInfo,
                           DWORD dwCapabilities );

We've seen most of the arguments before!

Parameter | Meaning
Pproxy | Pointer to a copy proxy on which this blanket will be set.
DwAuthnSvc | An RPC_C_AUTHN_xxx value.
DwAuthzSvc | An RPC_C_AUTHZ_xxx value.
PserverPrincName | A wide character string with server's principal name to be used for authentication.
DwAuthnLevel | An RPC_C_AUTHN_LEVEL_xxx value.
DwImpLevel | An RPC_C_IMP_LEVEL_xxx value.
PAuthInfo | Authentication service specific. NULL for default.
DwCapabilities | Extra capabilities for the proxy. Not defined.


Working with IServerSecurity

The server obtains the IServerSecurity interface by invoking CoGetCallContext(). This is the only easy way for the server to work with successfully negotiated security parameters from the client.

IServerSecurity*   pSS;
CoGetCallContext( IID_IServerSecurity, (void **)&pSS );

To get more information on the security blanket from the client, the server may invoke:

HRESULT CoQueryClientBlanket( DWORD* pAuthnSvc,
                              DWORD* pAuthzSvc,
                              OLECHAR** pServerPrincName,
                              DWORD* pAuthnLevel,
                              DWORD* pImpLevel,
                              RPC_AUTHZ_HANDLE* pPrivs,
                              DWORD* pCapabilities );

One of the interesting new fields here is the pPrivs (the type is really a void **) which is set to point to a Unicode string identifying the client. The caller must not modify the string in any way. The default NTLM security provider will return an Domain\\User value.

In order to access resources with the client's security context, the server can impersonate the client of this call and then revert to its own security context when done. You can use the wrapper functions for the IServerSecurity interface for this:

CoImpersonateClient();
//access resources
...
CoRevertToSelf();


Summary

In this chapter, we've given the vast topic of security a fair shake. We began by examining what security means in the intranet context. Narrowing it down to interactions between a web browser and a web server, we examined the specifics of where security measures should be implemented. We found that authentication of the client is a big and essential issue in intranet security. While simple on the surface, this is a complex task underneath and no standard implementation exists today. However, an all-Microsoft solution in an intranet environment can provide a secured environment with security features which can be transparent to an authorized end-user.

Laying the foundation for a more in-depth coverage of security, we examined all the fundamentals of Windows NT security. We covered the basic client/server security model, and stressed the importance of impersonation. We discovered the built-in and designed-in nature of Windows NT security and resolved the definitions of many security-specific terminologies.

We finally extend our discussion of security to the bigger picture of IIS, ISAPI Server Extensions, and DCOM distributed software components. Authentication is vital to proper IIS operations; we learnt about the anonymous user account created by IIS, and saw the importance of impersonation when accessing protected resources. ISAPI server extensions must work in harmony with the IIS and Windows NT security philosophy. This means the implementer must be careful not to grant the client process more access rights than it has. It also means a judicial use of impersonation wherever system resources are accessed.

In our DCOM security coverage, we examined the DCOM security blanket which significantly optimizes the security negotiation process. We discussed Access Security, Launch Security, and Call Security. The importance of fine-grain security control was stressed during our discussion, and we concluded with a comprehensive discussion of the APIs and COM interfaces available for both client and servers to set, discover, and manipulate security parameters.

The message is clear in this chapter. Security in the intranet context, or the distributed component computing context, is a nontrivial matter. A functional, secure computing environment requires careful planning and design. The Windows NT security model gives a solid and robust foundation upon which we can build more elaborate security schemes appropriate for our intranet project, using many of the new Win32 API and COM interfaces available.


Web Scripting with VBScript

PUBLISHED BY
M&T Books
A Division of MIS:Press, Inc.
A Subsidiary of Henry Holt and Company, Inc.
115 West 18th Street
New York, New York 10011
http://www.mispress.com

Copyright © 1996 by Steven Holzner

Printed in the United States of America

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage and retrieval system, without prior written permission from the Publisher. Contact the Publisher for information on foreign rights.

Limits of Liability and Disclaimer of Warranty

The Author and Publisher of this book have used their best efforts in preparing the book and programs contained in it. These efforts include development, research, and testing of the theories and programs to determine their effectiveness.

The Author and Publisher make no warranty of any kind, express or implied, with regard to these programs or the documentation contained in this book. The Author and Publisher shall not be liable in any event for incidental or consequential damages in connection with, or arising out of, the furnishing, performance, or use of these programs.

All products, names and services are trademarks or registered trademarks of their respective companies.

First Edition—1997

ISBN 1-55851-488-0

Library of Congress Cataloging-in-Publication Data

MIS:Press and M&T Books are available at special discounts for bulk purchases for sales promotions, premiums, and fundraising. Special editions or book excerpts can also be created to specification.

For details contact:
Special Sales Director
MIS:Press and M&T Books
Subsidiaries of the Henry Holt and Company, Inc.
115 West 18th Street

New York, New York 10011

10 9 8 7 6 5 4 3 2 1


Using ActiveX Controls in VBScript

In this chapter, we'll look at some of the ActiveX controls, which are essentially limited OCX controls, that Microsoft has developed or distributes for use with VBScript. The ability to use embedded ActiveX controls is one of the most powerful VBScript techniques. In fact, some people consider VBScript simply a means of making ActiveX controls work in Web pages. We will look at some of the most popular ActiveX controls and then go into more depth on three of them: grid controls, graph controls, and multimedia controls. Here are the ActiveX controls we'll review, along with their associated support files in Windows 95:

  • 3-D buttons: threed32.ocx

  • 3-D frames and panels: threed32.ocx

  • Grid controls: grid32.ocx

  • Graph controls: graph32.ocx

  • 3-D checkboxes: threed32.ocx

  • AVI file controls: avifile.dll

  • Multimedia controls: mci32.ocx

  • Spin buttons: spin32.ocx

  • 3-D option buttons: threed32.ocx

  • Sliders: comctl32.ocx

  • Keystate controls: keysta32.ocx

  • Rich text controls: richtx32.ocx

  • Progress bars: comctl32.ocx

    Let's start by seeing how to embed an ActiveX control into a Web page.


3-D Buttons

The first example (ocx.htm) is a Web page that holds a number of the most popular ActiveX controls, and the first step is to insert such a control into the Web page. Let's start by giving our Web page a title and a header ("OCX Control Page"):

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
--> <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .

Now we'll insert our first ActiveX control, the 3-D push-button control. This button is much like the standard buttons we have used throughout this book, but it has a more three-dimensional appearance. The 3-D button is supported by the threed.ocx file, which should be in your windows\system directory if you want to use this control; you can do Windows registration of .ocx files using RegSvr32.exe. We'll need the registration value of this control in the Windows registry.

To get that registration value, you can use the Windows regedit.exe tool (regedit.exe is in your Windows directory); start regedit and select Find in the Edit menu, opening the Find dialog box. Type the name of the control you are looking for, Threed Command Button, in the Find what: box and click Find Next. You can also use Microsoft's aclist.exe tool, available from www.microsoft.com, to list ActiveX controls available in your computer. You'll find that this control has a class ID in the registry: 0ba686b4-f7d3-101a-993e-0000c0ef6f5e. This value, called the control's class ID or clsid, uniquely represents the 3-D command button to Windows. We add it to our Web page using the <OBJECT> tag, giving the 3-D command button the ID Cmd1:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
    <!- SSCommand>
    <PRE>
--> SSCommand:   <OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
                   HEIGHT=30 WIDTH=70 ID=Cmd1></OBJECT>
    </PRE>
        .
        .
        .

Note: Each control that we are discussing in this chapter must be registered with Windows 95 in order to be accessible to VBScript. If one or more of the controls do not work, you should check your Registry to find out what ActiveX controls you have available.

That's the general form of the <OBJECT> tag; we fill the properties of the new OLE control with the <PARAM> tag between <OBJECT> and </OBJECT>:

<OBJECT CLASSID = "clsid:   " HEIGHT = 30 WIDTH = 70 ID=IDname>
                <PARAM NAME = "FontName" VALUE = "Arial">
                <PARAM NAME = "FontSize" VALUE = 12>
        </OBJECT>

Using the <PARAM> tag, we supply both the name of the property we are setting and the value we want to set it to. If we want to let users download our .ocx file automatically (for example, if we know they don't have it on disk), we can use the CODEBASE keyword, indicating that the needed support file, called control96.ocx, is in the server directory ocxcode:

<OBJECT CLASSID = "clsid:   " HEIGHT = 30 WIDTH = 70
            CODEBASE="/ocxcode/control96.ocx" ID=IDname>
                <PARAM NAME = "FontName" VALUE = "Arial">
                <PARAM NAME = "FontSize" VALUE = 12>
        </OBJECT>

In the case of our 3-D command button, we now refer to it as Cmd1 because we gave it that ID:

SSCommand:   <OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
                   HEIGHT=30 WIDTH=70 ID=Cmd1></OBJECT>

As with standard buttons, we can set the button's caption to, say, "Hello," when the page first loads by setting Cmd1's Caption property in Page_Initialize:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
    <!- SSCommand>
    <PRE>
    SSCommand:   <OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
                   HEIGHT=30 WIDTH=70 ID=Cmd1></OBJECT>
    </PRE>
       .
       .
       .
    <SCRIPT LANGUAGE = VBScript>
 -->        Sub Page_Initialize
 -->              Cmd1.Caption = "Hello"
 -->        End Sub
    </SCRIPT>
    </BODY>
    </HTML>

The result appears in Figure 6.1, where you can see the new 3-D button with the caption "Hello."

Figure 6.1 Our ActiveX Controls Web page.

We can use Cmd1 like any standard button, adding a click event like this (assuming we have a textbox named Text1):

Sub Cmd1_Click()
                  Text1.Text = "Hello World!"
            End Sub

So that's how we connect ActiveX controls to VBScript: we add them with an <OBJECT> tag and give them a control ID (such as Cmd1) that we can refer to in code. Now let's examine two more 3-D controls: 3-D frames and 3-D panels.


3-D Frames and Panels

3-D frames usually enclose other controls, indicating that they go together. For example, here is a 3-D frame with the caption "Vote" that encloses two buttons, Yes and No:

    -- Vote -------------------
          |                           |
          |    -------     --------   |
          |   |  Yes  |   |   No   |  |
          |    -------     --------   |
          |                           |
           ---------------------------

3-D panels give the appearance of being raised. If you give them a caption, it appears in the center:

    ---------------------------
          |                           |
          |   Ace Computer Co Inc.    |
          |                           |
           ---------------------------

Let's add such controls to our Web page. We first add a 3-D frame:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- SSFrame>
    <PRE>
--> SSFrame:   <OBJECT CLASSID="clsid:0BA686AF-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Frame1></OBJECT>
    </PRE>
        .
        .
        .

Then we add a 3-D panel:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- SSFrame>
    <PRE>
    SSFrame:   <OBJECT CLASSID="clsid:0BA686AF-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Frame1></OBJECT>
    </PRE>
    <!- SSPanel>
    <PRE>
--> SSPanel:   <OBJECT CLASSID="clsid:0BA686B9-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Panel1></OBJECT>
    </PRE>
       .
       .
       .

Then we add captions for both controls:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- SSFrame>
    <PRE>
    SSFrame:   <OBJECT CLASSID="clsid:0BA686AF-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Frame1></OBJECT>
    </PRE>
    <!- SSPanel>
    <PRE>
    SSPanel:   <OBJECT CLASSID="clsid:0BA686B9-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Panel1></OBJECT>
    </PRE>
       .
       .
       .
    <SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
   -->            Frame1.Caption = "Frame"
   -->            Panel1.Caption = "Panel"
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

These two new controls also appear in Figure 6.1. Now that we've reviewed the process of inserting ActiveX controls, let's turn to a more substantial control: the grid control.


Grid Controls

A grid control is like a two-dimensional array of textboxes:

Columns
                      0     1    2     3     4
                    _____________________________
                0  |_____|_____|_____|_____|_____|
          Rows  1  |_____|_____|_____|_____|_____|
                2  |_____|_____|_____|_____|_____|
                3  |_____|_____|_____|_____|_____|
                4  |_____|_____|_____|_____|_____|
                5  |_____|_____|_____|_____|_____|
                6  |_____|_____|_____|_____|_____|
                7  |_____|_____|_____|_____|_____|
                8  |_____|_____|_____|_____|_____|
                9  |_____|_____|_____|_____|_____|

(There is one big difference here, however: although we can display text in each cell of a grid control, users cannot type directly into a grid control's cells as they can with a textbox.) As you can see, grid controls make a perfect foundation for spreadsheets. We'll deal with grid controls in more detail later; for now, we simply add a new grid control to our ActiveX Web page:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
       .
       .
       .
    <!- Grid>
    <PRE>
--> Grid1:  <OBJECT CLASSID="clsid:a8c3b720-0b5a-101b-b22e-00aa0037b2fc"
                  HEIGHT=100 WIDTH=150 ID=Grid1></OBJECT>
    </PRE>
       .
       .
       .

We set the number of rows in the new grid control using the Rows property, and the number of columns with the Cols property. To make the new grid 3 x 3, we add this code:

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
   -->            Grid1.Rows = 3
   -->            Grid1.Cols = 3
                    .
                    .
                    .
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Next, let's say we want to enter the number 5 in row 1, column 1. To do that, we first set the Row and Col properties (not to be confused with the Rows and Cols properties) to 1 and then enter "5" into the grid's Text property:

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
   -->            Grid1.Row = 1
   -->            Grid1.Col = 1
   -->            Grid1.Text = "5"
                    .
                    .
                    .
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

This places 5 in the grid control, as shown in Figure 6.1. Our grid control is a success. We'll say more about grid controls soon. For now, let's continue our overview of ActiveX controls with the graph control.


Graph Controls

The graph control gives us an easy way to graph data in our Web page:

    _____________________________
          |                             |
          |                             |
          |            _____            |
          |           |     |           |
          |      _____|     |_____      |
          |_____|     |     |     |     |
          |     |     |     |     |_____|
          |     |     |     |     |     |
          |     |     |     |     |     |
          |_____|_____|_____|_____|_____|

To see how this works, we add a new graph control, using its class ID in the <OBJECT> tag:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- Graph>
    <PRE>
--> Graph1:  <OBJECT CLASSID="clsid:0842d100-1e19-101b-9aaf-1a1626551e7c"
                 HEIGHT=100 WIDTH=300 ID=Graph1></OBJECT>
    </PRE>
        .
        .
        .

The ID we've given our graph control is Graph1. Let's say that we have this data to plot:

Point   Value
                -----   -----
                  1      2
                  2      4
                  3      5
                  4      3

We plot it by repeatedly filling the graph control's GraphData property with these values. We place the first value in GraphData using this new code in the Page_Initialize subroutine:

Page_Initialize

--> Graph1.GraphData = 2
        .
        .
        .
End Sub

Next, we fill GraphData with the next data point to graph:

Page_Initialize
    Graph1.GraphData = 2
--> Graph1.GraphData = 4
        .
        .
        .
End Sub

The graph control automatically increments our position along the x axis every time we (re)fill the GraphData property. In this way, we place all the into the graph:

Page_Initialize
    Graph1.GraphData = 2
    Graph1.GraphData = 4
--> Graph1.GraphData = 5
--> Graph1.GraphData = 3
        .
        .
        .
End Sub

Adding the data to the graph was easy; the result appears in Figure 6.2.

Figure 6.2 Our graph and other new ActiveX controls.

We'll look into other graph types soon, but for now let's continue our overview of ActiveX controls by looking at 3-D checkboxes.


3-D Checkboxes

We've seen 3-D buttons, frames, and panels, and it's time to turn our attention to 3-D checkboxes. We add 3-D checkboxes to our Web page this way:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- SSCheck>
    <PRE>
--> SSCheck:  <OBJECT CLASSID="clsid:0ba686aa-f7d3-101a-993e-0000c0ef6f5e"
                 HEIGHT=40 WIDTH=200 ID=SSCheck1></OBJECT>
                 </OBJECT>
    </PRE>
        .
        .
        .

You can see the new 3-D checkbox in Figure 6.2 (the "check" is really an x and not a check mark). When the user checks or unchecks the 3-D checkbox, its click event occurs, and we can set up a subroutine for that event. (This subroutine is passed true if the checkbox was checked and false otherwise.)

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
            End Sub
-->         Sub SSCheck1_Click(OnOff)
                .
                .
                .
-->         End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Another useful control is the AVI file control, and we'll explore that now.


AVI File Controls

With the AVI file control, we can play .avi video files, allowing us to support video animation in our Web pages. We add an AVI file control to our Web page this way:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- AVIfile>
    <PRE>
    AVIfile: [Free Floating]   <OBJECT
-->              CLASSID="clsid:00022602-0000-0000-C000-000000000046"
                 HEIGHT=50 WIDTH=50 ID=avi1></OBJECT>
    </PRE>
        .
        .
        .

This ActiveX control displays a control panel as shown in Figure 6.3 (this control panel appears in its own window on the screen). You can use the File menu to open a file of type .avi, such as skiing.avi, in the Windows\media directory (this file comes with Windows). After opening skiing.avi, you can use the buttons in the control panel to play the video, as in Figure 6.3. That's it—now we're playing videos from our Web page. Our Web page is a success.

Figure 6.3 Our AVI file control.

We're dealing with multimedia now, so let's turn to the multimedia control.


Multimedia Controls

Multimedia controls let us handle multimedia such as video and audio. The appearance of a multimedia control is much like the control panel of a CD player or video tape player:

    --------------------------------------------------------------
         |      |      |      |      |      |      |   _  |   _  |  __  |
         | |/|  | |\|  |  |\  |  ||  |  /|| |  ||\ |  | | |  | | |  /\  |
         | |\|  | |/|  |  |/  |  ||  |  \|| |  ||/ |  |_| |  |_| |  ==  |
         |      |      |      |      |      |      |      |      |      |
          --------------------------------------------------------------

Using these buttons, we can control various multimedia devices. Let's add a multimedia control like this to our Web page:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- MMControl>
    <PRE>
--> MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
                HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
    </PRE>
       .
       .
       .

To see this control at work, let's have it work with a music CD in your CD-ROM drive. We set the multimedia control's DeviceType to "CDAudio" and tell it to open the CD in the CD-ROM drive. We have given the multimedia control the ID MMControl1, so we add a button with the caption Activate MM Control to initialize the multimedia control in Page_Initialize:

<!- MMControl>
    <PRE>
    MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
                HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
    </PRE>
    <CENTER>
--> <INPUT TYPE = BUTTON NAME = "MMButton" VALUE = "Activate MM Control">
    </CENTER>
        .
        .
        .
    <SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                .
                .
                .
            End Sub
            Sub MMButton_OnClick
  -->             MMControl1.DeviceType = "CDAudio"
  -->             MMControl1.Command = "Open"
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

And that's it—when the user clicks the Activate MM Control button, the multimedia control's panel becomes active, and the user can use the buttons to play music from the CD-ROM drive. The multimedia control appears at the bottom of the Web page in Figure 6.2. We'll learn more about this powerful control soon; in the meantime, let's turn to spin buttons.


Spin Buttons

Spin buttons look like two arrowheads, one pointing up and one down:

        /\
                   /    \
                   ------
                   \    /
                     \/

The user can click either the up or the down button; these controls are perfect for setting values that can be increased or decreased, such as volume. For example, if we give the spin button the ID SButton1, VBScript will call SButton1_SpinUp() if the user clicks the upper arrow, and SButton_SpinDown() if the user clicks the lower arrow. Let's add a spin button to our ActiveX control Web page, giving it the ID SButton1:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- Spinbutton>
    <PRE>
--> Spinbutton:  <OBJECT CLASSID="clsid:B16553C0-06DB-101B-85B2-0000C009BE81"
                HEIGHT=50 WIDTH=50 ID=SButton1></OBJECT>
    </PRE>
        .
        .
        .

If we want to catch button click events, we can put in subroutines such as SButton1_SpinUp:

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
                  PBar1.Value = 50
            End Sub
-->         Sub SButton1_SpinUp
                .
                .
                .
-->         End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Our spin button appears in Figure 6.4; we'll see more about spin buttons later. In the meantime, let's look into 3-D option buttons.

Figure 6.4 Spin buttons and more in our ActiveX page.


3-D Option Buttons

3-D option buttons look much like regular option buttons. We add a 3-D option button to our ActiveX overview page like this, giving it the ID Option1:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- 3D Option Button>
    <PRE>
--> 3D Option:  <OBJECT CLASSID="clsid:0BA686BE-F7D3-101A-993E-0000C0EF6F5E"
                HEIGHT=50 WIDTH=50 ID=Option1></OBJECT>
    </PRE>
        .
        .
        .
    <SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

This new option button appears in Figure 6.4. Like other buttons, it has a click event. A value of true is passed to this subroutine if the option button is selected, and false if it's unselected. (When it's unselected, there is no dot in the center of the option button.) In this way, 3-D option buttons function much as the option buttons we've already worked with. Let's turn now to another useful ActiveX control: sliders.


Sliders

A slider control displays a slider that looks like this:

    ---------------------------------
          |        --                       |
          |   ----|  |--------------------  |
          |        \/                       |
           ---------------------------------

The user can use the mouse to move the slider to a new position:

    ---------------------------------
          |                    --           |
          |   ----------------|  |--------  |
          |                    \/           |
           ---------------------------------

After the slider has been moved, we can get its new value and use it in our code; in this way, a slider works much like a scrollbar. Let's add a slider control to our ActiveX control Web page, calling it Slider1:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- Slider>
    <PRE>
--> Slider:   <OBJECT CLASSID="clsid:373FF7F0-EB8B-11CD-8820-08002B2F4F5A"
              HEIGHT=50 WIDTH=250 Change = Slider1_Change ID=Slider1></OBJECT>
    </PRE>
        .
        .
        .

When the user moves the slider, a Change event occurs, and we can catch it like this:

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
            End Sub
   -->      Sub Slider1_Change
                .
                .
                .
   -->      End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Our new slider appears in Figure 6.4. You can set the slider's range by setting its Min and Max properties. After the user moves the slider, a Change event occurs; you can find the slider's new value by checking its Value property. We'll put the slider to work soon. Now, however, let's look at the KeyState control.


KeyState Controls

The KeyState control indicates to the user the state of various toggle keys, such as the Caps Lock key, the Num Lock key, and others. We add a KeyState control like this:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- Keystate>
    <PRE>
--> KeyState:   <OBJECT CLASSID="clsid:B9D22270-0C24-101B-AEBD-04021C009402"
                HEIGHT=50 WIDTH=50 ID=Keystate1></OBJECT>
    </PRE>
        .
        .
        .
    </SCRIPT>
    </BODY>
    </HTML>

We select the toggle key to watch by setting the Style property this way:

Style     Key State
      -----     ---------
        0       CapsLock
        1       NumLock
        2       Insert key
        3       ScrollLock

The default Style is 0, which means the KeyState control displays the state of the Caps Lock key, as shown in Figure 6.4. This control is useful if your code depends on the state of the various toggle keys, because the control displays the state of those keys automatically for the user. Although KeyState checks the state of the keyboard regularly, you can make certain that its appearance is up-to-date at any time by calling its Refresh method. Now let's move on to another powerful control: the rich text control.


Rich Text Controls

A rich text control can display, as you might expect, rich text. Rich text is a special format used to create Microsoft hypertext documents, such as Windows help files, that contain jumps from page to page or topic to topic. In particular, rich text can include underlined text, double-underlined text, hidden text, footnotes, and more. Of course, a rich text control can also display standard text, and we'll use it for that purpose here. We install a rich text control like this in our Web page, giving it the ID RichText1:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- RichText>
    <PRE>
--> RichText:    <OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
                 HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
    </PRE>
        .
        .
        .

Our rich text control appears in Figure 6.4. We can place text in the control by setting its Text property. For example, we can have the rich text control display the text "Hello World!" when the user clicks the up spin button:

<SCRIPT LANGUAGE = VBScript>
  -->       Sub SButton1_SpinUp
  -->             RichText1.Text = "Hello World!"
  -->       End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Now let's move to the last of the ActiveX controls we'll see in our overview: progress bars.


Progress Bars

A progress bar displays a color bar like this:

    -----------------------------
          |    -------------------      |
          |   |***********        |     |
          |    -------------------      |
           -----------------------------

This bar usually indicates the status of an operation; when the operation is complete, the bar fills the control:

    -----------------------------
          |    -------------------      |
          |   |*******************|     |
          |    -------------------      |
           -----------------------------

We add a progress bar, PBar1, to our Web page this way:

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
        .
        .
        .
    <!- ProgressBar>
    <PRE>
--> ProgressBar: <OBJECT CLASSID="clsid:0713e8d2-850a-101b-afc0-4210102a8da7"
                  HEIGHT=20 WIDTH=200 ID=PBar1></OBJECT>
    </PRE>
        .
        .
        .

We specify the progress bar's setting by using its Value property. In addition, we set the progress bar's Min (default = 0) and Max (default = 100) properties to determine its range. If we set the Value to Max, the color bar will fill the control. Let's set the Value property to 50, which means the progress bar will appear half filled:

<SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
   -->            PBar1.Value = 50
            End Sub

The results appear in Figure 6.4. We can tie the displayed value to the 3-D button and the 3-D checkbox this way:

Sub Cmd1_Click()
  -->             PBar1.Value = 100
            End Sub
            Sub SSCheck1_Click(OnOff)
  -->             PBar1.Value = 80
            End Sub

When the user clicks those controls, the color bar in the progress bar will change to match. We can even tie the value displayed in the progress bar to the slider position:

Sub Cmd1_Click()
                  PBar1.Value = 100
            End Sub
            Sub SSCheck1_Click(OnOff)
                  PBar1.Value = 80
            End Sub
            Sub Slider1_Change
   -->            PBar1.Value = 10 * Slider1.Value
            End Sub

Now as the user moves the slider, the progress bar will change to match it.

That completes our ActiveX control overview. We've looked at 14 ActiveX controls that we can embed in Web pages and use with VBScript. Our Web page is a success. The entire Web page appears in Listing 6.1.

Listing 6.1 ocx.htm

<HTML>
    <HEAD>
    <TITLE>OCX Control Page</TITLE>
    </HEAD>
    <BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
    <CENTER>
    <H1>OCX Control Page</H1>
    </CENTER>
    <!- SSCommand>
    <PRE>
    SSCommand:   <OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
                   HEIGHT=30 WIDTH=70 ID=Cmd1></OBJECT>
    </PRE>
    <!- SSFrame>
    <PRE>
    SSFrame:   <OBJECT CLASSID="clsid:0BA686AF-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Frame1></OBJECT>
    </PRE>
    <!- SSPanel>
    <PRE>
    SSPanel:   <OBJECT CLASSID="clsid:0BA686B9-F7D3-101A-993E-0000C0EF6F5E"
                   HEIGHT=30 WIDTH=70 ID=Panel1></OBJECT>
    </PRE>
    <!- Grid>
    <PRE>
    Grid1:  <OBJECT CLASSID="clsid:a8c3b720-0b5a-101b-b22e-00aa0037b2fc"
                  HEIGHT=100 WIDTH=150 ID=Grid1></OBJECT>
    </PRE>
    <!- Graph>
    <PRE>
    Graph1:  <OBJECT CLASSID="clsid:0842d100-1e19-101b-9aaf-1a1626551e7c"
                 HEIGHT=100 WIDTH=300 ID=Graph1></OBJECT>
    </PRE>
    <!- SSCheck>
    <PRE>
    SSCheck:  <OBJECT CLASSID="clsid:0ba686aa-f7d3-101a-993e-0000c0ef6f5e"
                 HEIGHT=40 WIDTH=200 ID=SSCheck1 Click = SSCheck1_Click>
                 </OBJECT>
    </PRE>
    <!- AVIfile>
    <PRE>
    AVIfile: [Free Floating]   <OBJECT
                 CLASSID="clsid:00022602-0000-0000-C000-000000000046"
                 HEIGHT=50 WIDTH=50 ID=avi1></OBJECT>
    </PRE>

<!- MMControl>

<PRE>
    MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
                HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
    </PRE>
    <CENTER>
    <INPUT TYPE = BUTTON NAME = "MMButton" VALUE = "Activate MM Control">
    </CENTER>
    <!- Spinbutton>
    <PRE>
    Spinbutton:  <OBJECT CLASSID="clsid:B16553C0-06DB-101B-85B2-0000C009BE81"
                HEIGHT=50 WIDTH=50 ID=SButton1></OBJECT>
    </PRE>
    <!- 3D Option Button>
    <PRE>
    3D Option:  <OBJECT CLASSID="clsid:0BA686BE-F7D3-101A-993E-0000C0EF6F5E"
                HEIGHT=50 WIDTH=50 ID=Option1></OBJECT>
    </PRE>
    <!- Slider>
    <PRE>
    Slider:   <OBJECT CLASSID="clsid:373FF7F0-EB8B-11CD-8820-08002B2F4F5A"
              HEIGHT=50 WIDTH=250 Change = Slider1_Change ID=Slider1></OBJECT>
    </PRE>
    <!- Keystate>
    <PRE>
    KeyState:   <OBJECT CLASSID="clsid:B9D22270-0C24-101B-AEBD-04021C009402"
                HEIGHT=50 WIDTH=50 ID=Keystate1></OBJECT>
    </PRE>
    <!- RichText>
    <PRE>
    RichText:    <OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
                 HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
    </PRE>
    <!- ProgressBar>
    <PRE>
    ProgressBar: <OBJECT CLASSID="clsid:0713e8d2-850a-101b-afc0-4210102a8da7"
                  HEIGHT=20 WIDTH=200 ID=PBar1></OBJECT>
    </PRE>
    <SCRIPT LANGUAGE = VBScript>
            Sub Page_Initialize
                  Cmd1.Caption = "Hello"
                  Frame1.Caption = "Frame"
                  Panel1.Caption = "Panel"
                  Grid1.Rows = 3
                  Grid1.Cols = 3
                  Grid1.Row = 1
                  Grid1.Col = 1
                  Grid1.Text = "5"
                  Graph1.GraphData = 2
                  Graph1.GraphData = 4
                  Graph1.GraphData = 5
                  Graph1.GraphData = 3
                  PBar1.Value = 50
            End Sub
            Sub MMButton_OnClick
                  MMControl1.DeviceType = "CDAudio"
                  MMControl1.Command = "Open"
            End Sub
            Sub Cmd1_Click()
                  PBar1.Value = 100
            End Sub
            Sub SSCheck1_Click(OnOff)
                  PBar1.Value = 80
            End Sub
            Sub Slider1_Change
                  PBar1.Value = 10 * Slider1.Value
            End Sub
            Sub SButton1_SpinUp
                  RichText1.Text = "Hello World!"
            End Sub
    </SCRIPT>
    </BODY>
    </HTML>

Now that we've reviewed ActiveX controls, let's take a look at some of the more powerful controls in detail, beginning with the grid control.


The Grid Control in Detail

The grid control presents us with a spreadsheet-like display, and we can place text and graphics in each cell. Let's assume that we have a grid control in our Web page like this:

    -----------------------------------------------
          |           _____________________________       |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
          |          |_____|_____|_____|_____|_____|      |
           -----------------------------------------------

To become more familiar with this control, let's make this example a spreadsheet, allowing the user to enter data into the grid by typing it. However, the grid control cannot respond directly to key press events, so we will enter the data in a separate textbox, making the example look something like this:

    -----------------------------------------------
          |         ---------------   ------------        |
          |        |               | | Enter Data |       |
          |         ---------------   ------------        |
          |          _____________________________        |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
          |         |_____|_____|_____|_____|_____|       |
           -----------------------------------------------

The user clicks the grid, highlighting a cell, and then types the data for that cell into the textbox. When the data is ready to be transferred to the highlighted cell, the user clicks the control button we have called Enter Data.

This is a common way of entering data into a grid control. Another, more complicated way is to place code in a Grid_MouseDown() event handler so that when the user clicks a certain cell, you position a borderless textbox exactly over that cell and make it visible. The user gets the impression that it's possible to enter text directly into the grid. After the user presses, say, the Enter key, you can move the text from the textbox into the underlying grid cell and hide the textbox once again.

In addition to allowing the user to enter data into the grid, let's label the first row and the first column of cells just as they might appear in a spreadsheet:

    -----------------------------------------------
          |         ---------------   ------------        |
          |        |               | | Enter Data |       |
          |         ---------------   ------------        |
          |          _____________________________        |
          |         |_____|__A__|__B__|__C__|__D__|       |
          |         |__1__|_____|_____|_____|_____|       |
          |         |__2__|_____|_____|_____|_____|       |
          |         |__3__|_____|_____|_____|_____|       |
          |         |__4__|_____|_____|_____|_____|       |
          |         |__5__|_____|_____|_____|_____|       |
          |         |__6__|_____|_____|_____|_____|       |
          |         |__7__|_____|_____|_____|_____|       |
          |         |__8__|_____|_____|_____|_____|       |
          |         |__9__|_____|_____|_____|_____|       |
           -----------------------------------------------

Create a new Web page named grid.htm. Next, add a rich text box, RichText1, to the page:

<HTML>
<HEAD>
<TITLE>The Grid Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>The Grid Page</H1>
</CENTER>
<CENTER>
<!- RichText - richtx32.ocx>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"   <--
        HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
        .
        .
        .

Now we add a 3-D button, with the ID Cmd1, to the page; we'll give this button the caption Enter Data in a minute:

<HTML>
<HEAD>
<TITLE>The Grid Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>The Grid Page</H1>
</CENTER>
<CENTER>
<!- RichText - richtx32.ocx>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
        HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
<!- SSCommand - threed32.ocx>
<OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"   <--
        HEIGHT=30 WIDTH=150 ID=Cmd1></OBJECT>
</CENTER>
     .
     .
     .

Finally, we add the grid control:

<HTML>
<HEAD>
<TITLE>The Grid Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>The Grid Page</H1>
</CENTER>
<CENTER>
<!- RichText - richtx32.ocx>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
        HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
<!- SSCommand - threed32.ocx>
<OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
        HEIGHT=30 WIDTH=150 ID=Cmd1></OBJECT>
</CENTER>
<!- Grid - grid32.ocx>
<PRE>
<OBJECT CLASSID="clsid:a8c3b720-0b5a-101b-b22e-00aa0037b2fc"   <--
        HEIGHT=200 WIDTH=500 ID=Grid1></OBJECT>
</PRE>
   .
   .
   .

Now our Web page is almost finished:

    -----------------------------------------------
          |         ---------------   ------------        |
          |        |               | | Enter Data |       |
          |         ---------------   ------------        |
          |          _____________________________        |
          |         |_____|__A__|__B__|__C__|__D__|       |
          |         |__1__|_____|_____|_____|_____|       |
          |         |__2__|_____|_____|_____|_____|       |
          |         |__3__|_____|_____|_____|_____|       |
          |         |__4__|_____|_____|_____|_____|       |
          |         |__5__|_____|_____|_____|_____|       |
          |         |__6__|_____|_____|_____|_____|       |
          |         |__7__|_____|_____|_____|_____|       |
          |         |__8__|_____|_____|_____|_____|       |
          |         |__9__|_____|_____|_____|_____|       |
           -----------------------------------------------

The next step is to prepare the grid by labeling the first row with the letters A, B, C, and so on, and the first column with the numbers 1, 2, 3, and so forth. By default, the first row and column of a grid control appear gray and are usually used to label the cells; such label rows and columns, called fixed rows and columns, are controlled by the FixedRows and FixedCols properties. If you do not want to label the cells, simply set FixedRows and FixedCols to 0. To label the cells, create the Page_Initialize subroutine:

Sub Page_Initialize
        .
        .
        .
    End Sub

As we saw before, each grid control has both a Row and a Col property, and they hold the (Row, Col) location of the currently active cell. (Don't confuse the Row and Col properties with the Rows and Cols properties, which hold the total number of rows and columns in the grid.) We start by giving our grid control 21 rows and 11 columns with the Rows and Cols properties:

Sub Page_Initialize
 -->          Grid1.Rows = 21
 -->          Grid1.Cols = 11
                .
                .
                .

In addition, we set the caption of the button (Cmd1) to Enter Data this way:

Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
 -->    Cmd1.Caption = "Enter Data"
                .
                .
                .

When we transfer data to the grid control, the data will appear in the cell at the location (Row, Col). To label the first column, column 0, with numbers, we start by setting Grid1's Col property to 0 with this new code:

Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
 -->    Grid1.Col = 0
                .
                .
                .

Next, we loop over all rows, setting the Row property accordingly (note that we start at Row 1):

--> Dim loop_index
    Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
        Grid1.Col = 0
-->     For loop_index = 1 To 20
-->         Grid1.Row = loop_index
                .
                .
                .
-->     Next
        .
        .
        .
    End Sub

Now that we've set the Row and Col properties of the grid, we are free to enter text in the grid's Text property. That text will appear in the cell at the current (Row, Col) location (which the user has set by clicking the grid control with the mouse). We place ascending numbers in the first column like this:

Dim loop_index
    Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
        Grid1.Col = 0
        For loop_index = 1 To 20
            Grid1.Row = loop_index
  -->       Grid1.Text = CStr(loop_index)
        Next
          .
          .
          .
    End Sub

Now we label the first row with letters. We set Row to 0 and loop over the columns:

Dim loop_index
    Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
        Grid1.Col = 0
        For loop_index = 1 To 20
            Grid1.Row = loop_index
            Grid1.Text = CStr(loop_index)
        Next
  -->       Grid1.Row = 0
  -->   For loop_index = 1 To 10
  -->       Grid1.Col = loop_index
                  .
                  .
                  .
  -->   Next
    End Sub

All we have to do is to convert the loop index into a character starting at "A" and place it into the grid's Text property:

Dim loop_index
    Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
        Grid1.Col = 0
        For loop_index = 1 To 20
            Grid1.Row = loop_index
            Grid1.Text = CStr(loop_index)
        Next
            Grid1.Row = 0
        For loop_index = 1 To 10
            Grid1.Col = loop_index
  -->       Grid1.Text = Chr(Asc("A") + loop_index - 1)
        Next
    End Sub

Now our grid control's cells are labeled.

The next step is to allow the user to enter data. When the user clicks the grid control, the Row and Col properties are automatically set to the location of the clicked cell. After that, the user enters the data in the textbox RichText1 and clicks the command button marked Enter Data. We need only transfer the data from the textbox to the currently active cell and then clear the textbox. Add the subroutine Cmd1_Click() to intercept button clicks, and place this code in it:

Sub Cmd1_Click()
 -->    Grid1.Text = RichText1.Text
 -->    RichText1.Text = ""
    End Sub

And that's it—our Grid example is complete. Run it now, as shown in Figure 6.5, and enter data by clicking the correct cell with the mouse, typing into the textbox, and clicking the Enter Data button. The grid page is a success.

Figure 6.5 Our grid control allows the user to enter and display data.

Note that if you were creating a spreadsheet and the cells were tied together—such as a running sum being kept at the bottom of every column—you could perform any required operations (such as adding the column) when the new data was entered into the cell.

The code for this Web page, grid.htm, appears in Listing 6.2.

Listing 6.2 grid.htm

<HTML>
<HEAD>
<TITLE>The Grid Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>The Grid Page</H1>
</CENTER>
<CENTER>
<!- RichText - richtx32.ocx>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
        HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
<!- SSCommand - threed32.ocx>
<OBJECT CLASSID="clsid:0ba686b4-f7d3-101a-993e-0000c0ef6f5e"
        HEIGHT=30 WIDTH=150 ID=Cmd1></OBJECT>
</CENTER>
<!- Grid - grid32.ocx>
<PRE>
<OBJECT CLASSID="clsid:a8c3b720-0b5a-101b-b22e-00aa0037b2fc"
        HEIGHT=200 WIDTH=500 ID=Grid1></OBJECT>
</PRE>
<SCRIPT LANGUAGE = VBScript>
    Dim loop_index
    Sub Page_Initialize
        Grid1.Rows = 21
        Grid1.Cols = 11
        Cmd1.Caption = "Enter Data"
        Grid1.Col = 0
        For loop_index = 1 To 20
            Grid1.Row = loop_index
            Grid1.Text = CStr(loop_index)
        Next
            Grid1.Row = 0
        For loop_index = 1 To 10
            Grid1.Col = loop_index
            Grid1.Text = Chr(Asc("A") + loop_index - 1)
        Next
    End Sub
    Sub Cmd1_Click()
        Grid1.Text = RichText1.Text
        RichText1.Text = ""
    End Sub
</SCRIPT>
</BODY>
</HTML>

That's it for our coverage of the grid control. The next custom control we'll cover is also an extremely popular one: the graph control.


The Graph Control

As we've seen, the graph control allows us to display numerical data in a graph:

    _____________________________
          |                             |
          |                             |
          |            _____            |
          |           |     |           |
          |      _____|     |_____      |
          |_____|     |     |     |     |
          |     |     |     |     |_____|
          |     |     |     |     |     |
          |     |     |     |     |     |
          |_____|_____|_____|_____|_____|

To see what's possible with the graph control, let's begin by adding a graph control, Graph1, to a new Web page:

<HTML>
<HEAD>
<TITLE>Graph Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Graph Control Page</H1>
</CENTER>
<!- Graph>
<PRE>
Graph1:  <OBJECT CLASSID="clsid:0842d100-1e19-101b-9aaf-1a1626551e7c"  <--
             HEIGHT=300 WIDTH=300 ID=Graph1></OBJECT>
</PRE>
  .
  .
  .

Let's say that we have this data to plot:

Point   Value
1      50
2      60
3      30
4      40
5      60
6      70

We enter this data into the graph control by filling the GraphData property:

Sub Page_Initialize
    -->         Graph1.GraphData = 50
    -->         Graph1.GraphData = 60
    -->         Graph1.GraphData = 30
    -->         Graph1.GraphData = 40
    -->         Graph1.GraphData = 60
    -->         Graph1.GraphData = 70
                   .
                   .
                   .
        End Sub

Next, we select a graph type. (The default is the histogram type we saw in Figure 6.2.)


The GraphType Property

The available selections for the graph type, set with the GraphType property, appear in Table 6.1.

Table 6.1 The graph control's GraphType property.

GraphType | Type of Graph
0 | None
1 | 2-D pie
2 | 3-D pie
3 | (Default) 2-D bar
4 | 3-D bar
5 | Gantt
6 | Line
7 | Log/Lin
8 | Area
9 | Scatter
10 | Polar
11 | HLC

We'll start with a simple line chart, GraphType = 6. Add this code to Page_Initialize:

Sub Page_Initialize
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
   -->          Graph1.GraphType = 6    'Change for diff graphs
                        .
                        .
                        .
        End Sub

In addition, let's label the graph. To use GraphTitle to add the title "Fish Consumption," we add this code:

Sub Page_Initialize
  -->           Graph1.GraphTitle = "Fish Consumption"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
                Graph1.GraphType = 6    'Change for diff graphs
                        .
                        .
                        .
        End Sub

Let's also add more explanation at the bottom. We include a label reading "Measured in Tons" by placing that string in the BottomTitle property. (In addition to BottomTitle, LeftTitle is available for labeling the y axis.)

Sub Page_Initialize
                Graph1.GraphTitle = "Fish Consumption"
   -->          Graph1.BottomTitle = "Measured in Tons"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
                Graph1.GraphType = 6    'Change for diff graphs
                        .
                        .
                        .
        End Sub

Now examine the Web page in the Internet Explorer, producing the graph shown in Figure 6.6. As you can see, our data appears in the graph as a line graph, and our titles appear at the top and bottom of the graph. So far, then, the graph Web page is a success.

Figure 6.6 Our first graph displays its data as a line graph.

Let's try another type of graph: a 3-D pie chart. We change the GraphType property from 6 (line chart) to 2 (3-D pie chart):

Sub Page_Initialize

Graph1.GraphTitle = "Fish Consumption"
                Graph1.BottomTitle = "Measured in Tons"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
   -->          Graph1.GraphType = 2    'Change for diff graphs
                        .
                        .
                        .
        End Sub

We can do more with pie charts. For example, we can "explode" one of the pie slices to draw attention to it—draw it slightly separated from the rest of the pie. Let's explode the first data point by setting its ExtraData property to 1. Each data point has an ExtraData property, which means different things for different GraphType settings; when we're drawing a 3-D pie chart, setting a point's ExtraData property to 1 explodes that pie piece.


The ThisPoint Property

To work with an individual data point, we use the ThisPoint property. This property selects the current point in the graph control; to select the first data point, we set Graph1.ThisPoint to 1:

Sub Page_Initialize
                Graph1.GraphTitle = "Fish Consumption"
                Graph1.BottomTitle = "Measured in Tons"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
                Graph1.GraphType = 2    'Change for diff graphs
    -->         Graph1.ThisPoint = 1
                        .
                        .
                        .
        End Sub

Now that we've selected the data point to explode, we set its ExtraData property to 1:

Sub Page_Initialize
                Graph1.GraphTitle = "Fish Consumption"
                Graph1.BottomTitle = "Measured in Tons"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
                Graph1.GraphType = 2    'Change for diff graphs
                Graph1.ThisPoint = 1
    -->         Graph1.ExtraData = 1
        End Sub

The new graph appears in Figure 6.7.

Figure 6.7 Our graph control displays our data as a 3-D pie chart with one exploded slice.

Of course, not all data will appear neatly at x positions 1, 2, 3, 4, and so on. Usually, we must set the x data just as we've set the y data. To set the x position of a point, we first select that point with the ThisPoint property and then set its XPosData property to the correct x value. Using this property (which cycles just as the GraphData property cycles), we can specify the x locations of our data points as well as the y values.

That's it for the graph control. As you can see, using it makes graphing numerical data very easy. The file graph.htm appears in Listing 6.3.

Listing 6.3 graph.htm

<HTML>
<HEAD>
<TITLE>Graph Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Graph Control Page</H1>
</CENTER>
<!- Graph>
<PRE>
Graph1:  <OBJECT CLASSID="clsid:0842d100-1e19-101b-9aaf-1a1626551e7c"
             HEIGHT=300 WIDTH=300 ID=Graph1></OBJECT>
</PRE>
<SCRIPT LANGUAGE = VBScript>
        Sub Page_Initialize
                Graph1.GraphTitle = "Fish Consumption"
                Graph1.BottomTitle = "Measured in Tons"
                Graph1.GraphData = 50
                Graph1.GraphData = 60
                Graph1.GraphData = 30
                Graph1.GraphData = 40
                Graph1.GraphData = 60
                Graph1.GraphData = 70
                Graph1.GraphType = 2    'Change for diff graphs
                Graph1.ThisPoint = 1
                Graph1.ExtraData = 1
        End Sub
</SCRIPT>
</BODY>
</HTML>

The graph control is useful and easy to use. The same is true for the next custom control we turn to: the multimedia control.


The Multimedia Control

Multimedia is certainly a hot topic, because it lets us create an integrated environment of video, sound, still pictures, and text. VBScript gives us access to multimedia through custom controls such as the media control interface (MCI) multimedia control. Let's take advantage of this and create a new Web page, multimed.htm. As we've seen, the multimedia control consists of a set of buttons much like you would see on a CD player:

    --------------------------------------------------------------
         |      |      |      |      |      |      |   _  |   _  |  __  |
         | |/|  | |\|  |  |\  |  ||  |  /|| |  ||\ |  | | |  | | |  /\  |
         | |\|  | |/|  |  |/  |  ||  |  \|| |  ||/ |  |_| |  |_| |  ==  |
         |      |      |      |      |      |      |      |      |      |
          --------------------------------------------------------------

With these buttons, we can control the operation of multimedia devices. The different device types that the multimedia control can work with are shown in Table 6.2.

Table 6.2 MMControl device types.

Name | Type of Device
Animation | Plays animation
CDAudio | Plays CD audio
AVIVideo | Plays .AVI video files
Other | Device is not defined
Overlay | Plays analog video
Sequencer | The musical instrument digital interface (MIDI)
VideoDisc | Plays video discs
WaveAudio | Plays .WAV sound files

Let's see how to use this control with CD audio. We begin by adding a new multimedia control to a Web page, multimed.htm, and giving that new control the ID MMControl1:

<HTML>
<HEAD>
<TITLE>Multimedia Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript>
<CENTER>
<H1>Multimedia Control Page</H1>
</CENTER>
<!- MMControl>
<PRE>
MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3" <--
            HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
</PRE>
   .
   .
   .

As we did earlier, we add a button, MMButton, to start the multimedia control:

<HTML>
<HEAD>
<TITLE>Multimedia Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript>
<CENTER>
<H1>Multimedia Control Page</H1>
</CENTER>
<!- MMControl>
<PRE>
MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
            HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
</PRE>
<CENTER>
<INPUT TYPE = BUTTON NAME = "MMButton" VALUE = "Activate MM Control"> <--
</CENTER>
<BR>
    .
    .
    .

When the user clicks this button, we set the multimedia control to play CD audio and start it as before in MMButton_OnClick:

Sub MMButton_OnClick
              MMControl1.DeviceType = "CDAudio"
              MMControl1.Command = "Open"
        End Sub

We can do more with the multimedia control by using its StatusUpdate event, which is called when it switches modes (e.g., from Stop to Play). The mode of the multimedia control is stored in its Mode property, and the available settings appear in Table 6.3.

Table 6.3 Multimedia modes.

Mode Value | Means
524 | Device not open
525 | Device stop
526 | Device play
527 | Device record
528 | Device seek
529 | Device pause
530 | Device ready

If we add a textbox to our Web page, we can indicate the status of the multimedia control: whether it is playing or paused, or whatever condition it is in. We add a rich text box, RichText1, for this purpose:

<HTML>
<HEAD>
<TITLE>Multimedia Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript>
<CENTER>
<H1>Multimedia Control Page</H1>
</CENTER>
<!- MMControl>
<PRE>
MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
            HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
</PRE>
<CENTER>
<INPUT TYPE = BUTTON NAME = "MMButton" VALUE = "Activate MM Control">
</CENTER>
<BR>
<!- RichText>
<CENTER>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"   <--
             HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
</CENTER>
    .
    .
    .

We will display the status of MMControl1 with the MMControl1_StatusUpdate() subroutine; the control calls this subroutine when its status changes:

Sub MMControl1_StatusUpdate()
             .
             .
             .
        End Sub

In this subroutine, we place the correct text into the rich text box to match the control's status:

Sub MMControl1_StatusUpdate()
  -->        If MMControl1.Mode = 525 Then RichText1.Text = "Stopped."
  -->        If MMControl1.Mode = 526 Then RichText1.Text = "Playing."
  -->        If MMControl1.Mode = 527 Then RichText1.Text = "Recording."
  -->        If MMControl1.Mode = 528 Then RichText1.Text = "Seeking."
  -->        If MMControl1.Mode = 529 Then RichText1.Text = "Paused."
  -->        If MMControl1.Mode = 530 Then RichText1.Text = "Ready."
        End Sub

And that's it—now our multimedia program is set. Run it as shown in Figure 6.8. If you place a music CD in your CD-ROM drive, you can use the multimedia control to control it and play music. (Even if you don't have a sound card, CD-ROM drives often have earphone jacks for this purpose.) In addition, the status of each multimedia device is shown. Our new Web page is a success. The multimed.htm file appears in Listing 6.4.

Figure 6.8 Our multimedia control plays CD music.

Listing 6.4 multimed.html

<HTML>
<HEAD>
<TITLE>Multimedia Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript>
<CENTER>
<H1>Multimedia Control Page</H1>
</CENTER>
<!- MMControl>
<PRE>
MMControl:  <OBJECT CLASSID="clsid:C1A8AF25-1257-101B-8FB0-0020AF039CA3"
            HEIGHT=50 WIDTH=400 ID=MMControl1></OBJECT>
</PRE>
<CENTER>
<INPUT TYPE = BUTTON NAME = "MMButton" VALUE = "Activate MM Control">
</CENTER>
<BR>
<!- RichText>
<CENTER>
<OBJECT CLASSID="clsid:3b7c8860-d78f-101b-b9b5-04021c009402"
             HEIGHT=40 WIDTH=300 ID=RichText1></OBJECT>
</CENTER>
<SCRIPT LANGUAGE = VBScript>
        Sub MMButton_OnClick
              MMControl1.DeviceType = "CDAudio"
              MMControl1.Command = "Open"
        End Sub
        Sub MMControl1_StatusUpdate()
             If MMControl1.Mode = 525 Then RichText1.Text = "Stopped."
             If MMControl1.Mode = 526 Then RichText1.Text = "Playing."
             If MMControl1.Mode = 527 Then RichText1.Text = "Recording."
             If MMControl1.Mode = 528 Then RichText1.Text = "Seeking."
             If MMControl1.Mode = 529 Then RichText1.Text = "Paused."
             If MMControl1.Mode = 530 Then RichText1.Text = "Ready."
        End Sub
</SCRIPT>
</BODY>
</HTML>

That's it for our ActiveX control chapter. In this chapter, we've explored many controls: 3-D buttons, 3-D frames and panels, grid controls, graph controls, AVI file controls, multimedia controls, spin buttons, progress bars, and more. As you can see, there is a great deal of power here—all that remains is to put it to work.


Building Your Own ActiveX Controls for Use with VBScript

In this chapter, you'll learn how to create OLE custom controls, also known as OCX controls or ActiveX controls. We'll use Microsoft Visual C++ 4.0, the most popular tool for creating ActiveX controls. I'll assume that you've used this package and have a fair understanding of how it works (if not, you can still follow along, but it will be harder). With Visual C++, you can design and implement your own customized controls using Visual C++'s ControlWizard. For example, you can create a control that displays the date and then install this control in VBScript Web pages as easily as other controls such as buttons or listboxes.

As you can imagine, this is a powerful technique. The potential (and after-market) for new controls is endless, and the power of VBScript is extended to amazing proportions with the ability to embed and access OLE controls.


LISTER.OCX: A Listbox-Based OLE Custom Control

Our first example custom OLE control is a customized listbox control. In all ways, this control acts as a listbox except when the user clicks it. After being clicked, our customized listbox displays the text "Hello world.":

Web Page
         -----------------------
        |                       |
        |        ------------   |
        |       |Hello, world|  |
        |       |            |  |
        |       |            |  |
        |       |            |  |
        |        ------------   |
        |                       |
         -----------------------

We will place the support for this custom control in a file named LISTER.OCX, and we'll be able to add this control to our VBScript pages in the same way we add any other ActiveX control.

Visual C++ provides an excellent tool for testing the ActiveX controls we create. In the Visual C++ Tools menu, you will find the menu item OLE Control Test Container. Clicking that tool opens the Visual C++ test container, and the Edit menu of that container program has an item named Insert OLE Control:

    -----------------------
         | Container             |
         |-----------------------|
         | File Edit             |
         |-----|---------------------
         |     |Insert Ole Control...|
         |     |                     |
         |      ---------------------
         |                       |
         |                       |
         |                       |
          -----------------------

When you select that item, the Insert OLE Control box opens, displaying all the custom controls available, including our LISTER control:

    -----------------------
         | Container             |
         |-----------------------|
         |         -------------------------------
         |        |Insert OLE Control   --------  |
         |        |                    |   OK   | |
         |        |Object Type          --------  |
         |        | ---------------     --------  |
         |        ||Lister Control |   | Cancel | |
          --------||               |    --------  |
                  | ---------------               |
                   -------------------------------

Here, you see the entry Lister Control in the Object Type box. (We assume that we have already registered our ActiveX control with Windows, and now Windows is informing us that it is available.) When we select Lister Control in the Object Type box and then click OK, a new LISTER control appears in the test container. (It is just an empty listbox at this point):

    -----------------------
         | Container             |
         |-----------------------|
         |        ------------   |
         |       |            |  |
         |       |            |  |
         |       |            |  |
         |       |            |  |
         |        ------------   |
         |                       |
          -----------------------

When a user clicks this listbox, it displays the text "Hello, world.":

    -----------------------
         | Container             |
         |-----------------------|
         |        ------------   |
         |       |Hello, world|  |
         |       |            |  |
         |       |            |  |
         |       |            |  |
         |        ------------   |
         |                       |
          -----------------------

Now we know what we are aiming for. Let's get started.


Using ControlWizard to Create Custom Control Code

The first step in creating the LISTER.OCX is to use Visual C++'s ControlWizard to create a skeleton version of all the support files we'll need, just as AppWizard creates skeletal support files for EXE programs. To create LISTER, select the New item in Visual C++'s File menu and then select Project Workspace. Next, select the OLE ControlWizard option, as shown in Figure 9.1.

Figure 9.1 Visual C++'s ControlWizard.

We'll base the LISTER control on the standard List Box control so that we'll have all the functionality of a listbox. (But note that you need not base your custom control on any preexisting control type.) In ClassWizard's step 2 box, select LISTBOX as the type of control our control should subclass, as shown in Figure 9.2.

Figure 9.2 ControlWizard's Control Options box.

This selection indicates to ControlWizard that we want to base our new control on the listbox type. Now click OK in the Control Options box and Finish in ControlWizard to create the support files for LISTER.OCX.

At this point, ControlWizard creates the new project, including these important files:

  • LISTER.MAK: The project file

  • LISTER.CPP: OLE DLL initialization, termination, other bookkeeping

  • LISTERCTL.CPP: Main implementation file (CListerCtrl class)

  • LISTERPPG.CPP: Implementation of the CListerPropPage property page class

  • LISTER.ODL: Object Description Language source code for the type library

    Let's take a moment to examine some of the files created by ControlWizard.


Inside a ControlWizard Program

The file LISTER.CPP serves to initialize the program and connect it to Windows. In our case, the function InitInstance() in LISTER.CPP (as written by ControlWizard) simply passes control back to the base class's InitInstance() function (do not add this code):

BOOL CListerApp::InitInstance()
{
    BOOL bInit = COleControlModule::InitInstance();
    if (bInit)
    {
        // TODO: Add your own module initialization code here.
    }
    return bInit;
}

It is also important to register the new control with Windows, and the program as it stands now does that in LISTER.CPP's DllRegisterServer() function:

STDAPI DllRegisterServer(void)
{
    AFX_MANAGE_STATE(_afxModuleAddrThis);
    if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
        return ResultFromScode(SELFREG_E_TYPELIB);
    if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
        return ResultFromScode(SELFREG_E_CLASS);
    return NOERROR;
}

That's about all that happens in LISTER.CPP; not much is going on in the initialization section of our program.

The next file worth mentioning is LISTER.ODL, which describes our control to Windows. The line that gives an English name to our control looks like this:

//  Class information for CListerCtrl
[ uuid(14CF3A60-092A-101C-BAC7-040224009C02),
  helpstring("Lister Control"), control ] coclass Lister
{
    [default] dispinterface _DLister;
    [default, source] dispinterface _DListerEvents;
};

This is the uuid code that we will need to insert objects of the Lister type in our VBScript pages. (What VBScript refers to as a clsid.) In addition, this is the string, "Lister Control," that we'll see when we install LISTER.OCX in Visual C++'s test container. When the program is built, the source code in LISTER.ODL is compiled into a type library file, LISTER.TLB, which holds information that Windows can read directly. (TLB stands for type library.)

We will work mostly with the custom control's code file, which is LISTERCTL.CPP. The real action in our program takes place there. Open that file and take a look at the OnDraw() function (add no code to OnDraw()):

void CListerCtrl::OnDraw(CDC* pdc,
        const CRect& rcBounds, const CRect& rcInvalid)
{
    DoSuperclassPaint(pdc, rcBounds);
}

This function works in much the same way that OnDraw() usually does in Visual C++; here we simply draw the control. Note that we are passed a pointer to a device context (pdc) in which to draw. We also receive the bounding rectangle of the control we are supposed to draw (rcBounds) and the rectangle in our control that's been marked as invalid (rcInvalid)—that is, the part of the control that we are supposed to redraw.

If we wanted to draw our own control, we would do it here. For example, if we wanted to create a control that draws a picture of fish in an aquarium, we could draw that at this point. We can customize our control's appearance as we wish, even making it appear three-dimensional if we use the shading techniques used in Windows buttons. Because we are basing our control on listboxes, however, the program will take care of drawing it, using a call to the function DoSuperclassPaint(). We need not worry about drawing LISTER at all—the program takes care of that for us, giving it the appearance of a normal listbox.

Our goal is to display the words "Hello, world." when the user clicks the LISTER control. We do that simply by connecting a function to WM_LBUTTONDOWN as we have in other programs. As with other Visual C++ programs, we use ClassWizard to connect messages such as WM_LBUTTONDOWN to functions in LISTERCTL.CPP. Let's see how that works.


Reading Mouse Clicks in LISTER.OCX

Open ClassWizard from the Visual C++ View menu. Select the control class, CListerCtrl, in the Class Name box, and then select WM_LBUTTONDOWN in the Messages box. Connect the function OnLButtonDown() to the CListerCtrl class using the Add Function button, and open that new function:

void CListerCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    COleControl::OnLButtonDown(nFlags, point);
}

Our goal is to place a text string reading "Hello, world." in the listbox when the user clicks it. We begin by setting up the text string:

void CListerCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
--> char out_string[] = "Hello, world.";
        .
        .
        .
    COleControl::OnLButtonDown(nFlags, point);
}

Note that for a standard control, we could now set a flag and invalidate the control using Invalidate(); Lister's OnDraw() function would be called and the control would be redrawn with the string "Hello, world." in it (provided we had added code to OnDraw() to draw our custom control the way we wanted it).

Here, however, our control is built on a listbox control, so we should simply add the text string as an entry in the listbox and let it handle its own redrawing. We do that by sending a control message to the listbox; the message we want to send is LB_INSERTSTRING, and we use the Windows SendMessage() function:

void CListerCtrl::OnLButtonDown(UINT nFlags, CPoint point)

{

char out_string[] = "Hello, world.";
--> int nIndex = 0;
--> SendMessage(LB_INSERTSTRING, nIndex, (long) out_string);
    COleControl::OnLButtonDown(nFlags, point);
}

Here, we indicate that we want to add the new string as entry 0, the top entry, in the listbox. We pass to SendMessage() a pointer to the text string after first converting the pointer to a long value. That's it for the code—create LISTER.OCX by selecting Build lister.ocx in the Visual C++ Build menu.

After LISTER.OCX is created, we register it with Windows so that we can use it in other programs. To register LISTER.OCX with Windows, select Register Control in Visual C++'s Tools menu. A box appears, indicating that the control was registered successfully with Windows. To test our new control, select OLE Control Test Container in the Visual C++ Tools menu, opening the Visual C++ test container, as shown in Figure 9.3. Now select Insert OLE Control in the test container's Edit menu, as also shown.

Figure 9.3 The Visual C++ OLE control test container.

This action opens the Insert OLE Control box; select the entry Lister Control in the Object Type box and click OK. A new LISTER control is inserted in the test container, as shown in Figure 9.4. Now click the LISTER control; the control displays "Hello, world." in the listbox.

Figure 9.4 Our LISTER OLE custom control.

Now let's install Lister in a Web page. We use the control's uuid value mentioned earlier (which you can also find by using Windows' regedit.exe utility and searching for lister.ocx):

<HTML>
<HEAD>
<TITLE>Lister Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Lister Page</H1>
</CENTER>
<!- lister.ocx>
<pre>
Lister:   <OBJECT CLASSID="clsid:14CF3A60-092A-101C-BAC7-040224009C02" <--
        HEIGHT=50 WIDTH=100 ID=Cmd1></OBJECT>
</pre>
<SCRIPT LANGUAGE = VBScript>
        Sub Page_Initialize
        End Sub
</SCRIPT>
</BODY>
</HTML>

When we load the new page, the Lister control appears and functions properly, as shown in Figure 9.5.

Figure 9.5 Our LISTER control in a Web page.

Our first, simple ActiveX project, LISTER, is a success. As you can see, setting up a custom ActiveX control is not difficult in Visual C++, because Visual C++ handles most of the details. The code for the LISTER project can be found in these listings: Listing 9.1 (LISTER.H and LISTER.CPP), Listing 9.2 (LISTERCTL.H and LISTERCTL.CPP), and Listing 9.3 (LISTER.ODL).

Listing 9.1 LISTER.H and LISTER.CPP

// lister.h : main header file for LISTER.DLL
#if !defined( __AFXCTL_H__ )
        #error include ‘afxctl.h' before including this file

#endif

#include "resource.h"       // main symbols
/////////////////////////////////////////////////////////////////////////////
// CListerApp : See lister.cpp for implementation.
class CListerApp : public COleControlModule
{
public:
        BOOL InitInstance();
        int ExitInstance();
};
extern const GUID CDECL _tlid;
extern const WORD _wVerMajor;
extern const WORD _wVerMinor;
// lister.cpp : Implementation of CListerApp and DLL registration.
#include "stdafx.h"
#include "lister.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CListerApp NEAR theApp;
const GUID CDECL BASED_CODE _tlid =
   { 0x1dd85028, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66,
        0x8f, 0xc, 0xf4, 0x47 } };
const WORD _wVerMajor = 1;
const WORD _wVerMinor = 0;
////////////////////////////////////////////////////////////////////////////
// CListerApp::InitInstance - DLL initialization
BOOL CListerApp::InitInstance()
{
        BOOL bInit = COleControlModule::InitInstance();
        if (bInit)
        {
                // TODO: Add your own module initialization code here.
        }
        return bInit;
}
////////////////////////////////////////////////////////////////////////////
// CListerApp::ExitInstance - DLL termination
int CListerApp::ExitInstance()
{
        // TODO: Add your own module termination code here.
        return COleControlModule::ExitInstance();
}

/////////////////////////////////////////////////////////////////////////////

// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
        AFX_MANAGE_STATE(_afxModuleAddrThis);
        if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
                return ResultFromScode(SELFREG_E_TYPELIB);
        if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
                return ResultFromScode(SELFREG_E_CLASS);
        return NOERROR;
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
        AFX_MANAGE_STATE(_afxModuleAddrThis);
        if (!AfxOleUnregisterTypeLib(_tlid))
                return ResultFromScode(SELFREG_E_TYPELIB);
        if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
                return ResultFromScode(SELFREG_E_CLASS);
        return NOERROR;
}

Listing 9.2 LISTERCTL.H and LISTERCTL.CPP

// ListerCtl.h : Declaration of the CListerCtrl OLE control class.
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl : See ListerCtl.cpp for implementation.
class CListerCtrl : public COleControl
{
        DECLARE_DYNCREATE(CListerCtrl)
// Constructor
public:
        CListerCtrl();
// Overrides
        // Drawing function
        virtual void OnDraw(
               CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid);
        // Persistence
        virtual void DoPropExchange(CPropExchange* pPX);
        // Reset control state
        virtual void OnResetState();
// Implementation
protected:
        ~CListerCtrl();
        DECLARE_OLECREATE_EX(CListerCtrl)    // Class factory and guid
        DECLARE_OLETYPELIB(CListerCtrl)      // GetTypeInfo
        DECLARE_PROPPAGEIDS(CListerCtrl)     // Property page IDs
        DECLARE_OLECTLTYPE(CListerCtrl)      // Type name and misc status
        // Subclassed control support
        BOOL PreCreateWindow(CREATESTRUCT& cs);
        BOOL IsSubclassedControl();
        LRESULT OnOcmCommand(WPARAM wParam, LPARAM lParam);
// Message maps
        //{{AFX_MSG(CListerCtrl)
        afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
// Dispatch maps
        //{{AFX_DISPATCH(CListerCtrl)
        // NOTE - ClassWizard will add and remove member functions here.
        //    DO NOT EDIT what you see in these blocks of generated code !
        //}}AFX_DISPATCH
        DECLARE_DISPATCH_MAP()
        afx_msg void AboutBox();
// Event maps
        //{{AFX_EVENT(CListerCtrl)
        // NOTE - ClassWizard will add and remove member functions here.
        //    DO NOT EDIT what you see in these blocks of generated code !
        //}}AFX_EVENT
        DECLARE_EVENT_MAP()
// Dispatch and event IDs
public:
        enum {
        //{{AFX_DISP_ID(CListerCtrl)
        // NOTE: ClassWizard will add and remove enumeration elements here.
        //    DO NOT EDIT what you see in these blocks of generated code !
        //}}AFX_DISP_ID
        };
};
// ListerCtl.cpp : Implementation of the CListerCtrl OLE control class.
#include "stdafx.h"
#include "lister.h"
#include "ListerCtl.h"
#include "ListerPpg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CListerCtrl, COleControl)
/////////////////////////////////////////////////////////////////////////////
// Message map
BEGIN_MESSAGE_MAP(CListerCtrl, COleControl)

//{{AFX_MSG_MAP(CListerCtrl)

ON_WM_LBUTTONDOWN()
        //}}AFX_MSG_MAP
        ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
        ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Dispatch map
BEGIN_DISPATCH_MAP(CListerCtrl, COleControl)
        //{{AFX_DISPATCH_MAP(CListerCtrl)
        // NOTE - ClassWizard will add and remove dispatch map entries
        //    DO NOT EDIT what you see in these blocks of generated code !
        //}}AFX_DISPATCH_MAP
        DISP_FUNCTION_ID(CListerCtrl, "AboutBox", DISPID_ABOUTBOX,
            AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()
/////////////////////////////////////////////////////////////////////////////
// Event map
BEGIN_EVENT_MAP(CListerCtrl, COleControl)
        //{{AFX_EVENT_MAP(CListerCtrl)
        // NOTE - ClassWizard will add and remove event map entries
        //    DO NOT EDIT what you see in these blocks of generated code !
        //}}AFX_EVENT_MAP
END_EVENT_MAP()
/////////////////////////////////////////////////////////////////////////////
// Property pages
// TODO: Add more property pages as needed.  Remember to increase the count!
BEGIN_PROPPAGEIDS(CListerCtrl, 1)
        PROPPAGEID(CListerPropPage::guid)
END_PROPPAGEIDS(CListerCtrl)
/////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid
IMPLEMENT_OLECREATE_EX(CListerCtrl, "LISTER.ListerCtrl.1",
        0x14cf3a60, 0x92a, 0x101c, 0xba, 0xc7, 0x4, 0x2, 0x24, 0, 0x9c, 0x2)
/////////////////////////////////////////////////////////////////////////////
// Type library ID and version
IMPLEMENT_OLETYPELIB(CListerCtrl, _tlid, _wVerMajor, _wVerMinor)
/////////////////////////////////////////////////////////////////////////////
// Interface IDs
const IID BASED_CODE IID_DLister =
  { 0x1dd85029, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66, 0x8f,
        0xc, 0xf4, 0x47 } };
const IID BASED_CODE IID_DListerEvents =
  { 0x1dd8502a, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66, 0x8f,
        0xc, 0xf4, 0x47 } };
/////////////////////////////////////////////////////////////////////////////
// Control type information
static const DWORD BASED_CODE _dwListerOleMisc =
        OLEMISC_ACTIVATEWHENVISIBLE |
        OLEMISC_SETCLIENTSITEFIRST |
        OLEMISC_INSIDEOUT |
        OLEMISC_CANTLINKINSIDE |
        OLEMISC_RECOMPOSEONRESIZE;
IMPLEMENT_OLECTLTYPE(CListerCtrl, IDS_LISTER, _dwListerOleMisc)
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::CListerCtrlFactory::UpdateRegistry -
// Adds or removes system registry entries for CListerCtrl
BOOL CListerCtrl::CListerCtrlFactory::UpdateRegistry(BOOL bRegister)
{
        if (bRegister)
                return AfxOleRegisterControlClass(
                        AfxGetInstanceHandle(),
                        m_clsid,
                        m_lpszProgID,
                        IDS_LISTER,
                        IDB_LISTER,
                        FALSE,                      //  Not insertable
                        _dwListerOleMisc,
                        _tlid,
                        _wVerMajor,
                        _wVerMinor);
        else
                return AfxOleUnregisterClass(m_clsid, m_lpszProgID);
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::CListerCtrl - Constructor
CListerCtrl::CListerCtrl()
{
        InitializeIIDs(&IID_DLister, &IID_DListerEvents);
        // TODO: Initialize your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::~CListerCtrl - Destructor
CListerCtrl::~CListerCtrl()
{
        // TODO: Cleanup your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::OnDraw - Drawing function
void CListerCtrl::OnDraw(
        CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
        DoSuperclassPaint(pdc, rcBounds);
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::DoPropExchange - Persistence support
void CListerCtrl::DoPropExchange(CPropExchange* pPX)
{
        ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
        COleControl::DoPropExchange(pPX);
        // TODO: Call PX_ functions for each persistent custom property.
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::OnResetState - Reset control to default state
void CListerCtrl::OnResetState()
{
        COleControl::OnResetState();  // Resets defaults found in DoPropExchange
        // TODO: Reset any other control state here.
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::AboutBox - Display an "About" box to the user
void CListerCtrl::AboutBox()
{
        CDialog dlgAbout(IDD_ABOUTBOX_LISTER);
        dlgAbout.DoModal();
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::PreCreateWindow - Modify parameters for CreateWindowEx
BOOL CListerCtrl::PreCreateWindow(CREATESTRUCT& cs)
{
        cs.lpszClass = _T("LISTBOX");
        return COleControl::PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::IsSubclassedControl - This is a subclassed control
BOOL CListerCtrl::IsSubclassedControl()
{
        return TRUE;
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl::OnOcmCommand - Handle command messages
LRESULT CListerCtrl::OnOcmCommand(WPARAM wParam, LPARAM lParam)
{
#ifdef _WIN32
        WORD wNotifyCode = HIWORD(wParam);
#else
        WORD wNotifyCode = HIWORD(lParam);
#endif
        // TODO: Switch on wNotifyCode here.
        return 0;
}
/////////////////////////////////////////////////////////////////////////////
// CListerCtrl message handlers
void CListerCtrl::OnLButtonDown(UINT nFlags, CPoint point)

{

char out_string[] = "Hello, world.";
         int nIndex = 0;
         SendMessage(LB_INSERTSTRING, nIndex, (long) out_string);
        COleControl::OnLButtonDown(nFlags, point);
}

Listing 9.3 LISTER.ODL

// lister.odl : type library source for OLE Control project.
// This file will be processed by the Make Type Library (mktyplib) tool to
// produce the type library (lister.tlb) that will become a resource in
// lister.ocx.
#include <olectl.h>
[ uuid(1DD85028-3ADE-11CF-B01D-92668F0CF447), version(1.0),
  helpstring("lister OLE Control module"), control ]
library LISTERLib
{
        importlib(STDOLE_TLB);
        importlib(STDTYPE_TLB);
        //  Primary dispatch interface for CListerCtrl
        [ uuid(1DD85029-3ADE-11CF-B01D-92668F0CF447),
          helpstring("Dispatch interface for Lister Control"), hidden ]
        dispinterface _DLister
        {
                properties:
                // NOTE - ClassWizard will maintain property information here.
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_PROP(CListerCtrl)
                        //}}AFX_ODL_PROP
                methods:
                // NOTE - ClassWizard will maintain method information here.
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_METHOD(CListerCtrl)
                        //}}AFX_ODL_METHOD
                        [id(DISPID_ABOUTBOX)] void AboutBox();
        };
        //  Event dispatch interface for CListerCtrl
        [ uuid(1DD8502A-3ADE-11CF-B01D-92668F0CF447),
          helpstring("Event interface for Lister Control") ]
        dispinterface _DListerEvents
        {
                properties:
                        //  Event interface has no properties
                methods:
                // NOTE - ClassWizard will maintain event information here.
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_EVENT(CListerCtrl)
                        //}}AFX_ODL_EVENT
        };
        //  Class information for CListerCtrl
        [ uuid(14CF3A60-092A-101C-BAC7-040224009C02),
          helpstring("Lister Control"), control ]
        coclass Lister
        {
                [default] dispinterface _DLister;
                [default, source] dispinterface _DListerEvents;
        };
        //{{AFX_APPEND_ODL}}
};

At this point, then, we've seen how to set up a rudimentary ActiveX control. Let's dig deeper and see how to add it to a Web page.


Control.ocx: A Button-Based ActiveX Control

Basing custom controls on existing control types is useful, because it means that the standard events, such as mouse button presses or character entry, are already supported, and we can add event-handling functions to a control simply by using Visual C++'s ClassWizard. In fact, we can draw the control ourselves even if we base it on a standard Windows control. For example, let's draw a button with a cloudy day image that changes to a sunny day when the button is pushed. Although this control is based on the Windows button type, we can make it look like and act however we want, because we'll draw it ourselves. Let's call this new project CONTROL; when we insert a control of this type in a Web page, we'll be able to control its appearance when we draw it:

Web Page
         -----------------------
        |                       |
        |        ------------   |
        |       |            |<--- we draw our button here
        |       |            |  |
        |       |            |  |
        |       |            |  |
        |        ------------   |
        |                       |
         -----------------------

Using OLE ControlWizard, give this new project the name CONTROL and create it as we did for Lister. In Step 2 of the OLE ControlWizard, select BUTTON as the type of control our project should subclass (just as we selected LISTBOX in LISTER). Click Finish to allow ControlWizard to create this project.

As before, ControlWizard creates many files, including these important ones:

CONTROL.MAK: The project file

CONTROL.CPP: OLE DLL initialization, termination, other bookkeeping

CONTROLCTL.CPP: Main implementation file (CControlCtrl class)

CONTROLPPG.CPP: Implementation of the CControlPropPage property page class

CONTROL.ODL: Object Description Language source code for the type library


Creating Bitmaps for Our Control

We want our control to display a cloudy day before it is pressed (when the button is up), and a sunny day when it is pressed (when the button is down). To do that, we'll create the customized button bitmaps IDB_UPBITMAP (cloudy day) and IDB_DOWNBITMAP (sunny day). We select the Visual C++ Insert|Resource menu item and select Bitmap in the Insert Resource box that appears. This action opens the bitmap editor, as shown in Figure 9.6.

Figure 9.6 We design a custom bitmap.

This new bitmap has the default ID IDB_BITMAP1; change that to IDB_UPBITMAP using the bitmap's property page. Open that page by selecting Properties in the Visual C++ Edit menu. This opens the property page for the bitmap, and you can change the bitmap's ID directly. Next, draw a representation of a cloudy day in the IDB_UPBITMAP bitmap, as shown in Figure 9.6. Similarly, create a new bitmap with the ID IDB_DOWNBITMAP and draw a sunny day in it.

The customized images for CONTROL are now ready. Using these bitmaps, we'll see how to change the control's appearance. We will also have access to all the events a button normally has, such as click events, in ClassWizard. We'll also see how to add customized events, properties, and methods to control.ocx and how to install our new control in a Visual C++ program.

As before, the control support file, CONTROLCTL.CPP, will be the most important. Here is how our control is created, based on the Windows button control (from CONTROLCTL.CPP). Note that the program derives this control from the Windows button type.

BOOL CControlCtrl::PreCreateWindow(CREATESTRUCT& cs)
{
    cs.lpszClass = _T("BUTTON");
    return COleControl::PreCreateWindow(cs);
}

We want to draw the button, so we will modify this code a little. PreCreateWindow() is called when our control is first created, and we are passed a reference to a creation structure (a structure of type CREATESTRUCT). One of the members of this creation structure is the style member, which holds the style of the control we are creating. We OR the button style with two new button styles: BS_PUSHBUTTON and BS_OWNERDRAW.

BOOL CControlCtrl::PreCreateWindow(CREATESTRUCT& cs)
{
    cs.lpszClass = _T("BUTTON");
--> cs.style |= BS_PUSHBUTTON | BS_OWNERDRAW;
    return COleControl::PreCreateWindow(cs);
}

Using BS_OWNERDRAW means that now we are responsible for drawing the button. If we wanted to draw an unchanging control, we could place code in the OnDraw() function, and whatever we drew there would appear in our OLE control. However, we want to do more than that. We want to draw the button in both a pressed and an unpressed state as the user clicks it. This means that we will have to intercept the message that tells the button control when to show itself as pressed and when as released—which OnDraw() alone will not do.

To draw an active button, we intercept the OCM_DRAWITEM message. The prefix OCM stands for OLE control message, which OLE custom controls receive (in addition to Windows messages starting with the WM_ prefix). When we receive OCM_DRAWITEM, we will get information such as whether the button is supposed to be up or down. This information will enable us to draw the button with the correct bitmap (sunny day or cloudy day).

To attach a function to the OCM_DRAWITEM message, we edit the message map in CONTROLCTL.CPP directly. (ClassWizard will not work with OCM messages.) Find the message map in CONTROLCTL.CPP:

// ControlCtl.cpp : Implementation of the CControlCtrl OLE control class.
#include "stdafx.h"
#include "control.h"

.

.
        .
BEGIN_MESSAGE_MAP(CControlCtrl, COleControl)
    //{{AFX_MSG_MAP(CControlCtrl)
    ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
        .
        .
        .

We intercept the OCM_DRAWITEM message by connecting it to the function OnOcmDrawItem() (add this line of code):

// ControlCtl.cpp : Implementation of the CControlCtrl OLE control class.
#include "stdafx.h"
#include "control.h"
        .
        .
        .
BEGIN_MESSAGE_MAP(CControlCtrl, COleControl)
    //{{AFX_MSG_MAP(CControlCtrl)
    ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
--> ON_MESSAGE(OCM_DRAWITEM, OnOcmDrawItem)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
        .
        .
        .

In addition, we add the prototype of OnOcmDrawItem() to CONTROLCTL.H (note that this function returns an argument of type LRESULT, as we'll see soon):

// ControlCtl.h : Declaration of the CControlCtrl OLE control class.
/////////////////////////////////////////////////////////////////////////////
        .
        .
        .
    DECLARE_OLECREATE_EX(CControlCtrl)    // Class factory and guid
    DECLARE_OLETYPELIB(CControlCtrl)      // GetTypeInfo
    DECLARE_PROPPAGEIDS(CControlCtrl)     // Property page IDs
    DECLARE_OLECTLTYPE(CControlCtrl)        // Type name and misc status
    // Subclassed control support
    BOOL PreCreateWindow(CREATESTRUCT& cs);
    WNDPROC* GetSuperWndProcAddr(void);
    LRESULT OnOcmCommand(WPARAM wParam, LPARAM lParam);
--> LRESULT OnOcmDrawItem(WPARAM wParam, LPARAM lParam);
        .
        .
        .

Now create the OnOcmDrawItem() function in CONTROLCTL.CPP by typing it in:

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
}

As with most direct Windows message handlers, we are passed two parameters: wParam and lParam, the standard low-level parameters that accompany Windows messages. Here, lParam is a long pointer to a structure of type LPDRAWITEMSTRUCT; because this structure contains additional information about what we're supposed to be drawing, it will allow us to draw our button in both the pressed and the released states. For example, from the hDC member of the LPDRAWITEMSTRUCT structure we get a pointer to the device context we're supposed to draw in. The hDC member is actually a handle to a device context, and it is simply a numerical index standing for that device context as far as Windows is concerned. We create a pointer to the device context from hDC using the Visual C++ CDC class function FromHandle():

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
--> CDC *pdc;
--> pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
      .
      .
      .
}

Now that we have a pointer to the device context we want to draw in, the next step is to load the bitmap from the disk. We use the two bitmaps we created—IDB_DOWNBITMAP for the button-down bitmap and IDB_UPBITMAP for the button-up bitmap—and we load the appropriate bitmap depending on whether the button is up or down. We check the itemState member of the DRAWITEMSTRUCT structure that we were passed a pointer to. If the ODS_SELECTED bit in itemState is set, we draw the button as pressed; otherwise, we draw it as released. Based on itemState, then, we load the correct bitmap into an object of class CBitmap named bmpObj:

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
--> CBitmap bmpObj;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
--> bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
        .
        .
        .

At this point, we have loaded the bitmap to display into a bitmap object of the MFC class CBitmap. The next step is to display the bitmap. We need the dimensions of the bitmap, which we get by filling a structure of type BITMAP:

typedef struct tagBITMAP {
            int     bmType;
            int     bmWidth;
            int     bmHeight;
            int     bmWidthBytes;
            BYTE    bmPlanes;
            BYTE    bmBitsPixel;
            LPVOID  bmBits;
        } BITMAP;

We fill the BITMAP structure, named bmp, using the CBitmap member function GetObject():

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
    CBitmap bmpObj;
--> BITMAP  bmp;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
    bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
--> bmpObj.GetObject(sizeof(BITMAP), &bmp);
        .
        .
        .

To determine the dimensions of the bitmap, we use the BITMAP members bmWidth and bmHeight to fill a CRect object named rect:

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
    CBitmap bmpObj;
    BITMAP  bmp;
--> CRect rect;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
    bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
    bmpObj.GetObject(sizeof(BITMAP), &bmp);
--> rect.right = bmp.bmWidth;
--> rect.bottom = bmp.bmHeight;
        .
        .
        .
}

Now we have the bitmap and its size, so all that's left is to draw it. We'll use a shortcut method: the Visual C++ class CPictureHolder. A popular property (we'll see more about properties soon) for ActiveX controls is the Picture property, which holds the image of the control. Reloading the Picture property with a new bitmap changes the appearance of the control. We won't support the picture property here, but we will use the CPictureHolder class, because it is designed to hold a control's bitmap (its graphical representation) and display it easily. Because the CPictureHolder class is specially made to work with the image of our control, we will create a CPictureHolder object here and load our bitmap into it:

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
    CBitmap bmpObj;
    BITMAP  bmp;
--> CPictureHolder picHolderObj;
    CRect rect;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
    bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
    bmpObj.GetObject(sizeof(BITMAP), &bmp);
    rect.right = bmp.bmWidth;
    rect.bottom = bmp.bmHeight;
--> picHolderObj.CreateFromBitmap((HBITMAP)bmpObj.m_hObject, NULL, FALSE);
        .
        .
        .
}

To draw the item, we need only call the CPictureHolder member function Render(). We pass it the size of the control's bounding rectangle (which we get as rcItem in the DRAWITEMSTRUCT structure passed to us) and the bounding rectangle of the bitmap we want to draw. Finally, we return a value of 1 from OnOcmDrawItem(), indicating success:

LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
    CBitmap bmpObj;
    BITMAP  bmp;
    CPictureHolder picHolderObj;
    CRect rect;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
    bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
    bmpObj.GetObject(sizeof(BITMAP), &bmp);
    rect.right = bmp.bmWidth;
    rect.bottom = bmp.bmHeight;
    picHolderObj.CreateFromBitmap((HBITMAP)bmpObj.m_hObject, NULL, FALSE);
--> picHolderObj.Render(pdc, ((LPDRAWITEMSTRUCT)lParam)->rcItem, rect);
--> return 1;
}

We create and support control.ocx by selecting the Build control.ocx menu item in the Visual C++ Project menu, followed by Register Control in the Tools menu.

Now let's see our control in action. Find control.ocx's uuid value (from the corresponding .odl file or from the Windows registry by using regedit.exe) and create an .html file:

<HTML>
<HEAD>
<TITLE>Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBS ONLOAD = "Page_Initialize">
<CENTER>
<H1>Control Page</H1>
</CENTER>
<!- control.ocx>
<CENTER>
<OBJECT CLASSID="clsid:D96FBCC1-090A-101C-BAC7-040224009C02"
        HEIGHT=80 WIDTH=100 ID=Ctrl1></OBJECT>
</BODY>
</HTML>

The result appears in Figure 9.7; when you click this CONTROL button, the image displayed turns from the cloudy day to the sunny one and back again. Even though we've subclassed our control from an existing Windows control (a command button), we now know how to draw it ourselves. So far, then, control.ocx is a success.

Figure 9.7 Our control.ocx control.

There is far more to learn about controls. For example, controls may support properties, methods, and custom events. After we add our custom control to a VBScript page, we simply use VBScript to connect subroutines to our custom events, just as we would for the events of any other control.

Properties are useful. If you had a custom control that displayed text, for example, you might set the text simply by setting the control's Text property. Methods are also useful: if you had a custom control that displayed data in a graph, you might pass it the data to graph in a method named SetData().

We will also support custom events in our control. Let's say our control supports a custom event named posEvent, which we fire (firing an event causes its event-handing function to be called) when the mouse button is pushed. We could place our custom control in a VBScript page, and the control would display its cloudy or sunny day image as required. In addition, we could set up a VBScript subroutine named Ctrl1_posEvent() to handle the control's event (assuming we had given our control the ID Ctrl1) just as it might handle a button control's click event with a function named Button1_OnClick(). In this way, as far as VBScript is concerned, our new OCX acts just like any other control (such as a button or textbox). As another example, if we had a custom control named DATE that displayed the date, we could give it an event named DateChanged, which would be fired every time the date changed. Then, in VBScript, we could connect a subroutine, DateCtrl_DateChanged(), to an embedded DATE control.

Properties, methods, and events are integral parts of ActiveX controls, so let's examine them now.


Control Properties

Adding properties to control.ocx is easy with Visual C++'s ClassWizard. Let's add a property named Counter, which is like a data member of a C++ object. Counter simply holds a short value that we can initialize to zero and that we can increment as we like (we will count the number of times the button is clicked).

To add Counter to our custom control, open ClassWizard and select the OLE Automation tab. Make sure that the control's class, CControlCtrl, is selected in the Class Name box, as shown in Figure 9.8, and click Add Property, opening the Add Property box.

Figure 9.8 Using ClassWizard, we add a property to CONTROL.

Type the name of the new property, Counter, in the External Name box of the Add Property box, and select short in the Type box, making Counter a short value. Finally, click OK to create the new property. When controls of type CONTROL are inserted in our Web pages, we can reach this new property, Counter, easily, as we'll see in a minute.

Inside the CONTROL project, the new property will be stored in the short variable m_counter (declared by ClassWizard in CONTROLCTL.H). If you want to set the initial value of m_counter to something other than 0, you would look for this line in CONTROLCTL.CPP (do not add this code):

/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::DoPropExchange - Persistence support
void CControlCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);
--> PX_Short(pPX, _T("Counter"), m_counter, 0);
    // TODO: Call PX_ functions for each persistent custom property.
}

Here, the program sets up the Counter property; the PX_Short() function is used to initialize short properties. The arguments the program passes to PX_Short() are as follows: pPX is a pointer to the program's internal CPropertyExchange object (maintained by our program automatically), followed by the name of the property (Counter), followed by the name of the internal variable that holds the property's value (m_counter), and its initial value, here set to 0. That's exactly how a counter should start, at 0. We'll leave this line unchanged.

In addition to the new internal variable m_counter, ClassWizard has written a new function, OnCounterChanged() (from CONTROLCTL.CPP):

void CControlCtrl::OnCounterChanged()
{
    // TODO: Add notification handler code
    SetModifiedFlag();
}

The program calls this handy function when the Counter property is changed. For example, if changing this property is supposed to change other properties as well, we can make those changes in this function. Say that a control displays text and that the control's Text property (assuming the control has such a property) was changed. In this case, the program would call OnTextChanged(), allowing the control to update its display.

To test the new Counter property, create control.ocx using Build control.ocx in the Visual C++ Project menu. Open the control test container using the OLE Control Test Container item in the Tools menu. Insert a new control of type COUNTER in the test container and select Properties in the test container's View menu, opening the Properties box, as shown in Figure 9.9.

Figure 9.9 Setting our OCX control's Counter property.

Select Counter in the drop-down listbox labeled Property; the Counter property is set to 0, as shown in Figure 9.9. Using the Counter property in a Web page is easy, too. If the new control is given the ID Ctrl1, for example, we can reach the Counter property by referring to it as Ctrl1.Counter:

<HTML>
<HEAD>
<TITLE>Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Control Page</H1>
</CENTER>
<!- control.ocx>
<CENTER>
<OBJECT CLASSID="clsid:D96FBCC1-090A-101C-BAC7-040224009C02" HEIGHT=80
        WIDTH=100 ID=Ctrl1></OBJECT>
<INPUT TYPE = TEXT NAME = Textbox SIZE=20>
</CENTER>
<SCRIPT LANGUAGE = VBScript>
        Sub Page_Initialize
  -->           Ctrl1.Counter = 5
        End Sub
</SCRIPT>
</BODY>
</HTML>

That's all there is to it. We can also increment the Counter property each time the user clicks the control. Return to Visual C++ and use ClassWizard to add a WM_LBUTTONDOWN message handler to the CControlCtrl class. Open that function (from CONTROLCTL.CPP):

void CControlCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    COleControl::OnLButtonDown(nFlags, point);
}

This function is called when the user clicks our custom button, and we can increment the Counter property each time by incrementing the internal variable m_counter:

void CControlCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    COleControl::OnLButtonDown(nFlags, point);
--> m_counter++;
}

And that's all there is to it. As shown in Figure 9.10, we've incremented Counter to a value of 5.

Figure 9.10 We increment the Counter property by clicking our control.

It was easy to add properties to our control. Now let's take a look at how to add a method.


Control Methods

OLE automation methods are like member functions of C++ objects. For example, we might add a method named Beep() to the CONTROL project, making the computer beep. Using ClassWizard, methods are as easy to add to our control as properties were. To add the Beep() method, open ClassWizard and select the OLE Automation tab. Make sure that the control class, CControlCtrl, is selected in the Class Name box; click Add Method, opening the Add Method box, as shown in Figure 9.11.

Figure 9.11 Using the Add Method box, we add the Beep() method to our OCX control.

Type the name of this method, Beep, in the External Name box and select void in the Return Type drop-down listbox, indicating that we do not want to return any values from this function. If we had wanted to, we could have had our method return values such as long, short, or float by selecting them in this box. In addition, our method will not have any parameters passed to it. To receive parameters, we could type their names in the Name column of the Parameter List box (see Figure 9.11), and then select the parameter type in the drop-down listbox that appears in the Type column of the Parameter List box (we'll see how this works when we add a custom event next). Now click OK in the Add Method box, adding the Beep() method to CONTROL. Next, click the Edit Code button in ClassWizard, opening the new Beep() method:

void CControlCtrl::Beep()
{
}

We'll have the computer beep when this method is invoked, so add this line of code:

void CControlCtrl::Beep()
{
--> MessageBeep(MB_OK);
}

Now we've added a new method to our control. We can test this new method by inserting a control of the CONTROL type in the OLE control test container and selecting Invoke Methods in the test container's Edit menu, opening the Invoke Control Method box as shown in Figure 9.12.

Figure 9.12 Invoking our control's new method.

Our Beep() method is available for use; clicking Invoke calls the Beep() method and makes the computer beep. To reach the Beep() method from VBScript, we do just what you might expect; we call Ctrl1.Beep() (we've given our new control the ID Ctrl1).

<HTML>
<HEAD>
<TITLE>Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Control Page</H1>
</CENTER>
<!- control.ocx>
<CENTER>
<OBJECT CLASSID="clsid:D96FBCC1-090A-101C-BAC7-040224009C02"
HEIGHT=80  WIDTH=100 ID=Ctrl1></OBJECT>
<INPUT TYPE = TEXT NAME = Textbox SIZE=20>
</CENTER>
<SCRIPT LANGUAGE = VBScript>
        Sub Page_Initialize
   -->          Ctrl1.Beep()
                Ctrl1.Counter = 5
        End Sub
</SCRIPT>
</BODY>
</HTML>

That's it—it was easy to add a new method and reach it from VBScript. The next step is to support custom events, and we'll take a look at that now.


Custom Events

The custom event we add to control.ocx will be called posEvent, and we will fire this event when the mouse button goes down, passing the location of the mouse button to the handling function. (posEvent stands for position event.)

Open ClassWizard and select the OLE Events tab. Make sure that the control class, CControlCtrl, is selected in the Class Name box and click Add Event, opening the Add Event box as shown in Figure 9.13.

Figure 9.13 We add a custom event to our control.

Type posEvent into the External Name box, as shown in Figure 9.13, and type x in the Parameter List box. A drop-down listbox appears on the right side of the Parameter List box; select OLE_XPOS_PIXELS as the x parameter's type. Now enter a new parameter (y) in the Parameter List box and select the OLE_YPOS_PIXELS type for it. The result appears in Figure 9.13. Close the Add Event box by clicking OK, adding the new posEvent event to the control. Click OK in ClassWizard.

ClassWizard has connected the posEvent event to a function to fire that event. FirePosEvent() is set up this way in CONTROLCTL.CPP:

// Event maps
    //{{AFX_EVENT(CControlCtrl)
    void FirePosEvent(OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y)
        {FireEvent(eventidPosEvent,EVENT_PARAM(VTS_XPOS_PIXELS
            VTS_YPOS_PIXELS), x, y);}
    //}}AFX_EVENT
    DECLARE_EVENT_MAP()

To fire this event, then, we need only call FirePosEvent(). We fire the new event each time the mouse button is pressed, so we call FirePosEvent() in the OnLButtonDown() function (from CONTROLCTL.CPP):

void CControlCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    COleControl::OnLButtonDown(nFlags, point);
    m_counter++;
--> FirePosEvent(point.x, point.y);
}

Here, we fire a posEvent on event mouse click, and we pass the mouse position to this event. To test this event in the test container, we watch as the custom events are fired. Create control.ocx and insert a control of that type into the test container. Next, select Event Log in the test container's View menu, opening the event log as shown in Figure 9.14.

Figure 9.14 We see our custom event fired in control.ocx.

When you click the CONTROL button, the program fires a posEvent event, and the results appear in the event log, as shown in Figure 10.14. Also, the x and y mouse location inside the control (measured in pixels) is also passed to this event handler.

Now let's see this in VBScript. Let's add a textbox to our Web page so that we can display the current value of the control's Counter property:

     -----------------------------------------------
           |                                               |
           |                                               |
           |         --------------                        |
           |        |              |  --------------       |
           |        | OCX Control  | | Counter = 8  |      |
           |        |              |  --------------       |
           |         --------------                        |
           |                                               |
           |                                               |
           |                                               |
           |                                               |
            -----------------------------------------------

In the Web page itself, we connect control.ocx's posEvent event to the subroutine Ctrl1_posEvent(x, y) (our new control has the ID Ctrl1):

<HTML>
<HEAD>
<TITLE>Control Page</TITLE>
</HEAD>
<BODY LANGUAGE = VBScript ONLOAD = "Page_Initialize">
<CENTER>
<H1>Control Page</H1>
</CENTER>
<!- control.ocx>
<CENTER>
<OBJECT CLASSID="clsid:D96FBCC1-090A-101C-BAC7-040224009C02"
HEIGHT=80  WIDTH=100 ID=Ctrl1></OBJECT>
<INPUT TYPE = TEXT NAME = Textbox SIZE=20>
</CENTER>
<SCRIPT LANGUAGE = VBScript>
        Sub Page_Initialize
                Ctrl1.Beep()

Ctrl1.Counter = 5

End Sub
   -->  Sub Ctrl1_posEvent(x, y)
   -->          Textbox.Value = "Counter = " + CStr(Ctrl1.Counter)
   -->  End Sub
</SCRIPT>
</BODY>
</HTML>

Here, we increment the value in the control's Counter property by repeatedly clicking the control. The result appears in Figure 9.15.

Figure 9.15 Our new OCX control at work.

That's it—control.ocx is a complete success. The files CONTROL.H and CONTROL.CPP can found in Listing 9.4. CONTROLCTL.H and CONTROLCTL.CPP are in Listing 9.5, and CONTROL.ODL is in Listing 9.6.

Listing 9.4 CONTROL.H and CONTROL.CPP

// control.h : main header file for CONTROL.DLL
#if !defined( __AFXCTL_H__ )
        #error include ‘afxctl.h' before including this file
#endif
#include "resource.h"       // main symbols
/////////////////////////////////////////////////////////////////////////////
// CControlApp : See control.cpp for implementation.
class CControlApp : public COleControlModule
{
public:
        BOOL InitInstance();
        int ExitInstance();
};
extern const GUID CDECL _tlid;
extern const WORD _wVerMajor;
extern const WORD _wVerMinor;
// control.cpp : Implementation of CControlApp and DLL registration.
#include "stdafx.h"
#include "control.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CControlApp NEAR theApp;
const GUID CDECL BASED_CODE _tlid =
                { 0x1dd8502f, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66,
        0x8f, 0xc, 0xf4, 0x47 } };
const WORD _wVerMajor = 1;
const WORD _wVerMinor = 0;
////////////////////////////////////////////////////////////////////////////
// CControlApp::InitInstance - DLL initialization
BOOL CControlApp::InitInstance()
{
        BOOL bInit = COleControlModule::InitInstance();
        if (bInit)
        {
                // TODO: Add your own module initialization code here.
        }
        return bInit;
}
////////////////////////////////////////////////////////////////////////////
// CControlApp::ExitInstance - DLL termination
int CControlApp::ExitInstance()
{
        // TODO: Add your own module termination code here.
        return COleControlModule::ExitInstance();
}
/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
        AFX_MANAGE_STATE(_afxModuleAddrThis);
        if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
                return ResultFromScode(SELFREG_E_TYPELIB);
        if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
                return ResultFromScode(SELFREG_E_CLASS);
        return NOERROR;
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
        AFX_MANAGE_STATE(_afxModuleAddrThis);
        if (!AfxOleUnregisterTypeLib(_tlid))
                return ResultFromScode(SELFREG_E_TYPELIB);
        if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
                return ResultFromScode(SELFREG_E_CLASS);
        return NOERROR;
}

Listing 9.5 CONTROLCTL.H and CONTROLCTL.CPP

// ControlCtl.h : Declaration of the CControlCtrl OLE control class.
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl : See ControlCtl.cpp for implementation.
class CControlCtrl : public COleControl
{
        DECLARE_DYNCREATE(CControlCtrl)
// Constructor
public:
        CControlCtrl();
// Overrides
        // Drawing function
        virtual void OnDraw(
                    CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid);
        // Persistence
        virtual void DoPropExchange(CPropExchange* pPX);
        // Reset control state
        virtual void OnResetState();
// Implementation
protected:
        ~CControlCtrl();
        DECLARE_OLECREATE_EX(CControlCtrl)    // Class factory and guid
        DECLARE_OLETYPELIB(CControlCtrl)      // GetTypeInfo
        DECLARE_PROPPAGEIDS(CControlCtrl)     // Property page IDs
        DECLARE_OLECTLTYPE(CControlCtrl)      // Type name and misc status
        // Subclassed control support
        BOOL PreCreateWindow(CREATESTRUCT& cs);
        BOOL IsSubclassedControl();
        LRESULT OnOcmCommand(WPARAM wParam, LPARAM lParam);
        LRESULT OnOcmDrawItem(WPARAM wParam, LPARAM lParam);
// Message maps
        //{{AFX_MSG(CControlCtrl)
        afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
// Dispatch maps
        //{{AFX_DISPATCH(CControlCtrl)
        short m_counter;
        afx_msg void OnCounterChanged();
        afx_msg void Beep();
        //}}AFX_DISPATCH
        DECLARE_DISPATCH_MAP()
        afx_msg void AboutBox();
// Event maps
        //{{AFX_EVENT(CControlCtrl)
        void FirePosEvent(OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y)
                {FireEvent(eventidPosEvent,EVENT_PARAM(VTS_XPOS_PIXELS
            VTS_YPOS_PIXELS), x, y);}

//}}AFX_EVENT

DECLARE_EVENT_MAP()
// Dispatch and event IDs
public:
        enum {
        //{{AFX_DISP_ID(CControlCtrl)
        dispidCounter = 1L,
        dispidBeep = 2L,
        eventidPosEvent = 1L,
        //}}AFX_DISP_ID
        };
};
// ControlCtl.cpp : Implementation of the CControlCtrl OLE control class.
#include "stdafx.h"
#include "control.h"
#include "ControlCtl.h"
#include "ControlPpg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CControlCtrl, COleControl)
/////////////////////////////////////////////////////////////////////////////
// Message map
BEGIN_MESSAGE_MAP(CControlCtrl, COleControl)
        //{{AFX_MSG_MAP(CControlCtrl)
        ON_WM_LBUTTONDOWN()
        //}}AFX_MSG_MAP
        ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
        ON_MESSAGE(OCM_DRAWITEM, OnOcmDrawItem)
        ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// Dispatch map
BEGIN_DISPATCH_MAP(CControlCtrl, COleControl)
        //{{AFX_DISPATCH_MAP(CControlCtrl)
        DISP_PROPERTY_NOTIFY(CControlCtrl, "Counter", m_counter,
            OnCounterChanged, VT_I2)
        DISP_FUNCTION(CControlCtrl, "Beep", Beep, VT_EMPTY, VTS_NONE)
        //}}AFX_DISPATCH_MAP
        DISP_FUNCTION_ID(CControlCtrl, "AboutBox", DISPID_ABOUTBOX,
            AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()
/////////////////////////////////////////////////////////////////////////////
// Event map
BEGIN_EVENT_MAP(CControlCtrl, COleControl)
     //{{AFX_EVENT_MAP(CControlCtrl)
     EVENT_CUSTOM("posEvent", FirePosEvent, VTS_XPOS_PIXELS  VTS_YPOS_PIXELS)
     //}}AFX_EVENT_MAP
END_EVENT_MAP()
/////////////////////////////////////////////////////////////////////////////
// Property pages
// TODO: Add more property pages as needed.  Remember to increase the count!

BEGIN_PROPPAGEIDS(CControlCtrl, 1)

PROPPAGEID(CControlPropPage::guid)
END_PROPPAGEIDS(CControlCtrl)
/////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid
IMPLEMENT_OLECREATE_EX(CControlCtrl, "CONTROL.ControlCtrl.1",
        0xd96fbcc1, 0x90a, 0x101c, 0xba, 0xc7, 0x4, 0x2, 0x24, 0, 0x9c, 0x2)
/////////////////////////////////////////////////////////////////////////////
// Type library ID and version
IMPLEMENT_OLETYPELIB(CControlCtrl, _tlid, _wVerMajor, _wVerMinor)
/////////////////////////////////////////////////////////////////////////////
// Interface IDs
const IID BASED_CODE IID_DControl =
                { 0x1dd85030, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66,
        0x8f, 0xc, 0xf4, 0x47 } };
const IID BASED_CODE IID_DControlEvents =
                { 0x1dd85031, 0x3ade, 0x11cf, { 0xb0, 0x1d, 0x92, 0x66,
        0x8f, 0xc, 0xf4, 0x47 } };
/////////////////////////////////////////////////////////////////////////////
// Control type information
static const DWORD BASED_CODE _dwControlOleMisc =
        OLEMISC_ACTIVATEWHENVISIBLE |
        OLEMISC_SETCLIENTSITEFIRST |
        OLEMISC_INSIDEOUT |
        OLEMISC_CANTLINKINSIDE |
        OLEMISC_RECOMPOSEONRESIZE;
IMPLEMENT_OLECTLTYPE(CControlCtrl, IDS_CONTROL, _dwControlOleMisc)
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::CControlCtrlFactory::UpdateRegistry -
// Adds or removes system registry entries for CControlCtrl
BOOL CControlCtrl::CControlCtrlFactory::UpdateRegistry(BOOL bRegister)
{
        if (bRegister)
                return AfxOleRegisterControlClass(
                        AfxGetInstanceHandle(),
                        m_clsid,
                        m_lpszProgID,
                        IDS_CONTROL,
                        IDB_CONTROL,
                        FALSE,                      //  Not insertable
                        _dwControlOleMisc,
                        _tlid,
                        _wVerMajor,
                        _wVerMinor);
        else
                return AfxOleUnregisterClass(m_clsid, m_lpszProgID);
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::CControlCtrl - Constructor
CControlCtrl::CControlCtrl()
{
        InitializeIIDs(&IID_DControl, &IID_DControlEvents);
        // TODO: Initialize your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::~CControlCtrl - Destructor
CControlCtrl::~CControlCtrl()
{
        // TODO: Cleanup your control's instance data here.
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::OnDraw - Drawing function
void CControlCtrl::OnDraw(
             CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
        DoSuperclassPaint(pdc, rcBounds);
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::DoPropExchange - Persistence support
void CControlCtrl::DoPropExchange(CPropExchange* pPX)
{
        ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
        COleControl::DoPropExchange(pPX);
        PX_Short(pPX, _T("Counter"), m_counter, 0);
        // TODO: Call PX_ functions for each persistent custom property.
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::OnResetState - Reset control to default state
void CControlCtrl::OnResetState()
{
        COleControl::OnResetState();  // Resets defaults in DoPropExchange
        // TODO: Reset any other control state here.
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::AboutBox - Display an "About" box to the user
void CControlCtrl::AboutBox()
{
        CDialog dlgAbout(IDD_ABOUTBOX_CONTROL);
        dlgAbout.DoModal();
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::PreCreateWindow - Modify parameters for CreateWindowEx
BOOL CControlCtrl::PreCreateWindow(CREATESTRUCT& cs)
{
        cs.lpszClass = _T("BUTTON");
        cs.style |= BS_PUSHBUTTON | BS_OWNERDRAW;
        return COleControl::PreCreateWindow(cs);

}

/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::IsSubclassedControl - This is a subclassed control
BOOL CControlCtrl::IsSubclassedControl()
{
        return TRUE;
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl::OnOcmCommand - Handle command messages
LRESULT CControlCtrl::OnOcmCommand(WPARAM wParam, LPARAM lParam)
{
#ifdef _WIN32
        WORD wNotifyCode = HIWORD(wParam);
#else
        WORD wNotifyCode = HIWORD(lParam);
#endif
        // TODO: Switch on wNotifyCode here.
        return 0;
}
/////////////////////////////////////////////////////////////////////////////
// CControlCtrl message handlers
LRESULT CControlCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
    CDC *pdc;
    CBitmap bmpObj;
    BITMAP  bmp;
    CPictureHolder picHolderObj;
    CRect rect;
    pdc = CDC::FromHandle(((LPDRAWITEMSTRUCT)lParam)->hDC);
    bmpObj.LoadBitmap((((LPDRAWITEMSTRUCT)lParam)->itemState &
        ODS_SELECTED) ? IDB_DOWNBITMAP : IDB_UPBITMAP);
    bmpObj.GetObject(sizeof(BITMAP), &bmp);
    rect.right = bmp.bmWidth;
    rect.bottom = bmp.bmHeight;
    picHolderObj.CreateFromBitmap((HBITMAP)bmpObj.m_hObject, NULL, FALSE);
        picHolderObj.Render(pdc, ((LPDRAWITEMSTRUCT)lParam)->rcItem, rect);
        return 1;
}
void CControlCtrl::OnCounterChanged()
{
        // TODO: Add notification handler code
        SetModifiedFlag();
}
void CControlCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
        m_counter++;
        FirePosEvent(point.x, point.y);
        COleControl::OnLButtonDown(nFlags, point);
}
void CControlCtrl::Beep()
{
        MessageBeep(MB_OK);
}

Listing 9.6 CONTROL.ODL

// control.odl : type library source for OLE Control project.
// This file will be processed by the Make Type Library (mktyplib) tool to
// produce the type library (control.tlb) that will become a resource in
// control.ocx.
#include <olectl.h>
[ uuid(1DD8502F-3ADE-11CF-B01D-92668F0CF447), version(1.0),
  helpstring("control OLE Control module"), control ]
library CONTROLLib
{
        importlib(STDOLE_TLB);
        importlib(STDTYPE_TLB);
        //  Primary dispatch interface for CControlCtrl
        [ uuid(1DD85030-3ADE-11CF-B01D-92668F0CF447),
          helpstring("Dispatch interface for Control Control"), hidden ]
        dispinterface _DControl
        {
                properties:
                        // NOTE - ClassWizard will maintain property info
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_PROP(CControlCtrl)
                        [id(1)] short Counter;
                        //}}AFX_ODL_PROP
                methods:
                        // NOTE - ClassWizard will maintain method information
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_METHOD(CControlCtrl)
                        [id(2)] void Beep();
                        //}}AFX_ODL_METHOD
                        [id(DISPID_ABOUTBOX)] void AboutBox();
        };
        //  Event dispatch interface for CControlCtrl
        [ uuid(1DD85031-3ADE-11CF-B01D-92668F0CF447),
          helpstring("Event interface for Control Control") ]
        dispinterface _DControlEvents
        {
                properties:
                        //  Event interface has no properties
                methods:
                        // NOTE - ClassWizard will maintain event information
                        //    Use extreme caution when editing this section.
                        //{{AFX_ODL_EVENT(CControlCtrl)
                        [id(1)] void posEvent(OLE_XPOS_PIXELS x,
                            OLE_YPOS_PIXELS y);
                        //}}AFX_ODL_EVENT
        };
        //  Class information for CControlCtrl
        [ uuid(D96FBCC1-090A-101C-BAC7-040224009C02),

helpstring("Control Control"), control ]

coclass Control
        {
                [default] dispinterface _DControl;
                [default, source] dispinterface _DControlEvents;
        };
        //{{AFX_APPEND_ODL}}
};

That's it for our survey of custom controls, and that's it for our survey of VBScript. We've come far in this book: from the basics of Web pages to the basics of VBScript and beyond. We've discussed how to implement buttons and textboxes and forms and documents and many other types of controls and objects, including the creation of your own controls for use with VBScript. All that remains now is to put all this technology to work for yourself. Happy programming.


Topic Not Found

No matching topic was found. Some possible reasons and remedies are as follows:

  • The file (.chm) that contains the destination topic is not in the expected location.

    Remedy: Rerun the MSDN Library Setup.

  • The file (.chm) that contains the current topic or destination topic is out-of-date and the link is no longer valid.

    Remedy: Install the latest version of the MSDN Library.

  • The MSDN Library CD is not in the CD-ROM drive.

    Remedy: Insert the MSDN Library CD into the CD-ROM drive.