Coding a Custom Object with WSC

In the previous tutorial of this series we used the Windows Component Wizard to create our own custom COM object file. We told the wizard how we wanted it configured and it output the basic XML file for us. Now it’s time to put some code into that file.

Our object exposes some properties and methods, but it doesn’t actually do anything yet.  For that we need to add some scripting.  I’m not going to go into detail about how this code works exactly; you can just cut at paste it into the <script> section.  Make sure that the code is between the <![CDATA[ and the closing ]]>.  This is a method of commenting text in an XML file.  The file will not parse properly if this isn’t present.

Dim FullName

Dim Name

Dim Path

Dim Count

 

Function get_FullName()

   get_FullName = FullName

End Function

 

Function put_FullName(newValue)

   FullName = newValue

End Function

 

Function get_Name()

   get_Name = Right(FullName, Len(FullName) – InStrRev(FullName, ""))

End Function

 

Function get_Path()

   get_Path = Left(FullName, InStrRev(FullName, ""))

End Function

 

Function get_Count()

   get_Count = GetItemCount(FullName)

End Function

First things first. We define a few variables to hold the property values in our script, and then we provide the necessary functions for retrieving and setting those property values.  Notice the special syntax that is used in naming these functions.  They begin with either “get” or “put” followed by an underscore, and then the name of the property as defined in the <public> section from earlier.

The get function is called whenever an outside script requests the value of a property.  The put function is called whenever an outside script passes a value to be assigned to the property.  You may or may not need either or both of these functions, depending upon whether the property is read-only, write-only, or read-write.

{mospagebreak title=Adding code for the object methods} 

Continuing on, I wrote the functions that correspond to the exposed methods.

Sub Open(strFile)

   FullName = strFile

End Sub

 

Sub Create(strFile)

   If objFso.FileExists(strFile) Then objFso.DeleteFile(strFile)

   FullName = strFile

   NewCompressedFolder2 FullName

End Sub

 

Sub Add(strFile, blnKeepOriginal)

   AddFile FullName, strFile, blnKeepOriginal

End Sub

 

Sub AddMultiple(varSource, blnKeepOriginal)

   AddFiles FullName, varSource, blnKeepOriginal

End Sub

 

Sub Extract(strFolder)

   ExtractAll FullName, strFolder

End Sub

Essentially, these are the public functions that are exposed by my object.  Rather than placing all of the relevant code inside of these functions, I prefer to pass it to other internal functions.  I do this because (as you’ll see) my functions require several parameters.  Most of these are predetermined by the various properties, so creating these intermediate functions allows me to expose functions with a much simpler syntax to my users.

Sub NewCompressedFolder(strPath)

   With CreateObject("ADODB.Stream")

       .Open

       .Type = adTypeText

       .WriteText ChrB(&h50) & ChrB(&h4B) & ChrB(&h5) & ChrB(&h6)

       For i = 1 To 18

          .WriteText ChrB(&h0)

       Next

       .SaveToFile strPath, adSaveCreateNotExist

       .Close

       .Open

       .Type = adTypeBinary

       .LoadFromFile strPath

       .Position = 2

       arrBytes = .Read

       .Position = 0

       .SetEOS

       .Write arrBytes

       .SaveToFile strPath, adSaveCreateOverwrite

       .Close

   End With

End Sub

 

Function AddFile(strFolder, strFile, blnKeepOriginal)

   Set objFolder = objShell.NameSpace(strFolder)

   intCount = objFolder.Items.Count

   Select Case CBool(blnKeepOriginal)

       Case True

          objFolder.CopyHere strFile, 1548

       Case False

          objFolder.MoveHere strFile, 1548

   End Select

   Do Until objFolder.Items.Count = intCount + 1

       Sleep 200

       If Not ShellBusy Then Exit Do

   Loop

End Function

 

Function AddFiles(strFolder, varSource, blnKeepOriginal)

   If IsArray(varSource) Then

       For Each strPath In varSource

          AddFile strFolder, strPath, blnKeepOriginal

       Next

   Else

       Set colItems = objShell.NameSpace(strSource).Items

       intCount = .Items.Count

       Select Case CBool(blnKeepOriginal)

          Case True

              objShell.NameSpace(strFolder).CopyHere colItems, 1548

          Case False

              objShell.NameSpace(strFolder).MoveHere colItems, 1548

       End Select

       Do Until objShell.NameSpace(strFolder).Items.Count = intCount + colItems.Count

          Sleep 200

          If Not ShellBusy Then Exit Do

       Loop

   End If

