Encapsulating ADO Functions into a Windows Script Component

By Peter A. Bromberg, Ph.D.

Peter Bromberg

Windows Script Components came on the scene around 1998 and have become very popular with web developers, for several good reasons:

1) Easy to write using either VBScript or Javascript
2) Windows scripting host handles all the COM interface details for you automatically.
3) In certain situations WSC's can be instantiated using GetObject even though they have never been registered.
4) Great way to encapsulate frequently -used routines for DHTML, HTML controls, ADO and filesystem function and more - rather than using ASP <!--#include files. Eliminate "spaghetti code"!
5) You can write a component that's smart enough to be able to run either on the server or the client.
6) We can export a TypeLibrary and use the component - complete with Intellisense and Object Browsing - in Visual Basic or any other COM-compliant IDE.
7) Because its textual script, it's easy to add, modify, etc. and re-register the component using nothing more sophisticated than Notepad.
8) Because you can! (e.g., impress your boss and co-workers instead of doing things the same old way every time)

For those who are not familiar with WSC's, don't be intimidated. A WSC is just a text file written to follow a specific XML schema. Each WSC file contains two main blocks of information: a descriptive section and a script section. The descriptive section defines the programming interface of the component. It's completely declarative - here you specify the registration information (CLSID, progID, and version number) and the full set of exposed interfaces with a simple set of XML tags and attributes. By default, a WSC exposes the IDispatch interface for automation; all we need to do is specify the prototype of the various methods, properties, and events.



