MSDN Magazine > Issues and Downloads > 2000 > October >  Cutting Edge: Client-side Environment for ASP P...
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine
Client-side Environment for ASP Pages-Part 2
Dino Esposito
Download the code for this article: Cutting1000.exe (101KB)
Browse the code for this article at Code Center: Client-side ASP Browser
L
ast month (September 2000) I gave you a quick tour of the script technologies you need to create a client-side environment that can execute ASP pages. I built a custom browser in Visual Basic® that is capable of processing ASP pages without Microsoft® Internet Information Services (IIS). This browser renders the HTML contained in an ASP page when you double-click on the ASP file. This information allows you to write ASP pages that are more versatile; they'll work both online and offline. When you're online, they run within the context of IIS and Microsoft Transaction Services (MTS) and COM+ and take advantage of the IIS features such as the ASP intrinsic object model and protected environment. When you're offline, you have access to a runtime enriched with simulated ASP objects that you can customize with other functionality.
      A client-side environment for ASP pages shares some, but not all, of the features of its server-side counterpart. On the client, you will usually not need the Session or Application objects. The Server object's programming interface is limited. You will, however, need Response and Request, plus a few new objects.
      Last month I showed how to implement the Response object through a very simple ATL component. I then injected that code into the scripting namespace to make it available to the VBScript parser that processes all the ASP code blocks. This month I'll follow the same approach for the Request object, and illustrate how to process form-based submissions.

The Request Object

      In the original ASP object model, the Request object is the COM component that wraps all the data coming from the browser through HTTP POST and GET commands into a set of data structures. The Request object is also frequently used to retrieve special environmental variables through the ServerVariables collection. A client-side version of the Request object must expose at least the QueryString and Form collections. The QueryString contains the parameters sent through a GET command. The Form collection contains the parameters arriving from an HTML form via the POST command.
      You can append the QueryString parameters to the end of a URL, preceded by a ? character:
http://www.expoware.com/seminars.asp?code=1&info=all
      In a server-side ASP page, you retrieve those parameters through the QueryString collection of the Request object:
strCode = Request.QueryString("code")
strInfo = Request.QueryString("info")
      But what if you want to send a query string to a local ASP page? You must create a way for your offline environment to handle parameters sent along with the path:
c:\pages\seminars.asp?code=1&info=all
      The special browser I built last month is able to open an ASP page as a file and process its contents. Of course, if the ASP page contains unnecessary parameters, you should strip them before processing the page content. Furthermore, you should make the necessary parameters available through the QueryString collection of name/value pairs. The following VBScript code excerpt shows how to extend the ParseTextToFile method of the CAspParser class I introduced last month.
nPos = InStr(1, aspFile, "?")
If nPos > 0 Then
   strParams = Right(aspFile, Len(aspFile) - nPos)
   ExtractHttpGetParams strParams
        
   ' Truncates the URL
   aspFile = Left(aspFile, nPos - 1)
End If
      The ParseTextToFile method is invoked when the browser is called to navigate to a local ASP page. The argument (aspFile) is the ASP page name. As shown earlier, the path can be a sort of a hybrid: neither a fully qualified path name nor a regular URL.
      In the code shown earlier, I first extract all the characters that follow any ? character that is encountered, then strip them from the string. To the browser, the ? character has a special meaning within a file name since it is one of the few characters not allowed to be part of a Windows long file name.
      Next, the string
code=1&info=all
is passed for further processing to a new procedure called ExtractHttpGetParams (see Figure 1). This subroutine splits the string into name=value pairs and stores them in the collection that the client-side Request object makes available.
      Figure 2 shows the actual implementation of the Request object. The progID is MyASP2.Request. This code simply exposes two properties that turn out to be instances of the Dictionary collection object. I'll discuss the Scripting.Dictionary object in more detail later on.
      The Request object is added to the scripting object model through the AddObject method of the Script Control I use to execute the ASP script blocks.
' Populate the script's namespace with fake ASP 
' objects
Set m_objResponse = _ CreateObject("MyASP.Response")
m_objScriptCtl.AddObject "Response", _ 
    m_objResponse

Set m_objRequest = CreateObject("MyASP2.Request")
m_objScriptCtl.AddObject "Request", m_objRequest
      Let's take a look at a simple ASP page to verify that all this really works in practice. Consider the following ASP page called request.asp:
<html>
<body>
<table>
<%
For Each elem In Request.QueryString
    Response.Write "<tr><td>"        
    Response.Write elem & "=" & _
        Request.QueryString(elem) 
    Response.Write "</td></tr>"