End Function

 

Function ExtractAll(strZipFile, strFolder)

   If Not objFso.FolderExists(strFolder) Then objFso.CreateFolder(strFolder)

   intCount = objShell.NameSpace(strFolder).Items.Count

   Set colItems = objShell.NameSpace(strZipFile).Items

   objShell.NameSpace(strFolder).CopyHere colItems, 1548

   Do Until objShell.NameSpace(strFolder).Items.Count = intCount + colItems.Count

       Sleep 200

       If Not ShellBusy Then Exit Do

   Loop

End Function

 

Function GetItemCount(strZipFile)

   GetItemCount = objShell.NameSpace(strZipFile).Items.Count

End Function

 

Function ShellBusy()

   intStartSize = objFso.GetFile(FullName).Size

   Sleep 200

   ShellBusy = objFso.GetFile(FullName).Size > intStartSize

End Function

 

Sub Sleep(intDuration)

   dblSeconds = intDuration / 1000

   If dblSeconds < 1 Then dblSeconds = 1

   dteStart = Now()

   dteEnd = DateAdd("s", dblSeconds, dteStart)

 

   While dteEnd > Now()

       DoNothing

   Wend

End Sub

 

Sub DoNothing

   ‘While/Wend has quirks when it is empty

End Sub

This is the remaining code for my ZipFolder object.  If you look closely you’ll notice that I’m using the FileSystemObject (objFso) and the Shell Automation Object (objShell) quite frequently throughout my code, but that I haven’t instantiated them anywhere.  No, this isn’t a mistake.

Since these objects are being used globally, I could have easily instantiated them when I created my global variables earlier.  That works perfectly fine.  However, since WSCs provide some more advanced options, I wanted a chance to demonstrate them to you.

Let’s take a look at some ways you can improve the functionality of your component by adding to the XML file.

{mospagebreak title=Adding objects and resources}

Because Windows Script Components need to support COM-enabled applications outside of scripts alone, they have the ability to do things that you’re probably not used to seeing if you’re not an application developer.  I’m not going to get into too much detail here, but I’ll show you a few things that you can use to your advantage.

       </registration>

 

       <object id="objShell" progid="Shell.Application">

       <object id="objFso" progid="Scripting.FileSystemObject"/>

Below my registration section I’m adding two <object> elements.  The <object> element allows me to specify an object that I want to be globally available in my component.  I can provide the name I want to use, as well as specify the Prog ID.  Here I’m just creating my Shell and FileSystem objects.

While using the <object> element makes my object available in the global namespace, that doesn’t provide much benefit over using CreateObject() in my script.  However, I do not have access to the WScript.CreateObject() method in a script component.  This means that objects can only be loaded using VBScript’s internal CreateObject() method.  As you may or may not know, this has a limitation when it comes to event scripting.  VBS’s CreateObject method cannot create an event handle in scripts.  The <object> element can.  All I need to do is set its optional “events” attribute to true and my component will begin handling any events fired by that object.

Below my <object> elements I’m now going to add some object references.  Reference elements are used to create a reference to a resource’s Type Library.

       <reference object="ADODB.Stream"/>

       <reference object="Scripting.FileSystemObject"/>

If you’ve never developed in .Net or some higher-level programming language, you may not understand what resource references are used for.  Many COM objects expose enumerations, or lists of constants, used by the object.  In scripting we’re accustomed to defining these constants in our code because VBScript does not provide built-in support for enumerations.  You’re probably used to writing code like this:

Const ForWriting = 2

Set objFile = objFso.OpenTextFile(strPath, ForWriting, True)

Here we’re defining a constant ForWriting with an integer value of 2 that the FileSystemObject uses when calling the OpenTextFile method.  The fact is that this constant is already defined inside of the FileSystemObject, but VBScript can’t see it.  By providing a reference to the FileSystemObject, I can write this type of code inside of my component:

Set objFile = objFso.OpenTextFile(strPath, ForWriting, True)

Because my component has a reference to the FileSystemObject, it has access to any constants that are exposed by its enumerations.  I can safely use ForWriting without having to assign it first because it’s already been provided for me.