Rather than launching into a treatise on Windows Script Components here (which is really unecessary because all you need to do is search the MSDN Onlne under "Windows Script Components" and you'll find complete documentation, along with some good articles) what I'm going to do instead is something USEFUL by example. I'll write a Windows Script Component that you can use to do one of the most common things we all do as Windows DNA developers - use ADO to get recordsets and run stored procedures in SQL Server from our ASP pages.

You'll see that the use of the WSC "PAB.ADOFunctions" is very clean and easy, and can save you a bunch of coding headaches. Hopefully, this will give you the basis to put some of your own coding inventions into WSC's and make them available either here or at other sites to share with others.

I"m a big fan of giving examples "Inline" as I code so the logic flows, so let's look at the actual code of the WSC below, and you'll see my comments in red.


<!-- This is an XML-compliant document, so we need to declare it and follow XML-compliant form. This means our
attributes must be surrounded by quotation marks, our tags must all be closed, and our script which contains illegal XML characters should generally appear inside CDATA blocks -->

<?XML version="1.0"?>

<!-- opening required component tag -->
<component>

<!-- registration information. Need to get a unique classid (use uuidgen, or the Windows Script Component Wizard, which autogenerates one for you -->
<registration
progid="PAB.ADOFunctions"
version ="1"
classid="{7851f441-5344-11d2-a42d-00c04f8ec80b}"
remotable="true" >

<!-- here is the first script block with some nice little functions to popup a message box when component is registered / unregistered -->
<script language="VBScript">
<![CDATA[
function register()
MsgBox("Component 'ADOFunctions' is registered!")
end function
function unregister()
MsgBox("Component 'ADOFunctions' is unregistered!")
end function
]]>
</script>
</registration>

<!--beginning of the public interface definition block. This is where we define our COM interface descriptions for the typelibrary -->
<public>

<!-- here I define the only property for the component, a read/write "connect" to set the connection string -->
<property name="connect">
<get/>
<put />
</property>

<!-- Here we define each method in the component and its parameters-->
<method name="GetRecordSet">
<parameter name="strSQL" />
</method>
<method name="RunSP">
<parameter name="strSP" />
<parameter name="params" />
<parameter name="OutArray" />
</method>
<method name="RunSPReturnRS">
<parameter name="strSP" />
<parameter name="params" />
<parameter name="OutArray" />
</method>
<method name="RunSQL">
<parameter name="strSQL" />
<parameter name="params" />
</method>
<method name="RunSQLReturnRS">
<parameter name="strSQL" />
<parameter name="params" />
</method>
<method name="PageRecords">
<parameter name="rs" />
<parameter name="intPage" />
<parameter name="PageSize" />
<parameter name="NumPages" />
<parameter name="NumRecords" />
</method>
<method name="ViewHelp" />
</public>

<!-- here I define a resource which in this case is just some HTML that displays general help when you call the ViewHelp method which you'll see later down-->
<resource id ="hlpGeneral"><![CDATA[
<Basefont face=Verdana>
<TABLE cellspacing="2" cellpadding="2" border="0" width="80%" align="center">
<TR>
<TD bgcolor="#999999">
<h2 align="center"><font size="-1">ADO FUNCTIONS Windows Script Component</font></h2>
</TD>
<TR>
<TR>
<TD bgcolor="#00CCFF"> <b><i><font size="2">USAGE: set oDB = CreateObject("PAB.ADOFunctions")</font></i></b></TD>
</TR>
<TR>
<TD bgcolor="#CCCCCC">
<p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><b>Example:</b><br>
oDB.connect = "Provider=SQLOLEDB.1;Data Source=(local);User ID=sa;Initial
Catalog=NORTHWIND"<BR>
sSQL = "Sales by Year"<BR>
sParams = array(array("@Beginning_Date",adDateTime, AdParamInput, 0, "04/01/1998"),
_<BR>
array("@Ending_Date",adDateTime,AdParamInput,0,"04/30/2001")<BR>
set oRS = oDB.RunSPReturnRS(sSQL, sParams, OutArray)</font></p>
</TD>
</TR>
<TR>
<TD bgcolor="#FFFFCC"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">
The line that sets the sParams variable looks weird because it calls the
Array function twice. <BR>
First, the function sets up the array of parameter arrays; then, each parameter
calls the function once to <BR>
set up its own little subarray of param attributes. The result is an array
of parameters, with each parameter being <BR>
an array of five entries: ParamName, Type, direction, size, value, e.g.:</font></TD>
</TR>
<TR>
<TD bgcolor="#CCCCCC"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">
Set rs = oDB.RunSPReturnRS("sp_BeyondArticlesUpdate", Array( _<BR>
Array("@RETURN_VALUE", adInteger, adParamReturnValue, 0), __<BR>
Array("@ArticleId", adInteger, adParamInput,0,clng(msArticleID)), _<BR>
Array("@InstructorId", adInteger, adParamInput,0,clng(msInstructorID)),
__<BR>
Array("@ArticleCategoryID", adInteger, adParamInput, 0, Clng(msArticleCategoryID)),
_<BR>
Array("@Title", adVarWChar, adParamInput, 100, msTitle), _<BR>
Array("@URL", adVarWChar, adParamInput, 200, msURL), _<BR>
Array("@BodyText", adVarWChar, adParamInput, 1073741823, msBodyText)),outArray)</font></TD>
</TR>
<TR>
<TD bgcolor="#FFFFCC"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Seven
array statements are nested in the first array, and each of the subarrays
sets one parameter.</font></TD>
</TR>
<TR>
<TD bgcolor="#00CCFF">
<div align="left"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">THE
FUNCTIONS:<br>
A:) Set / Get connection string:<BR>
(obj).connect ="your conn string"<BR>
myconnstring = (obj).connect<BR>
1) Run SP with no return recordset and optional output params:<BR>
<i><b>RunSP(ByVal strSP, params, byRef OutArray)</b></i><BR>
2) Run sp with params and return disconnected recordset and optional return
values:<BR>
<i><b>RunSPReturnRS(ByVal strSP, params, byRef OutArray)</b></i><BR>
3) Run prepared SQL text and return recordset:<BR>
<i><b>RunSQLReturnRS(ByVal strSQL,ByVal params)</b></i><BR>
4) Run prepared SQL text with no return recordset:<BR>
<i><b>RunSQL(ByVal strSQL, byRef params)</b></i><BR>
5)Run prepared SQL text and return disconnected recordset<BR>
<i><b>GetRecordset(ByVal strSQL)</b></i><BR>
6) Page large recordset object for display in more than one page:<BR>
<i><b>PageRecords(rs, byRef intPage, byVal PageSize, NumPages, NumRecords)
</b></i></font></div>
</TD>
</TR>
</TABLE>
]]>
</resource>

<!-- That's the end of the interface definitions, now we have our main code block, roughly equivalent to a "Class" in VB-->