Next
%>
</table>
</body>
</html>
This code dumps out all the name/value pairs stored in the QueryString collection. When called over HTTP with a query string like
code=1&info=all
it produces the output shown in Figure 3. As you can see in Figure 4, the result is identical in the offline browser. In both cases, the source code of the ASP page has access to a valid Request object that is capable of satisfying all of its requests.

Figure 3 Request.asp in the Browser
Figure 3 Request.asp in the Browser


Figure 4 Request.asp in the Offline Browser
Figure 4 Request.asp in the Offline Browser

The Dictionary and Collection Objects

      If you're using Visual Basic to implement the Request object, it might seem a natural choice to employ the native Visual Basic Collection object to code both the Form and the QueryString collections. Admittedly, this is exactly what I did when I first coded this object. The Visual Basic Collection object, though, is not a perfect reproduction of the ASP native collections. Using a Visual Basic Collection, you won't be able to get the name or the value of any particular item. You must use either an index or the specified key to retrieve a value. In other words, to populate a Visual Basic Collection you need code like this:
Dim c As New Collection
c.Add "1", "Code"
c.Add "all", "Info"
      The Collection object is designed to be more of a flexible container for data than an associative array or a dictionary. The name or the key is only a shortcut to reach a certain item, it's not the actual data. There's no way for you to enumerate the names or keys available from a Visual Basic Collection. While this feature is supported by the ASP Request object, you need something else to simulate the behavior of Request locally. The answer is the dictionary object that Microsoft introduced with VBScript 3.0. The dictionary object is also available as a standalone component and can be downloaded from the Microsoft Web site as part of the Microsoft Scripting Runtime Library (http://msdn.microsoft.com/scripting). It is also part of the Windows® Script Host runtime and a native component of both Windows 98 and Windows 2000.
      The progID of this component is Scripting.Dictionary and its whole programming interface is summarized in Figure 5. To populate a dictionary, you would use code like this:
Set d = CreateObject("Scripting.Dictionary")
d.Add "Code", "1"
d.Add "Info", "all"
Note that now the name/key is the first argument of the Add method and is the one that the For Each enumerator retrieves by default. A dictionary object allows you to write and run the following code successfully:
For Each elem In d
    MsgBox elem & "=" & d(elem)
Next
      To build a client-side Request object, you need to use the Scripting.Dictionary object to expose the Form, QueryString, and ServerVariables objects. In general, any ASP collection that you want to emulate must be coded through this dictionary or a programmatically equivalent object.

HTML Forms

      Forms are a key element in the Web applications architecture, and you must be able to simulate them to make offline ASP a reality.
      An HTML form gathers input elements and makes their values available to the ASP page designated as the target of the form.
<form action="foo.asp"> ••• </form>
      The foo.asp page will process all the information entered in the form's input fields. Whether these arguments will be available through the Form or the QueryString collection depends on the HTTP command you choose to issue the request. By default, the request goes through a GET command (QueryString), but if you set the form's method property to POST, the HTTP POST command is used and the arguments are available through Form. Since software isn't magic, the next natural question is who's responsible for filling either the Form or the QueryString collection? The answer is easy: IIS.
      IIS is responsible for setting up and exposing the whole ASP object model and the environment where an ASP page will be processed. When the browser detects a form submission, it collects all the arguments in the form, creates a sort of query string, and uses the syntax of the HTTP GET or POST commands to send it to the Web server in a standard way. IIS grabs this data and makes it available through extremely flexible data structures such as dictionaries.
      In a client-side scenario, you obviously cannot rely on IIS, but the browser must still hook on the form submission. In this example, when I realize that the user clicked on a submit button, I run a piece of code that scans the form content, searching for those input elements with both non-null and non-empty name and value properties. Next, I create an ampersand-separated string (&) by concatenating the name of the input element, an = symbol, and its value. For example, given
<form name="Main" method="post" action="foo.asp">
<input type=text name=MyName></input>
<input type=submit name=MyButton value=Go></input>
</form>
the string is
MyName=...&MyButton=Go
      The code shown in Figure 6 demonstrates how a string like this is then split and inserted into either the Request's Form or QueryString dictionaries.
      The offline browser is a Visual Basic-based (or C++-based) application built around the WebBrowser control. To detect the form submission, it needs to know what's going on in the context of the page being viewed. The easiest way of achieving this is to reference the MSHTML object model in the project, then declare a WithEvents variable of type HTMLDocument:
Private WithEvents m_htmlDoc As HTMLDocument
Set this variable with the current HTML document:
Private Sub WB_DocumentComplete(ByVal pDisp As Object,URL As Variant)
    Set m_htmlDoc = WB.Document
    Set m_htmlWindow = m_htmlDoc.parentWindow
End Sub
      Now you're ready to handle all the events, such as the onclick event shown in Figure 6. Storing a reference to the parent window object during the DocumentComplete event helps to retrieve the source event object later.
Dim objSrc As HTMLInputButtonElement
Set objSrc = _
m_htmlWindow.event.srcElement
      At this point, it's easy to check the tag name and the type of the element that was clicked to start the DHTML event bubbling up to the document object.
      When a form submission is in progress, you can govern the whole operation yourself: populate the proper dictionary and navigate to the place the form's action property dictates. If the form's target is a relative file name, the WebBrowser control automatically completes it with the current path. In a client-side scenario this will be the path of the executable, not the path of the visited page. To adjust this, cancel the default handling of the click event (see Figure 6). The LocalNavigate method can add the full path to relative ASP file names.

Figure 7 HTML Post
<html>
<body>
 <form method="post"
  action="target.asp">
    <input type=text name=MyName>
    </input>
    <input type=submit value=Go>
    </input>
 </form>
</body>
</html>
Figure 7 HTML Post
<%
s = Request.Form("MyName")
Response.Write "You passed me " & _
               "<b>" & s & "</b>"
%>
Figure 7 HTML Post

      In Figure 7 you can see a simple, yet typical HTML form that posts the content of the input box to a target ASP page. The address bars show that it happened over HTTP. Figure 8 shows the same process in an ASP offline viewer.

Figure 8 Post in the Offline Browser
Figure 8 Post in the Offline Browser

Working Offline

      Being able to manipulate forms offline is a significant benefit since most of the work you do on the Web, such as using search and registration pages, involves filling out forms and posting data to the server. In Figure 9, you can see a page built on these principles. The page allows you to query for articles by specifying the magazine and the year of publication. The same ASP page also works when viewed with the offline browser (see Figure 10).
      After using this approach in practice for a few months, it became clear that most existing ASP pages have to be modified slightly to work both offline and online. The main goal of this project was not to preserve backward compatibility, but to ensure that ad-hoc ASP pages could work unchanged when viewed through both a standard Web browser and a custom client-side browser.
      After playing with the demo for a few days, the client for whom I created this browser asked for more features. In particular, he wanted to be able to detect the working mode (offline or online) from within the page, and extend the offline object model with additional offline-only objects. Initially, the new tasks sounded impossible to accomplish. I had to rethink the whole project and the overall architecture.
      Remember, I used a few lines of code I inject the simulated Response and Request objects into the scripting namespace. Following that same principle you insert custom objects as well. More on this later.
      The first approach I considered for distinguishing between online and offline activity was based on the ServerVariables collection (see Figure 2). In Figure 11 I summarized the main changes I made to the code to create a custom set of environment variables for offline availability. The HTTP_USER_AGENT variable now contains the name of the offline browser, while LOGON_USER is the name of the currently connected user, and REMOTE_HOST is the machine name. PATH_TRANSLATED points to the current path name (to maintain consistency with the API). GetUserName is the system API that returns the name of the logged user. Using it from Visual Basic poses the problem of determining the real string length:
Dim temp As String*40
Dim nLen As Long
GetUserName temp, nLen
MsgBox Left(temp, nLen - 1)
      The nLen argument represents the size of the buffer to be filled. Unlike several other Win32® APIs, GetUserName doesn't return the actual length of the string upon exit, but requires nLen to be passed by reference and modifies it. nLen must be a Long (32-bit, like a pointer), not an Integer, which is only 16-bit in Visual Basic. In addition, GetUserName sets nLen with the length of the string plus oneâ€"the terminating null character. You may think that GetComputerName, an API that you probably use in conjunction with GetUserName, would follow the same pattern for determining the actual size of the buffer. That's true, except that shortening the returned size by one actually truncates the buffer. The GetComputerName API doesn't add the final null to the count.
      However, in one way or another you can populate your customized ServerVariables collection. The following code renders the page shown in Figure 12.
<html><body>
<table>
<%
For Each elem In Request.ServerVariables
    Response.Write "<tr><td>"        
    Response.Write "<b>" & elem & "=</b>" & _
        Request.ServerVariables(elem) 
    Response.Write "</td></tr>"
Next
%>
</table>
</body></html> 
      Now you can make decisions based on the working mode. For example, you could check the content of the custom OFFLINE variable. (When choosing a custom variable name like this, make sure it doesn't appear in the standard (server-side) set of environment variables.) The following code snippet addresses the creation of dual pages:
<%
if Request.ServerVariables("OFFLINE") = "yes" then
    Response.Write "Working offline"
else
    Response.Write "Working online"
end if
%>
      Repeatedly using the Request object might create unnecessary overhead. But thanks to the capabilities of the Windows Scripting interface (the Script control is only a wrapper built around those interfaces) there is another option. The Script control exposes a method called AddCode that allows you to import into the scripting context any piece of valid script code. In this way, you can import external files with handmade procedures, and you can also define some global variables, like this:
m_objScriptCtl.AddCode "X_OFFLINE = True"
      Given this, checking the working mode is as easy as using the following lines of code:
<%
if X_OFFLINE then
    Response.Write "Working offline"
else
    Response.Write "Working online"
end if
%>
When you implement this approach, you no longer need the ServerVariables collection.

Adding Custom COM Objects

      To inject the simulated Response and the Request objects into the script, use the AddObject method of the Script control like I did here.
Set m_objRequest = CreateObject("MyASP2.Request")
m_objScriptCtl.AddObject "Request", m_objRequest
      Likewise, you can add all the custom objects you want and make them natively available to the script. For example, suppose you want to extend the object model with the FileSystemObject component. With a couple of lines of code, you're finished.
Set m_objFSO = CreateObject("Scripting.FileSystemObject")
m_objScriptCtl.AddObject "FileSystem", m_objFSO
      You can choose any name you want to associate with the object's instance. In this case, FileSystem becomes a sort of keyword just like Response or Request. Figure 13 illustrates how you can use FileSystem to produce the result shown in Figure 14 (offline use) and Figure 15 (online use).

Figure 14 Offline View of FileSystem
Figure 14 Offline View of FileSystem

Figure 15 Online View of FileSystem
Figure 15 Online View of FileSystem

The output differs when viewed in the offline browser and Microsoft Internet Explorer. Note the code necessary to enumerate the local drives:
for each d in FileSystem.Drives
    Response.Write d.VolumeName & "<br>"
next
FileSystem is now a native component of the ASP object model that is used on the client side.

ASP COM Components

      The final point I'd like to discuss concerns the use of ASP COM components. An ASP COM object is a COM component that can internally access the ASP intrinsic objects. Such a component must link directly to a type library that exposes the original IIS ASP objects.
      Normally, in Visual Basic you early-bind to the Microsoft Active Server Pages Object Library (asp.dll) and then use code like this:
Dim cxt As COMSVCS.ObjectContext
Set cxt = GetObjectContext()
Set oResponse = cxt("Response")
      This is a specific solution that works great with IIS 5.0 and ASP 3.0, but a similar approach using the ScriptingContext object has been available since IIS 3.0.
      Once you hold a reference to an ASP intrinsic object (say, the Response object), you start using it from within the Visual Basic component. The ASP page is simply invoking a method of a COM object and knows nothing about the internal behavior of that object. In turn, such an object goes to work within IIS and the COM+ environment and takes advantage of this.
      The problem arises when a page like this is processed in an offline manner outside of the IIS native context. Pages that utilize ASP COM components cannot work offline if the component itself is early-binding to the ASP type library. There are two possible ways to work around this problem. First, you could modify the component and make it detect (or be notified) whether the hosting page is viewed online or offline. An alternative approach is to design the ASP COM components to receive the reference to the various ASP objects through a method. In this case, you pass the component the Response or the Request object you want it to use. This automatically makes the underlying working mode transparent for the object. For example:
<%
Set obj = CreateObject("MyComp.Foo")
obj.SetResponse Response
obj.PrintOut 
%>
There's only one little drawback: you can't use the early-binding!

Conclusion

      This month's source code contains the updated project for the ASP offline browser and the Visual Basic implementation of the Request object plus a bunch of sample ASP pages. To conclude this two-part series, let me recap the main points. Creating a client-side environment for ASP pages means writing a slightly customized version of the browser that handles navigation to local ASP pages as well as form submissions. The browser must be able to parse the ASP source code and process the script code block in a scripting namespace where valid instances of Request and Response objects are available. You can inject these simulated objects (and execute the script code) thanks to the services of the Script Control. Taking advantage of its programming interface, you can define new environment variables and extend the ASP object model with new objects. A typical example of this would be accessing the registry rather than a LDAP directory to authenticate a certain user. Once you've taken some extra measures to manage form submissions and the use of ASP COM components, there's nothing that prevents you from employing the same type of ASP technology to deliver content on the Web and on local media such as a CD.
Dino Esposito is a senior trainer and consultant based in Rome. He has recently written Windows Script Host Programmer's Reference (WROX, 1999). You can reach Dino at desposito@vb2themax.com.

From the October 2000 issue of MSDN Magazine.

Page view tracker