For those of you who only do general scripting, this may cause some problems, since you aren’t familiar with the enumerations presented by the various objects.  You can dig around in the MSDN and get the enumeration lists, but it may or may not be worth your time.  There are some script editors, however, that are capable of using this reference in conjunction with their auto-completion features.  This will allow your script editor to auto-complete properties and methods for objects in the same way that it does for VBScript’s internal methods.  The only editor that I’m aware of, however, that supports listing enumerated constants is SystemScripter 6.0.

{mospagebreak title=Registering and using your component}

With all of the pieces in place, you’re ready to put your new component to the test.  Make sure that you save the latest changes and move it to a directory where you can leave it.  Once a component is registered, it cannot be moved, because the system is going to save its location.

You should be able to register and unregister your component very easily using the context menu entry in My Computer.  Navigate to the directory where you put it, right-click on the WSC file, and choose Register.  If you don’t have an option to register it, you can do it from the command line as follows.

regsvr32 “C:pathtoZipFolder.wsc”

Once your component has been registered, it is immediately available to the system.  You can write a script now that implements your object by instantiating it by its Prog ID.

Set objZipFolder = CreateObject("ZipFolder.WSC")

One of the beauties of using WSCs over compiled code is that now, even though it’s already registered, you can still open the file and make changes without having to unregister and re-register the component.  Changes will take effect immediately!

While you really should always register your components, let’s assume for some reason that you don’t want to.  Maybe you’re in a really restricted environment or only intend to use your component a couple of times.  In any case, there is one little-known method for instantiating a Windows Script Component without having to register it on the host system.

Set objZipFile = GetObject("script:C:componentsZipFolder.wsc")

You can use the GetObject method to instantiate your script component directly.  The magic is the use of the script: protocol.  You will need to provide the full path to the WSC file.  But that’s not all…

Set objZipFile = GetObject("script:ServercomponentsZipFolder.wsc")

…it can be any valid file path—including network paths…

Set objZipFile = GetObject("script:http://www.example.com/pub/ZipFolder.wsc")

…and even web locations!

{mospagebreak title=Protecting your intellectual property} 

Okay, so you’ve taken the time to write your own custom component and you’d like to begin distributing it, right?  There’s just one small problem—you don’t want people jacking your code, do you?  So let’s talk about how you can protect your code before you start passing it around.

If you’ve ever protected your script source in the past, you’re probably familiar with Microsoft’s Script Encoder tool.  It’s a tool that can encode your script’s contents so that it can’t be read.  It’s similar in nature to encryption, but it’s not done on a binary level, and the source file remains a simple text file.  The scripting host is able to read and decipher this encoded script with no problem at all.

So you set out to use the Script Encoder, only to run into an error when you try to encode your WSC file.  It’s not supported?  It’s still just VBS code, there must be a way—and there is.

The problem is that the script encoder can not process XML files.  (This same concept applies when trying to encode WSF jobs as well.)  But there is a viable workaround.  Since the code inside your WSC file will still be interpreted by the same scripting engine, all you need to do is encode your VBScript portion.

So go ahead and open up your WSC file and cut the actual script portion of your component (that’s everything between the <![CDATA[ and the ]]> ), paste it into Notepad or some other text editor, and save it with a .vbs extension.  It doesn’t matter that the code won’t execute properly; it just needs to be saved in a format that the Script Encoder can understand.

Once your script is encoded, you will probably have a file that now has a .vbe extension.  This is the executable VBScript Encoded file type.  Open this file in Notepad and you should see one really long string of unreadable characters.  This is your encoded script.  Copy it from this file and paste it back into your original WSC file between the <![CDATA[ and the ]]> where the original script used to be.  You should end up with something similar to this:

       <script language="VBScript.Encode">

<![CDATA[

#@~^ZxYAAA==9b:~wE^V1m:@&fb:~1m:n@&Gk:,KlDt@&GrhP;W!xO@&9rsPG4NjtVs

]]>

       </script>

Make certain that you copy the encoded file and paste it exactly as it was.  Altering even one character will render it useless.  Your editor may wrap the line on the screen, but the code should be one single continuous line.

Finally, you’ll need to got back to the <script> element and change the language attribute from VBScript to VBScript.Encode (or Jscript.Encode for Jscript).  This change tells the scripting engine that it’s dealing with an encoded script.

That’s all there is to it.  You now have all the tools you need to begin creating and distributing your own custom COM objects without the need for compiled code or expensive development environments.  Have fun with this concept and see what you can come up with.  Until next time, keep coding!

You can download a working example of this component here.

[gp-comments width="770" linklove="off" ]