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.
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:
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")
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:
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
nPos = InStr(1, aspFile, "?")
If nPos > 0 Then
strParams = Right(aspFile, Len(aspFile) - nPos)
' Truncates the URL
aspFile = Left(aspFile, nPos - 1)
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
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 ' objectsSet m_objResponse = _ CreateObject("MyASP.Response")m_objScriptCtl.AddObject "Response", _ m_objResponseSet m_objRequest = CreateObject("MyASP2.Request")m_objScriptCtl.AddObject "Request", m_objRequest
' Populate the script's namespace with fake ASP
Set m_objResponse = _ CreateObject("MyASP.Response")
m_objScriptCtl.AddObject "Response", _
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>
For Each elem In Request.QueryString
Response.Write elem & "=" & _
This code dumps out all the name/value pairs stored in the QueryString collection. When called over HTTP with a query string like
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 BrowserFigure 4 Request.asp in the Offline Browser
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 Collectionc.Add "1", "Code"c.Add "all", "Info"
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"
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
For Each elem In d
MsgBox elem & "=" & d(elem)
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.
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>
<form name="Main" method="post" action="foo.asp">
<input type=text name=MyName></input>
<input type=submit name=MyButton value=Go></input>
the string is
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.parentWindowEnd Sub
Private Sub WB_DocumentComplete(ByVal pDisp As Object,URL As Variant)
Set m_htmlDoc = WB.Document
Set m_htmlWindow = m_htmlDoc.parentWindow
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 HTMLInputButtonElementSet objSrc = _m_htmlWindow.event.srcElement
Dim objSrc As HTMLInputButtonElement
Set objSrc = _
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.
<html><body> <form method="post" action="target.asp"> <input type=text name=MyName> </input> <input type=submit value=Go> </input> </form></body></html>
<input type=text name=MyName>
<input type=submit value=Go>
<%s = Request.Form("MyName")Response.Write "You passed me " & _ "<b>" & s & "</b>"%>
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
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*40Dim nLen As LongGetUserName temp, nLenMsgBox Left(temp, nLen - 1)
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>
For Each elem In Request.ServerVariables
Response.Write "<b>" & elem & "=</b>" & _
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%>
if Request.ServerVariables("OFFLINE") = "yes" then
Response.Write "Working offline"
Response.Write "Working online"
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%>
if X_OFFLINE then
When you implement this approach, you no longer need the ServerVariables collection.
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
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 FileSystemFigure 15 Online View of FileSystemThe 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
for each d in FileSystem.Drives
Response.Write d.VolumeName & "<br>"
FileSystem is now a native component of the ASP object model that is used on the client side.
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.ObjectContextSet cxt = GetObjectContext()Set oResponse = cxt("Response")
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 Responseobj.PrintOut %>
Set obj = CreateObject("MyComp.Foo")
There's only one little drawback: you can't use the early-binding!
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.
From the October 2000 issue of MSDN Magazine.
More MSDN Magazine Blog entries >
Browse All MSDN Magazines
Subscribe to MSDN Flash newsletter
Receive the MSDN Flash e-mail newsletter every other week, with news and information personalized to your interests and areas of focus.