Adding VBScript to Your VB Applications
Overview
By now, it should be clear that VBScript is useful in many contexts within Windows. Not surprisingly, along with a variety of different technologies, Microsoft provides yet another component capable of supporting VBScript-the Script Control. This ActiveX control provides a simple way for your application written in Visual Basic (or any other language that supports ActiveX controls) to host its own scripting environment, allowing you, or your users, to customize the application.
In the past, programmers had to struggle to provide customizability to their projects, or pay license fees for other products such as Microsoft's portable VB variant, Visual Basic for Applications (VBA). In 1997, Microsoft released Windows Script Interfaces (WSI) as an interface to scripting engines, and eventually followed up with Script Control. Although WSI provides greater control over how your application interfaces with a scripting engine, WSI was intended for C++ programmers. The Script Control, on the other hand, is tailor made for Visual Basic (VB).
Why Add Scripting to Your Application?
Allowing customization of your application through scripting can open many opportunities-not only to you, the programmer (allowing you to change or customize your application's behavior without having to recompile and redistribute), but also to your end users, who will be able to do more with your application. The possibilities of scripting are almost endless, but, as usual, adding this capability to your application will require additional time and effort for design, coding, testing.
Note |
This chapter assumes that you are familiar with the fundamentals of using the Visual Basic language and its Integrated Development Environment (IDE) to create executables and DLLs, including the use of external libraries. |
Adding scripting support to your application is most appropriate when you want to allow customization of the application from within-as opposed to from without. Adding customization from without is often what you really want. For example, consider Microsoft Excel, which offers customization through scripting both from within and from without. Customization of Excel from without comes from the fact that Excel exposes a public, COM-based programming interface that programmers in any other COM-enabled language (like Visual Basic or VBScript) can code against, all without even starting the Excel graphical interface. From within, however, you can add macros and VBA code that add customizations to the behavior of Excel from within Excel itself.
In your Visual Basic application, you can accomplish customization from without simply by creating ActiveX DLLs or EXEs that expose published interfaces. This technique does not necessarily require any additional coding or testing time, though you do need to design your program and segregate your code in a particular way in order to do it right. For many applications, this level of customization is all that is needed.
However, you can also, like Excel, offer customization from within by using the Script Control. There are different approaches to this, and you have to design the internals of your program (at least the parts you want to expose to scripting) a little differently than you are used to. In this chapter we will discuss these techniques and introduce a freely downloadable sample Visual Basic projects that implements the Script Control.
Macro and Scripting Concepts
Before we dig into the details of the Script Control, it is helpful to conceptualize how the Script Control can be used inside an application. There are two approaches: first, using externally stored 'scriptlets' that are executed at certain times for very targeted purposes; and second, exposing whole sections of an application to customization and automation through scripting. In both approaches, you have the option of sharing some or all your application's internal object model with your application's hosted scripts.
Both approaches are similar in that your application gives up control over some portion of its logic or functionality to scripts that are loaded at runtime as opposed to being compiled into your application. They are different primarily in terms of scope. Let's look at two examples.
As an example of the smaller scope approach, imagine an application that has a complex algorithm coded as a series of steps spread across several procedures and/or classes. Let's say this complex algorithm computes the amounts of a series of invoices over time, calculated based on a series of subformulas that have different inputs and outputs depending on the portion of the total invoice amount being computed. Lets say there are 10 subformulas required to compute the amount of one month's invoice. Imagine that nine of these formulas are static, meaning that they are the same in all cases and can therefore be hard coded.
However, one of the formulas is different based on the type of invoice. Sometimes the formula must work one way, sometimes another. Also, the company paying for this application adds new types of invoices all the time, each with unique requirements for this tenth formula. The company wants to be able to add new invoice types without having to add unique, hard-coded versions of this formula for each invoice type. They don't want to have to redeploy the application each time a new invoice type is added.
The application designers decide to use the Script Control to solve this quandary . They add a column called ForumlaScript to the InvoiceTypes table in the database. In this column they store 'scriptlets,' written in VBScript, that compute the result for this tenth subformula in a unique way for each invoice type. Each scriptlet is different based on the requirements for each invoice type. When the invoice amount calculation algorithm reaches the step for the tenth subformula it loads the appropriate scriptlet from the InvoiceType table and uses the Script Control to dynamically execute the scriptlet.
As you can see, in this first example, the application uses scripting capability for a very targeted purpose, adding just the necessary amount of customizability . In a larger scope example, a whole section of the application might be opened up for scripting and automation. The scripts used by the application might be more complex, like the Windows Script Host (WSH) and ASP scripts we've been looking at in this book. The scripts could have access to the entire object model of the hosting application, much in the way that the WSH and ASP engines expose objects like WScript and Server to the scripts they host.
This larger scope approach might be used to allow users to write their own macros to control the user interface of the application. Or the application might expose an object model that allows users to write and plug in their own scripts to generate reports .
Large scope or small scope, the possibilities are endless, and an application you are tasked with designing may or may not have a use for the Script Control at all. If you are trying to add customizability for your end users, then the Script Control may indeed be the best thing because you probably want to allow your users to create and edit scripts from within your application, and you have no control over how or which users to choose to add scripting to the application. Or maybe your application is deployed at the customer's site, and your deployment consultants (who may not be expert programmers) need the flexibility to customize the application on the spot, right when they are in the customer's office installing the application.
However, if you are trying to achieve a design that simply allows for 'plug in' components to ease deployment of bug fixes and new features into your production environment, upon further consideration of your design options, and perhaps a little research, you may decide that an object-oriented polymorphic design that utilizes interfaces and the class factory design pattern might be a better, more stable, and more predictable alternative.
That's a mouthful. Unfortunately, there is no room here to explain what we exactly mean by 'an object-oriented polymorphic design that utilizes interfaces and the class factory design pattern.' The point is that the Script Control is not the solution to every design problem. Consider carefully why you are thinking of using the Script Control. What requirements exactly are you trying to implement? You may find that another solution that achieves the same thing without some of the downsides of the Script Control, namely the fact that script code will almost always run slower than you application's native compiled code and that you do not have any control over syntax errors and poor programming practices that people might insert into the scripts.
Adding the Script Control to a VB Application
If you have not alreadydownload the Script Control you can download it from this address on the Microsoft Web site: http://msdn.microsoft.com/downloads/list/webdev.asp . The installation program automatically adds the control to your machine and registers it.
When you are ready (you might want to read ahead some first, and perhaps take some time to go through this chapter's downloadable sample project), the Script Control can be easily added to a VB project as an ActiveX control (attached to a form) or as a normal COM object that you declare and instantiate in code. In the former case, you would add the control to a form as normal from the Component Toolbox in the VB IDE. In the latter case, you would instead add a reference to the Script Control using the Project > References menu.
When you attach the control to a form, it is an invisible control. That means it does not add any visual properties to your form, much like the native Timer control you may have used before. Unlike the Timer control, which can only be used as a control attached to a form, the Script Control is not limited in this way. In fact, using the Script Control as an object rather than as a control attached to a form offers more flexibility, as demonstrated by the sample project discussed later in the chapter.
Script Control Reference
This middle part of the chapter is a complete reference for the Script Control, including the objects, collections, properties, methods , method syntax, and examples. After the reference, the chapter continues with some additional explanation, followed by an example Visual Basic application that makes use of the Script Control.
Object Model
The Script Control object model is illustrated in Figure 18-1. The details of these objects and their properties and methods are documented in the upcoming sections.
Figure 18-1
Objects and Collections
The Script Control component has several objects and collections (which are a special kind of multivalued object) that work together to provide a wide range of capabilities for adding scripts to a Visual Basic application. For each object and collection, we will describe the object in general; document its properties, methods, and events; and, where appropriate, provide example code.
ScriptControl Object
ScriptControl is the main element that enables scripting in an application. It provides a simple interface for hosting scripting engines such as VBScript or JScript. All of the other available objects depend on an instance of ScriptControl . ScriptControl can be instantiated in three different ways:
- Early bound, on a form (add it through the Components dialog)
- Early bound, through code (add it through the References dialog)
- Late bound (at any time)
Declaration Syntax
This is the syntax to declare an early bound variable for the ScriptControl object (this is preferred over the late bound syntax.).
DimPrivatePublic [WithEvents] objSC As [MSScriptControl.]ScriptControl
This is the syntax to declare a late bound variable for the ScriptControl object. A late bound variable will not be able to handle events. If using a late bound variable, the Script Control does not need to be referenced in the project.
DimPrivatePublic objSC [As ObjectVariant]
Properties
The properties of the ScriptControl object are described in the following table.
Name | Accepts/Returns | Access | Description |
AllowUI |
Boolean |
Read/Write |
Sets or returns the value indicating whether or not visual elements such as MsgBox or InputBox can be displayed. When this is set to False , the only way to communicate visually with the user is directly through the hosting application |
Error |
Error object |
Read-only |
Returns a reference to the Error object for a ScriptControl instance. |
Language |
String |
Read/Write |
Sets or returns the name of the scripting language used by the ScriptControl object. 'VBScript' and 'JScript' are natively supported. If other compatible scripting languages are installed, the name of other scripting languages can be used as well. Setting this property resets all other members of the ScriptControl and its child objects |
Modules |
Modules object |
Read-only |
Returns a reference to the Modules collection of the ScriptControl object |
Name |
String |
Read-only |
If the ScriptControl object is attached to a form, this property returns the name assigned to the control on its properties page |
Procedures |
Procedures object |
Read-only |
Returns a reference to the Procedures collection of the default 'Global' Module object. To access the Procedures collection of other modules, access the module directly through the Modules collection and use the Module.Procedures property |
SitehWnd |
Long |
Read/Write |
Sets or returns the 'handle' to the parent window used by the executing code. When the Script Control is used as an ActiveX Control, placed on a form, the default value of SitehWnd is the hWnd property of the container of the control. Otherwise, when the Script Control is used as an object (not attached to a form), SitehWnd is always 0, which corresponds to the Desktop. This property may impact which window (or control) has UI control over the scripted UI elements. You may change this property to make the Script Control dependent upon a specific window rather than, in some cases, the Desktop (for example, you might want the Script Control to freeze a part of your application and not the Desktop). Circumstances under which you would need to change this property should be rare |
State |
Long (States) |
Read/Write |
Sets or returns the mode of the ScriptControl object; uses the States enumerated constant. When this value is set to Connected (1) , the ScriptControl will be able to sink events generated by objects added using the AddObject method. Thus, changing the state gives you some control over the handling of events |
TimeOut |
Long |
Read/Write |
Sets or returns a number representing time in milliseconds , indicating how long the ScriptControl object will wait before aborting a script that is taking a long time. This property can be set to a constant NoTimeout ( -1 ) which removes time restrictions placed on the execution of script code; turning off the timeout can be dangerous, however, because someone might, for instance, create a script that contains an endless loop. The default value is 10,000 milliseconds (10 seconds). When the timeout expires , a Timeout event may occur (depending on whether or not the ScriptControl can handle events), and at that time, if the ScriptControl has the AllowUI property enabled, the user is alerted with a dialog box, permitting the user to continue execution of the script. Otherwise, the script is terminated and an error is raised. When this property is set to 0 (not recommended), a Timeout event occurs as soon as the script stops Windows messaging for slightly more than 100 milliseconds |
UseSafeSubset |
Boolean |
Read/Write |
Sets or returns a Boolean value indicating whether or not the Script Control may run components that are not marked as 'Safe for Scripting.' For example, a script may try to use the scripting runtime FileSystemObject , which is not 'Safe for Scripting' since it allows access to the file system. You may set this property to True when you are concerned about the ability of the script to create damage on the client computer. When the Script Control is used in a host that requires that components are 'Safe for Scripting' (such as Internet Explorer), this property defaults to True and is read-only |
Methods
The methods of the ScriptControl object are described in the following table.
Name | Arguments | Returns | Description |
AddCode |
code - String value representing a valid script |
N/A |
This is the primary method of adding script to the Script Control. When called on the ScriptControl object, automatically adds the new code to the default 'Global' module. Calls on an individual Module object to add code to a particular module. When adding code for entire procedures and blocks of code, the code must be added in a single call to the AddCode method. Each statement in the block can be separated by colons (:) or the line break characters vbNewLine (preferred), vbCr , vbLf , and vbCrLf |
AddObject |
name Unique String name for the object being added |
||
object -Any object within the scope of your application |
|||
addmembers -Optional Boolean value indicating whether or not the object's public members should be accessible to the ScriptControl object and its scripts |
N/A |
Allows the script to access the host's runtime object model as exposed by the object(s) added through this method. Objects added to the ScriptControl are available globally within the ScriptControl object. The optional addmembers parameter indicates whether or not the members of the added object are also available to the scripts within |
|
Eval |
expression -A String value representing a valid 'expression,' meaning any script fragment that can be compiled and executed |
The output of the expression, if any |
Evaluates an expression. Similar to Eval function in VBScript. This is one of the best ways to evaluate dynamic expressions provided by the user. When comparing Eval to the ExecuteStatement method, be aware that the '=' operator will be treated as a comparison operator when used with Eval , but as an assignment operator when used with ExecuteStatement . Hence, x = y will evaluate to a Boolean subtype when used with Eval , but when used with ExecuteStatement , the value of y will be assigned to variable x , and nothing will be returned. The expression evaluated can take advantage of any members within scope of the Module or ScriptControl object |
Execute Statement |
statement - String value representing the statement to executed |
N/A |
Unlike the Eval method, ExecuteStatement only executes a statement and does not return any value. When comparing Eval to the ExecuteStatement method, be aware that the '=' operator will be treated as a comparison operator when used with Eval , but as an assignment operator when used with ExecuteStatement . Hence, x = y will evaluate to a Boolean subtype when used with Eval , but when used with ExecuteStatement , the value of y will be assigned to variable x , and nothing will be returned. The statement executed can take advantage of any members within scope of the Module or ScriptControl object. In order to obtain a return value from a procedure, you should use either the Eval or Run methods |
Reset |
None |
N/A |
Discards all the members and child objects of the ScriptControl object and initializes them to their default state. When the Reset method is called, the State property is reset to Initialized (0) |
Run |
procedurename - String name of the procedure or function to run |
||
paramarray() -optional array of parameter values, as accepted by the procedure or function |
If a function is called, returns the return value of that function |
When called on the ScriptControl object, attempts to run the named procedure or function in the default 'Global' module. When called on a Module object, attempts to run the named procedure or function within that module. Alternatively, you can use the CodeObject property call procedures and functions directly |
Events
The events of the ScriptControl object are described in the following table.
Name | Arguments | Description |
Error |
None |
Occurs when the ScriptObject encounters an error while running a script. In order to receive notification of this event, you must declare the ScriptControl object variable 'early bound' and using the WithEventskeyword |
Timeout |
None |
Occurs when script execution exceeds the time allotted in the Timeout property, and the user decides to stop the execution of the script. When several ScriptControl objects are present, a Timeout event will occur only for the first ScriptControl object to timeout |
Examples
This line of code shows how to instantiate a new early bound Script Control object.
Set objSC = New [MSScriptControl.]ScriptControl
This line of code shows how to instantiate a new late bound Script Control object.
Set objSC = CreateObject("[MSScriptControl.]ScriptControl")
This script fragment shows how variables and procedures that are outside the scope of a procedure or function can be added in separate steps using the AddCode method. Entire procedures and functions should be added in one call.
strCode = "Option Explicit" & vbNewLine & vbNewLine objSC.AddCode strCode strCode = "Dim x, y" & vbNewLine & vbNewLine objSC.AddCode strCode strCode = "x = 15" & vbNewLine & "y = 2" objSC.AddCode strCode strCode = "Function MultiplyXY(): MultiplyXY = x * y : End Function" objSC.AddCode strCode
The Eval function allows you to execute code fragments at runtime. Eval is simple but effective, capable of achieving tasks nearly impossible in VB.
MsgBox objSC.Eval(InputBox$( _ "Enter Numeric Expression", _ "Eval Example", "5 * 3 - 1"))
Depending upon the type of a procedure, you may call the Run method in several different ways, depending on return values and parameters.
strCode = "Sub TwoArg(a,b): MsgBox CInt(a + b)" & " : End Sub" objSC.AddCode strCode objSC.Run "TwoArg", 1, 2 strCode = "Function ManyArg(a,b,c,d): ManyArg = a * b + c - d" strCode = strCode & ": End Function" objSC.AddCode strCode lngResult = objSC.Run("ManyArg", 1, 2, 3, 4)
The following script fragment illustrates the use of the Error event.
Private WithEvents objSC As ScriptControl Private Sub Main() Set objSC = New ScriptControl ... objSC.Run "MyProc" End Sub Private Sub objSC_Error MsgBox "Script error occurred:" & vbNewLine & _ "Number: " & objSC.Error.Number & vbNewLine & _ "Description: " & objSC.Error.Description & vbNewLine & _ "Line: " & objSC.Error.Line & vbNewLine & _ "Column: " & objSC.Error.Column & vbNewLine & _ "Script Text: " & objSC.Error.Text End Sub
The script following fragment illustrates the use of the Timeout event.
Private WithEvents objSC As ScriptControl Private Sub Main() Set objSC = New ScriptControl ... objSC.Timeout = 10000 objSC.Run "MyProc" End Sub Private Sub objSC_TimeOut MsgBox "The script has timed out." End Sub
Module Object
The Module object, a member of the Modules collection (see below), contains procedure, type, and data declarations used in a script. The Script Control has a default Global module, which is automatically used unless specific member calls are made to other modules that have been added. You can add code to a Module object using the AddCode method. Individual Module objects, on the other hand, are added by using the Add method of the Modules collection. Since the code in each module is private in scope to its module, you can repeat variable and procedure names across modules. This is useful when you have several similar scripts that are only partially different from each other.
Declaration Syntax
This is the syntax to declare a variable for a Module object.
DimPrivatePublic objModule [As [MSScriptControl.]ModuleObject]
Properties
The properties of the Module object are described in the following table.
Name | Returns | Access | Description |
CodeObject |
Object |
Read-only |
Returns an object that can be used to call the public procedures and functions in a Module object. This is a late bound object, but it is useful in that it allows direct calls to procedures in the script without using the Run method. Procedures and functions in the module will appear as public methods of the object returned by this property |
Name |
String |
Read/Write |
The logical name of a Module object. Also used as its Modules collection key, so must be unique within the Modules collection. If you add another Module object of the same name to the collection, the new object will overwrite the original object |
Procedures |
Procedures object |
Read-only |
Returns a reference to the Procedures collection of a Module object |
Methods
The methods of the Module object are described in the following table.
Name | Arguments | Returns | Description |
Eval |
it expression -A String value representing a valid 'expression,' meaning any script fragment that can be compiled and executed |
The output of the expression, if any |
Evaluates an expression. Similar to Eval function in VBScript. This is one of the best ways to evaluate dynamic expressions provided by the user. When comparing Eval to the ExecuteStatement method, you should be aware that the '=' will be treated as a comparison operator when used with Eval , but as an assignment operator when used with ExecuteStatement . Hence, x = y will evaluate to a Boolean subtype when used with Eval , but when used with ExecuteStatement , the value of y will be assigned to variable x , and nothing will be returned. The Eval method may be used against the ScriptControl or Module object, and take advantage of its members |
AddCode |
code - String value representing a valid script |
N/A |
This is the primary method of adding script to the Script Control. When called on the ScriptControl object, automatically adds the new code to the default 'Global' module. Call on an individual Module object to add code to a particular module. When adding code for entire procedures and blocks of code, the code must be added in a single call to the AddCode method. Each statement in the block can be separated by colons (:) or the line break characters vbNewLine (preferred), vbCr , vbLf , and vbCrLf |
Execute Statement |
statement - String value representing the statement to executed |
N/A |
Unlike the Eval method, ExecuteStatement only executes a statement and does not return any value. When comparing Eval to the ExecuteStatement method, be aware that the '=' operator will be treated as a comparison operator when used with Eval , but as an assignment operator when used with ExecuteStatement . Hence, x = y will evaluate to a Boolean subtype when used with Eval , but when used with ExecuteStatement , the value of y will be assigned to variable x , and nothing will be returned. The statement executed can take advantage of any members within scope of the Module or ScriptControl object. In order to obtain a return value from a procedure, you should use either the Eval or Run methods |
Run |
procedurename - String name of the procedure or function to run |
||
paramarray() -optional array of parameter values, as accepted by the procedure or function |
If a function is called, returns the return value of that function |
When called on the ScriptControl object, attempts to run the named procedure or function in the default 'Global' module. When called on a Module object, attempts to run the named procedure or function within that module. Alternatively, you can use the CodeObject property call procedures and functions directly |
Examples
The script following fragment illustrates the use of the Module.Run method to call a procedure contained in a module.
Set objModule = objSC.Modules.Add("NewModule") objModule.AddCode "Sub Test(): " & _ vbNewLine & vbTab & "MsgBox ""Hello, world.""" & _ vbNewLine & "End Sub" objModule.Run "Test"
The following script fragment illustrates the use of the Module.CodeObject to call code within the module. You may find that calling procedures this way is more natural, and perhaps more readable than using the ScriptControl.Run method since procedures are exposed as methods of the CodeObject object.
Set objModule = objSC.Modules.Add("TestMod") objModule.AddCode "Sub TestProc(): " & _ vbNewLine & vbTab & "MsgBox ""Hello, world.""" & _ vbNewLine & "End Sub" objModule.AddCode "Function TestFunction(a) : & _ vbNewLine & vbTab & "TestFunction = a * a " & _ vbNewLine & "End Function" Set objCodeObject = objModule.CodeObject objCodeObject.TestProc lngVal = objCodeObject.TestFunction(2)
This script fragment shows how variables and procedures that are outside the scope of a procedure or function can be added in separate steps. Entire procedures and functions should be added in one call.
strCode = "Option Explicit" & vbNewLine & vbNewLine objModule.AddCode strCode strCode = "Dim x, y" & vbNewLine & vbNewLine objModule.AddCode strCode strCode = "x = 15" & vbNewLine & "y = 2" objModule.AddCode strCode strCode = "Function MultiplyXY(): MultiplyXY = x * y : End Function" objModule.AddCode strCode
The Eval function allows you to execute code fragments at runtime. Eval is simple but effective, capable of achieving tasks nearly impossible in VB.
MsgBox objSC.Eval(InputBox$( _ "Enter Numeric Expression", _ "Eval Example", "5 * 3 - 1"))
Depending upon the type of a procedure, you may call the Run method in several different ways, depending on return values and parameters.
strCode = "Sub TwoArg(a,b): MsgBox CInt(a + b)" & " : End Sub" objSC.AddCode strCode objSC.Run "TwoArg", 1, 2 strCode = "Function ManyArg(a,b,c,d): ManyArg = a * b + c - d" strCode = strCode & ": End Function" objSC.AddCode strCode lngResult = objSC.Run("ManyArg", 1, 2, 3, 4)
Modules Collection
The Modules collection contains all the Module objects for a ScriptControl object, including the default Global module. Calls to the members of the Global module can be made directly through the ScriptControl object without iterating through the Modules collection. It also has an index matching the value of the constant GlobalModule .
Module objects can be added to the Modules collection using the Add method. Specific Module objects can be accessed through the default Modules.Item method. The Count property provides the number of Module objects in the collection. The entire collection can be iterated in various ways, most commonly using the For Each...Next loop. Since there is no way of deleting individual modules, you will have to use the Reset method of the ScriptControl object to delete unwanted modules, which clears the entire collection.
Properties
The single property of the Modules collection object is described in the following table.
Name | Returns | Access | Description |
Count |
Long |
Read-only |
Returns the number of Module objects in the Modules collection |
Methods
The methods of the Modules collection object are described in the following table.
Name | Arguments | Returns | Description |
Add |
name - String value representing the name of the Module object being added; will be used as the Modules collection key |
||
module -optional Module object to be added to the collection |
If module argument omitted, returns new Module object |
Use this method to add a new Module object to the collection of Modules; if your project has a relatively small set of scripts, you may wish to just use the default 'Global' module, but if you have a larger set of scripts, you may find it beneficial to break them up into separate modules- especially if you need to repeat procedures and functions with the same name in different modules |
|
Item |
index -A Long or String value, representing an index or key, respectively |
Returns a Module object from the collection if one matching the index exists |
This is the default property of the collection, so many programmers omit the actual name of the item method: Set objModule = objSC.Modules("MyModule") |
Examples
The following line of code shows how to access the Global module directly. The same syntax, using different module names, can be used with other named modules.
Set objModule = sc.Modules("Global")
The following script fragment illustrates how to iterate through the Modules collection.
For Each objModule In objSC.Modules strModuleList = strModuleList & vbNewLine & objModule.Name Next
Modules allow the use of separate scripts and provide separate namespaces. The following script fragment shows how two different modules can contain scripts with the same name.
' Add code to separate modules, using same sub names. Set objModule = objSC.Modules.Add("Maine") objModule.AddCode "Sub ShowState" & _ vbNewLine & vbTab & "MsgBox ""In Maine""" & _ vbNewLine & "End Sub" Set objModule = Nothing Set objModule = objSC.Modules.Add("Ohio") objModule.AddCode "Sub ShowState" & _ vbNewLine & vbTab & "MsgBox ""In Ohio""" & _ vbNewLine & "End Sub"
Procedure Object
The Procedure object defines a logical unit of code, which in case of VBScript can be either a Sub or a Function . The Procedure object contains a number of useful properties that allow us to inspect a procedure's name, the number of arguments, and whether or not the procedure returns any values. Entry to the script code is also provided via the Procedure object.
Declaration Syntax
This is the syntax to declare a variable for a Procedure object.
DimPrivatePublic objProc [As [MSScriptControl.]ProcedureObject]
Properties
The properties of the Procedure object are described in the following table.
Name | Returns | Access | Description |
HasReturnValue |
Boolean |
Read-only |
Returns whether or not a procedure returns a value (in other words, whether it is a procedure or a function) |
Name |
String |
Read-only |
The name of a Procedure object, which will match the actual name of the procedure or function contained in the object. Also used as the Procedures collection key, so must be unique within the Procedures collection. If you use the AddCode method to add another procedure of the same name to a module, the new procedure will overwrite the original procedure in that module |
NumArgs |
Long |
Read-only |
Returns the number of arguments accepted by a procedure or function in a Procedure object |
Methods
The methods of the Procedure object are described in the following table.
Name | Arguments | Returns | Description |
Item |
index -A Long or String value, representing an index or key, respectively. |
Returns a Procedure object from the collection if one matching the index exists |
This is the default property of the collection, so many programmers omit the actual name of the item method: Set objProc = objMod. Procedures("MyProc") |
Note that the Procedures collection does not have an Add method. New procedures are added to a module using the AddCode method; Procedure objects are created and added to the collection behind the scenes.
Procedures Collection
The Procedures collection holds all of the procedures in a given Module object. It provides a convenient way to iterate through all of the procedures in a module and access the code therein. Individual procedures are added through the Module object's AddCode method, not through the Procedures collection directly. Also, you can't remove an individual procedure once it has been added, as there is no Remove method on the Procedures collection.
Properties
The single property of the Procedures collection object is described in the following table.
Name | Returns | Access | Description |
Count |
Long |
Read-only |
Returns the number of Procedure objects in the Procedures collection |
Methods
The Procedures collection object does not have any methods.
Examples
The following script fragment iterates through the Procedures collection using the For Each loop syntax.
For Each objProcedure In objModule.Procedures strList = strList & "Name: " & objProcedure.Name strList = strList & vbNewLine & vbTab strList = strList & "Argument Count: " & objProcedure.NumArgs strList = strList & vbNewLine & vbTab strList = strList & "Has Return: " & objProcedure.HasReturnValue strList = strList & vbNewLine & vbNewLine Next
Error Object
The Error object provides information about syntax and runtime errors associated with the Script Control. Although information provided by the Error object is similar to that of the Err object in VB and VBScript, there are additional properties ( Column , Text , Line ) that are invaluable when diagnosing problems associated with the script. Although it is possible to declare and initialize the Error object in VB, it is common to access members of the Error object directly through the ScriptControl object.
Unlike the Err object, the Error object is not global in scope and only handles errors associated with a single instance of a ScriptControl object. The Error object is reset each time you change the ScriptControl.Language property, or when you call the Reset , AddCode , Eval , ExecuteStatement , or Clear methods of the ScriptControl object. Use the Clear method to explicitly reset the Error object properties. Runtime errors handled internally by the script will not be raised to the application level.
The section called Error Handling with the Script Control provides additional information about error handling strategies. Chapter 6, 'Error Handling and Debugging,' is also a good reference if you need a primer on VBScript error handling.
Properties
The properties of the Erro r object are described in the following table.
Name | Returns | Access | Description |
Column |
Long |
Read-only |
Returns the column number indicating the place where a syntax error occurred while adding script code |
Description |
String |
Read-only |
Returns a description of a script error |
HelpContext |
Long |
Read Only |
If the error raised from a script has a help file available (which is highly unlikely ), this property returns the identifier for the section within the help file that has information about the error |
HelpFile |
String |
Read-only |
If the error raised from a script has a help file available (which is highly unlikely), this property returns the pathname to the help file |
Line |
Long |
Read-only |
Returns the line number indicating the place where a syntax error occurred while adding script code |
Number |
Long |
Read-only |
Returns the error number of a script error |
Source |
String |
Read-only |
Returns the name of the source where a script error occurred |
Text |
String |
Readonly |
Returns a string containing a snippet of code where a script syntax error has occurred. If you allow your users to add or edit scripts from within your application, you can use this property along with Description , Line , and Column to help the user understand how to fix the syntax error. Also useful for debugging scripts |
Methods
The single method of the Error object is described in the following table.
Name | Arguments | Returns | Description |
Clear |
None |
N/A |
Resets all of the properties of the Error object. This method is called implicitly when the ScriptControl.Language property is changed, or when the Reset , AddCode , Eval , or ExecuteStatement methods are called |
Constants
The following named constants and enumerated constants are available to projects with a reference to the Script Control. These constants are globally available within any Visual Basic application that has a reference to the Script Control. For each of the constants, we explain the type, value(s), and where the constant is used.
GlobalModule Named Constant
Type: String
Value: 'Global'
When using the Script Control with scripting engines (like VBScript or Jscript) that support more than one module, use the GlobalModule constant to access the default 'global' module in the ScriptControl.Modules collection.
NoTimeout Named Constant
Type: Long
Value: -1
This constant can be used to set the Timeout property of the ScriptControl object, and prevent the execution from timing out. Please refer to the ScriptControl.Timeout property reference (previous) for more specifics.
ScriptControlState Enumerated Constant
This enumerated constant is intended for use with the ScriptControl.State property. The purpose of the State property is to control how events raised by objects is added to the ScriptControl through the AddObject method. The default value, Initialized ( ), means that the ScriptControl will not respond to events raised by these objects. The other possible value, Connected ( 1 ) means that the ScriptControl will respond to raised events.
Error Handling with the Script Control
Error handling can never be underestimated, especially when dealing with several sources of code. This is especially true for dynamically generated scripts, and user -entered expressions. In order to handle the errors, you may have to work with both VBs Err object and the Script Control's Error object. If you are working with several instances of the Script Control, each will have a separate Error object. When an error occurs, if you have a proper strategy to handle the error, you may always clear the error and continue execution of the program. You should use all possible script error-handling techniques in your scripts (especially the scripts you load from files), and handle them internally as much as possible.
Note |
Depending on VBs settings, your error handlers may not work properly in debug mode (check Break on Unhandled Errors in IDEs General Options tab). In addition, error handlers in script will depend on the Disable Script Debugging option set in Internet Explorer, and on the availability of the debugger. Script errors may automatically invoke the debugger, bypassing your error handling code. Consult Chapter 6 for more information on script debugging. |
Common Errors
The Script Control may raise several types of errors when setting global properties.
Error | Description |
Can't execute; script is running |
An attempt has been made to modify one of ScriptControl object's members while the script is running |
Can't set UseSafeSubset property |
The application hosting the Script Control may force it into safe mode |
Executing script has timed out |
Script execution has ended because it went over the time allotted in the Timeout property |
Language property not set |
Certain properties can only be set after the Language property is set |
Member is not supported by selected scripting engine |
When working with languages other than VBScript or JScript, not all of the properties and methods may be supported |
Object is no longer valid |
When the Script Control is reset (caused by call to the Reset method or change to Language property), objects that have been set previously are released |
These errors can most probably be avoided by careful programming, and should not be a big factor of your error handling strategy. The two cases when errors will be a major nuisance are when adding the scripting code to the Script Control (syntax errors in the script), and when executing it (runtime errors in the script). When an error occurs, you may inspect both the Err and Error objects; however, the Script Control's Error object provides additional information about the nature of the error. The following example shows hypothetical error handling through VB.
Dim strCode As String Dim strValue As String sc.Reset On Error GoTo SyntaxErrorHandler strCode = InputBox("Enter Function (name it Test(a))", _ "Syntax Error Testing", _ "Sub Test(a): MsgBox ""Result: "" & CStr(a*a): End Sub") sc.AddCode strCode On Error GoTo RuntimeErrorHandler strValue = InputBox("Enter a Value for Test function", _ "Runtime Error Testing", _ "test") sc.Run "Test", strValue Exit Sub SyntaxErrorHandler: MsgBox "Error # " & Err.Number & ": " & _ Err.Description, vbCritical, "Syntax Error in Script" Exit Sub RuntimeErrorHandler: MsgBox "Error # " & Err.Number & ": " & _ Err.Description, vbCritical, "Runtime Error in Script"
There are several different ways in which VB can handle errors: through use of On Error GoTo [Label] , and, as in VBScript, through On Error Resume Next and immediate testing of Err.Number . The following example illustrates the use of On Error Resume Next , combined with an inspection of the Err object as well as Script Control's Error object, which provides us with more information.
On Error Resume Next sc.AddCode strCode If Err Then With sc.Error MsgBox "Error # " & .Number & ": " _ & .Description & vbCrLf _ & "At Line: " & .Line & " Column: " & .Column _ & " : " & .Text, vbCritical, "Syntax Error" End With Else MsgBox "No Error, result: " & CStr(sc.Run("Test", _ strValue)) If Err Then With sc.Error MsgBox "Error # " & .Number & ": " _ & .Description & vbCrLf _ & "At Line: " & .Line _ {}, vbCritical, "Runtime Error" End With End If End If
Finally, you may also use two of the events exposed by the ScriptControl object/control, Event and Timeout , to handle some of the errors; however, in some circumstances it may be a nuisance, and the use of the On Error... statement may be preferred because:
- The Timeout event will only occur for the initial ScriptControl object if more than one is in use
- The Script Control either has to be attached to a form, or has to be initialized using the WithEvents keyword, which may not always be desirable
- You may lose the granularity required when executing certain scripts that are likely to cause errors
You should use the Error event when you do not plan on adding any other error-handling script code to your application, as the following example code shows.
Private Sub sc_Error() Dim strMsg As String With sc.Error strMsg = "Script error has occurred:" & vbCrLf & vbCrLf strMsg = strMsg & .Description & vbCrLf strMsg = strMsg & "Line # " & .Line ' Syntax errors have additional properties If InStr(.Source, "compilation") > 0 Then strMsg = strMsg & ", Column# " & .Column strMsg = strMsg & ", Text: " & .Text End If strMsg = strMsg & vbCrLf End With MsgBox strMsg, vbCritical, "Script Error" sc.Error.Clear End Sub
Note that when using the ScriptControl Error event, the event handler is invoked before any On Error... code. Hence, use of both error-handling techniques may produce double error messages and disable any effective error handling.
Debugging
A quick note on debugging scripts hosted by your application using the Script Control: you can debug your native Visual Basic code in the VB IDE. However, in order to debug the code inside of a script, you have to use the freely downloadable Microsoft Script Debugger. When the debugger is installed, any unhandled errors or Stop statements inside of a script will invoke the debugger, just as with any other script. Please read Chapter 6, 'Error Handling and Debugging,' for more information on the Script Debugger and script debugging techniques.
Also, keep in mind that if the debugger is invoked during script execution, the script execution time as it relates to the Timeout property continues to accumulate. In other words, if your Timeout is set to 10,000 milliseconds (10 seconds) and the debugger comes up and the script pauses for more than 10 seconds, the Script Control will bring up the timeout dialog box.
For this reason, while you are debugging, you might want to set the Timeout property to NoTimeout ( -1 ) and then set it to another value when you release to production. A good way to do this is to use a named constant for setting the Timeout value, but control the value of this constant using a conditional compilation flag and the #IFDEF statement, such as the following.
#IFDEF blnDebugging Const TIMEOUT_VAL = -1 #ELSE Const TIMEOUT_VAL = 15000 #ENDIF ...objSC.Timeout = TIMEOUT_VAL ...
Using Encoded Scripts
The Script Control does support encoded scripts (see Chapter 14). There are two things to keep in mind.
First, when setting the Language property for an encoded script, set the property to 'VBScript.Encode' instead of the normal 'VBScript.'
Second, if you are loading an encoded script from a file or database, you may have to use alternative techniques to account for the fact that an encoded script will have a lot of special characters . For loading from a file, you might want to use the scripting runtime TextStream object with the FileSystemObject.OpenTextFile method (see Chapter 7) rather than using native Visual Basic functions to open the file. For storing in and loading from a database, your best bet is to store the script as binary data rather than in a normal Char or VarChar column.
Sample Project
The sample project, ComplexSC , demonstrates the basics of the Script Control, including how an application can share its objects with the script and pass static events-because of this requirement, the project is an ActiveX EXE type.
Tip |
You can download this sample project, and all of the other code in this book, from wrox.com . |
When building database applications that depend on an outside database, we always encounter the problem of feeding the application with the connection string associated with the appropriate database and the appropriate server. Often, this information is retrieved from the system registry, identifying the software author and the application, and then by a custom key:
Sample Registry Path : SOFTWARECompany NameApp Name
Sample Key Name : MyAppConnection
The sample project provides a way to create, store, and edit registry settings for application settings such as connection strings. The Visual Basic form and code in the ComplexSC project are designed to be generic and customized through a script. This means that you can distribute a new script without having to recompile or redistribute the VB application.
Figure 18-2 shows the main form of the ComplexSC project.
The possibilities here are almost endless: by exposing the objects in the application, and passing some of the events to the script, the script can act as a macro and adapt to your needs. There are some idiosyncrasies, especially when it comes to passing events between the form and the script. To make this possible, all of the controls are placed on the form at design time, some of them in control arrays. The script can easily control all of the properties and methods of all controls, but when the control arrays (optional connection string tags and their values) are used, dynamic modification of the form members is simplified. Here, depending on the choice of connection (OLE DB, ODBC, and DSN) we can display different labels and editable values associated with the connection type.
Sharing of the form members is easily achieved through the CShared class, which allows us to share the main form and all of its members with a script (shown below). Although we could expose individual elements as opposed to the entire form, and prevent the script from manipulating any of the elements we want protected, in the case of this application it is simply not necessary.
Figure 18-2
Option Explicit Private m_Form As Form Public Property Get Form() As Object Set Form = m_Form End Property Friend Property Set Form(ByVal newValue As Object) Set m_Form = newValue End Property
What we are doing here is wrapping the form in the CShared class. With the CShared class in place, we need to use the AddObject method of the Script Control to share the form with the script. This is done via the InitScriptControl procedure, which is executed when the form is loaded (called from the Form_Load event handler). We are passing the reference to the VB form, exposed through the CShared .Form property.
Private Sub Form_Load() Set objScript = InitScriptControl(Me) objScript.Run "init" End Sub
The InitScriptControl instantiates the ScriptControl object, loads the script, instantiates the CShared object, and exposes it to the Script Control. Because we set the third parameter of the AddObject method to true, all of the members of the form are shared too.
Function InitScriptControl(frmForm As Form) As ScriptControl Dim objSC As ScriptControl Dim fileName As String, intFnum As Integer Dim objShare As New CShared ' create a new instance of the control Set objSC = New ScriptControl objSC.Language = "VBScript" objSC.AllowUI = True Set objShare.Form = frmForm objSC.AddObject "share", objShare, True ' load the code into the script control fileName = App.Path & " regeditor.scp" intFnum = FreeFile Open fileName For Input As #intFnum objSC.AddCode Input$(LOF(intFnum), intFnum) Close #intFnum ' return to the caller Set InitScriptControl = objSC End Function
After the Script Control is initialized , the Form_Load code calls the Init procedure in the script, which sets up all of the necessary controls on the form. In actuality, some of the controls are pre-set with certain properties (such as background color , enabled, and so on.), while others are initialized by the script by accessing the members of the form exposed by CShared.Form .
Sub Init() Dim i, strTmp Form.Caption = "Connection Registration Manager" strTmp = "This application saves the database connection" strTmp = strTMP & string in the registry. " & vbCrLf Form.lblExplanation = strTmp Form.lblRegistry.Caption = "" ' this information should be reflected in your application ' the standard is to store the registry keys in subhives ' for different companies and projects Form.txtSubpath.Text = "SOFTWARE Company Name App Name " ' finally the name of the key ' you could similarly extend this application so it would ' work like a wizard, and register several keys Form.txtKey.Text = "MyAppConnection" Form.lblRegistry.Caption = "" Form.cmdRegister.Enabled = False Form.cmdProcess.Enabled = True For i = 0 To 5 Form.lblLabel(i).Visible = False Form.txtText(i).Visible = False Next Form.cboCombo.Clear Form.cboCombo.AddItem "OLE DB" Form.cboCombo.AddiTem "ODBC" Form.cboCombo.AddItem "DSN" End Sub
Next, we need to respond to events generated by the application. In this simple case, we simply pass the events as intercepted by the application directly to the script. Hence, our application may have the following events passed to the script.
Private Sub cboCombo_Click() objScript.Run "cboCombo_Click" End Sub Private Sub txtText_KeyPress(Index As Integer, KeyAscii As Integer) KeyAscii = objScript.Run("txtText_KeyPress",Index, KeyAscii) End Sub
As the example shows, we pass the events directly to the script, optionally passing along the parameters generated by the event. Because in certain cases we might want to modify one of the parameters, we should treat the event-handling procedure as a function, which would return the modified value. This is probably the simplest mechanism for modifying such parameters. Although this functionality is not required by our application, the following function inside the script would capitalize each character entered into one of the text boxes.
Function txtText_KeyPress(Index , KeyAscii) txtText_KeyPress = Asc(Ucase(Chr(KeyAscii))) End Function
This approach is a little different from what you'd expect in VB code, because even if we pass the value of KeyAscii by reference (normal VB code would be KeyAscii = Asc(Ucase(Chr(KeyAscii))) ), the script will not update this value back in VB. Hence, we employ a simple work around by turning the event handler from procedure into a function.
It is also possible to override the default event handling, or to provide optional event handling in the script. When the script does not have the member procedure, an error is generated, which provides us with a possibility of either ignoring events or providing default events, in case the script does not have an appropriately named procedure. The following example shows the simplest error trapping, which allows us to create a default event handler. Moreover, when the error handler is disabled (with On Error Resume Next ), the script must contain an appropriately named procedure with the correct number of parameters.
Private Sub cboCombo_Click() On Error Resume Next objScript.Run "cboCombo_Click" If Err = 0 Then Exit Sub ' default event handler goes here ...End Sub
Details of the application lie in the script itself, so rather than copying the entire code listing, the following example only shows partial implementation of the cboCombo_Click procedure within the script. After the key controls are reset, we set up values of the labels and the associated text that would correspond to an OLE DB type connection string.
Sub cboCombo_Click() Dim strComboSelection, strTmp ' Clean Up in case this was pressed already Form.cmdRegister.Enabled = False Form.cmdProcess.Enabled = True Form.lblRegistry.Caption = "" For i = 0 To 5 Form.lblLabel(i).Visible = False Form.txtText(i).Visible = False Next strComboSelection = _ Trim(Form.cboCombo.List(Form.cboCombo.ListIndex)) Select Case strComboSelection Case "OLE DB" For i = 0 To 4 Form.lblLabel(i).Visible = True Form.txtText(i).Visible = True Next Form.lblLabel(0).Caption = "Provider=" Form.lblLabel(1).Caption = "Data Source=" Form.lblLabel(2).Caption = "Initial Catalog=" Form.lblLabel(3).Caption = "User ID=" Form.lblLabel(4).Caption = "Password=" Form.txtText(0).Text = "SQLOLEDB" Form.txtText(1).Text = "DATABOX" Form.txtText(2).Text = "MyAppDB" Form.txtText(3).Text = "Student" Form.txtText(4).Text = "teacher" [...] End Select strTmp = "Please Fill In Remaining Values in the available" strTmp = strTmp & " text boxes. " & vbCrLf strTmp = strTmp & "You may press ""Proceed"" button, or" strTmp = strTmp & " change the connection method again. " strTmp = strTmp & "Leaving User ID empty will leave out" strTmp = strTmp & " user infromation from registry" Form.lblExplanation = strTmp End Sub
The remainder of the application responds to the end-user events, and builds the connection string as required by the core application, enabling and disabling controls, and modifying values on the form, depending on the 'stage.' The last action is actually carried out directly by the application itself; a value is written to the registry based on the string that is stored in one of the labels on the form.
This little application can be further extended to take advantage of several scripts, and provide wizard- like functionality that can easily be scripted.
Summary
The Script Control is a free control provided by Microsoft that enables your application to host a scripting engine. Uses of the Script Control can range from simple dynamic evaluation of expressions, to a full-fledged macro language add-on capable of automating your applications.
This chapter covered the following topics:
- What the Script Control is
- How the Script Control can be a useful addition to your Visual Basic application
- Why you would want to consider using the Script Control (or why not)
- The Script Control object model, including its properties, methods , and events
- Error handling and debugging
- A sample project demonstrating the use of the Script Control