<script language="VBScript">
<![CDATA[

' Set the ADO constants we know we'll need
'---- DataTypeEnum Values ----
Const adEmpty = 0
Const adTinyInt = 16
Const adSmallInt = 2
Const adInteger = 3
Const adBigInt = 20
Const adUnsignedTinyInt = 17
Const adUnsignedSmallInt = 18
Const adUnsignedInt = 19
Const adUnsignedBigInt = 21
Const adSingle = 4
Const adDouble = 5
Const adCurrency = 6
Const adDecimal = 14
Const adNumeric = 131
Const adBoolean = 11
Const adError = 10
Const adUserDefined = 132
Const adVariant = 12
Const adIDispatch = 9
Const adIUnknown = 13
Const adGUID = 72
Const adDate = 7
Const adDBDate = 133
Const adDBTime = 134
Const adDBTimeStamp = 135
Const adBSTR = 8
Const adChar = 129
Const adVarChar = 200
Const adLongVarChar = 201
Const adWChar = 130
Const adVarWChar = 202
Const adLongVarWChar = 203
Const adBinary = 128
Const adVarBinary = 204
Const adLongVarBinary = 205
Const adChapter = 136
Const adFileTime = 64
Const adPropVariant = 138
Const adVarNumeric = 139
Const adArray = &H2000
Const adUseClient = 3
Const adOpenForwardOnly = 0
Const adLockReadOnly = 1
Const adCmdStoredProc = &H0004
Const adExecuteNoRecords = &H00000080
Const adOpenStatic = 3
Const adCmdText = &H0001
Const adBookmarkCurrent = 0
Const adParamUnknown = &H0000
Const adParamInput = &H0001
Const adParamOutput = &H0002
Const adParamInputOutput = &H0003
Const adParamReturnValue = &H0004

'Instantiate the connect property variable and set it to something, provide the 'connect function get and put methods. Note the get_connect and put_connect 'syntax
Dim connect
connect = ""
function get_connect()
get_connect = connect
end function

function put_connect(newValue)
connect = newValue
end function

' Code that follows is the same as you would write in VB or an ASP Page. You can ' also use Javascript (which I prefer, but this code was already written in an 'ASP include file I had written)...
' Run prepared SQL text and return disconnected recordset...
Function GetRecordset(ByVal strSQL)
Dim rs
Set rs = CreateObject("adodb.Recordset")
rs.CursorLocation = adUseClient
rs.Open strSQL, get_connect() , adOpenForwardOnly, adLockReadOnly
Set rs.ActiveConnection = Nothing
Set GetRecordset = rs
End Function
' Run SP with no return recordset and optional output params...
Function RunSP(ByVal strSP, params, byRef OutArray)
Dim cmd, OutPutParms
Set cmd = CreateObject("adodb.Command")
cmd.ActiveConnection = Get_connect()
cmd.CommandText = strSP
cmd.CommandType = adCmdStoredProc
collectParams cmd, params, OutPutParms
cmd.Execute , , adExecuteNoRecords
if OutPutParms then OutArray = collectOutputParms(cmd, params)
Set cmd.ActiveConnection = Nothing
set cmd = Nothing
RunSP = 0
End Function
' Run sp with params and return disconnected recordset and optional return values
Function RunSPReturnRS(ByVal strSP, params, byRef OutArray)
Dim rs, cmd, OutPutParms
Set rs = CreateObject("adodb.Recordset")
Set cmd = CreateObject("adodb.Command")
cmd.ActiveConnection = get_connect()
cmd.CommandText = strSP
cmd.CommandType = adCmdStoredProc
collectParams cmd, params, OutPutParms
rs.CursorLocation = adUseClient
rs.Open cmd, , adOpenStatic, adLockReadOnly
if OutPutParms then OutArray = collectOutputParms(cmd, params)
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
Set rs.ActiveConnection = Nothing
Set RunSPReturnRS = rs
End Function
' Run prepared SQL text and return recordset...
Function RunSQLReturnRS(ByVal strSQL,ByVal params)
Dim rs, cmd, OutPutParms
Set rs = CreateObject("adodb.Recordset")
Set cmd = CreateObject("adodb.Command")
cmd.ActiveConnection = get_connect()
cmd.CommandText = strSQL
cmd.CommandType = adCmdText
cmd.Prepared = true
collectParams cmd, params, OutPutParms
rs.CursorLocation = adUseClient
rs.Open cmd, , adOpenForwardOnly, adLockReadOnly
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
Set rs.ActiveConnection = Nothing
Set RunSQLReturnRS = rs
End Function
' Run prepared SQL text with no return recordset
Function RunSQL(ByVal strSQL, byRef params)
Dim cmd, outPutParms
Set cmd = CreateObject("adodb.Command")
cmd.ActiveConnection = get_connect()
cmd.CommandText = strSQL
cmd.CommandType = adCmdText
collectParams cmd, params, OutPutParms
cmd.Execute , , adExecuteNoRecords
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
End Function
' Page large recordset object for display in more than one page ...
Function PageRecords(rs, byRef intPage, byVal PageSize, NumPages, NumRecords)
If intPage < 1 Then intPage = 1
rs.PageSize = PageSize
NumPages = rs.PageCount
If intPage > NumPages Then intPage = NumPages
rs.AbsolutePage = intPage
PageRecords = rs.GetRows(PageSize, adBookmarkCurrent)
If (intPage < NumPages) Or (rs.RecordCount Mod PageSize = 0) Then
NumRecords = PageSize
Else
NumRecords = rs.RecordCount Mod PageSize
End If
End Function
' collect and add input and output params to params object
Private Sub collectParams(ByRef cmd, ByVal argparams, ByRef OutPutParms)
Dim params, v
Dim i, l, u
If Not IsArray(argparams) Then
Exit Sub
end if
OutPutParms = false
params = argparams
For i = LBound(params) To UBound(params)
l = LBound(params(i))
u = UBound(params(i))
If u - l >= 3 Then
If VarType(params(i)(4)) = vbString Then
if params(i)(4) = "" then
v=null
else
v=params(i)(4)
end if
Else
v = params(i)(4)
End If
if params(i)(2) = adParamOutput then OutPutParms = true
cmd.Parameters.Append cmd.CreateParameter(params(i)(0), params(i)(1), params(i)(2), params(i)(3), v)
Else
err.raise "ADO Error", "collectParams(): incorrect number of parameters"
End If
Next
End Sub
' create output params array
Private Function collectOutputParms(ByRef cmd, argparams)
Dim params, v, OutArray(20)
Dim i, l, u
params = argparams
For i = LBound(params) To UBound(params)
OutArray(i) = cmd.Parameters(i).Value
Next
collectOutputParms = OutArray
End Function

' Note the getResource function call to display the help resource block
' You can use this method to display forms, popups, any DHTML
..
Public Function ViewHelp()
ViewHelp=getResource("hlpGeneral")
end Function

'Close our CDATA, script and component tags and we're done!
]]>
</script>
</component>

Now when you save this file and right click on it in WIndows Explorer, you'll be offered the context menu choices to register the component and to generate a Type Library. From this point on you treat it just like any other COM component, e.g.:

(This is client-side code:)

set n = CreateObject("PAB.ADOFunctions")
n.connect = "Provider=SQLOLEDB.1;Data Source=(local);User ID=sa;Initial Catalog=NORTHWIND"
'document.write "Using: " & n.connect & "<BR/>"
document.write n.ViewHelp
Set rs = createobject("ADODB.Recordset")
sSQL = "SalesByCategory"
sParams = Array(Array("@CategoryName", adVarWChar, adParamInput, 15, "Beverages"), Array("@OrdYear", adVarWChar, adParamInput, 4, "1998"))
outArray = Array("@RETURN_VALUE", adInteger, adParamReturnValue, 0)
Set rs = n.RunSPReturnRS(sSQL, sParams, outArray)

... etc.

This short arficle barely scratches the surface of what you can do with Windows Script Components - you can call other components, expose the ASP Server model (Response, Request, etc.) and much more. You can even get limited access to the Windows API (boy, that should get the juices flowing with you hackers). The samples in the download include a fully working copy of the WSC (without the comments), a client -side HTM page, and a server - side ASP page, both in VBScript, to show how to use the component. Bring up a reference to it in VB with CreateObject("PAB.ADOFunctions") and you'll see you can look through it in the Object Browser and it even exposes Intellisense when you go to use one of the functions! Don't forget that if your ASP or HTM page passes in ADO constants, they will need to be defined in advance. My client - side page has an "if" test that instantiates them if you don't already have a reference to the ADO type library in your global.asa, and OBJECT tag, or as an include such as ADOVBS.INC.

And finally, if you look at the code in "TestADOFunctions.asp", it uses the GetObject() directive, which can be used to instantiate a WSC even when it is not registered on the server.   All you need to know is where it is in the filesystem, or use Server.MapPath("ADOFunctions.wsc") if it is in the same folder as the ASP script calling it. The client-side example, TestADOFunctions.htm, show how to use the WSC on the client and provide a great DHTML experience without the mess of posting and reloading the page, something that has, at least for IE developers, well-- really become passé.

Enjoy! And remember - If debugging is the process of finding and removing errors from code, this doesn't mean that programming has to be the process of putting them in!

Download the code that accompanies this article.

Peter Bromberg is an independent consultant specializing in distributed .NET solutionsa Senior Programmer / Analyst at in Orlando and a co-developer of the NullSkull.com developer website. He can be reached at info@eggheadcafe.com