This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
Exploring the Microsoft Script Control
Francesco Balena
|
Adding a macro language to an application used to be a time-consuming task. Today, you can add a script engine to your Windows-based application by dropping a simple control in it.
Of the many new technologies that Microsoft has recently made available to developers, the one that intrigues me the most is Active Scripting. Adding a script engine to a Windows®-based application opens up many great opportunities. Scripts let you customize your software to meet customer needs without recompiling the source code. You can evaluate expressions and queries that are entered by the user at runtime, and that can't be hardcoded in your source code at compile time. You can provide users with the ability to create customized reports containing formulas and select conditions, without the need for a third-party report generator. You may even design your application to deliver a compiled executable to a customer and later customize it with pieces of script code to be executed when an event occursa document is opened, an email is received, a timeout elapses, and so on.
When the first interfaces for hosting script engines in Windows-based applications were released in the first half of 1997, I was a bit disappointed. Using Visual Basic® to develop my apps, I couldn't exploit all the potential of this technology. You had to be a C++ programmerand a good one at thatto encapsulate script engines in your programs. Things changed when Microsoft released version 1.0 of the Script Control. The Script Control is an ActiveX® component that can be used as a form-hosted control or a standalone Automation object, and supports any ActiveX-compatible scripting language. When used as a control, simply placing the Script Control on a Visual Basic form or any other ActiveX-compliant container lets your program execute VBScript and JScript® scripts. You can download the Script Control from http://msdn.microsoft.com/scripting. The control comes with its own documentation, but I soon discovered that it omits many interesting details that I've learned by experience, and that I will discuss in this article. Using Scripts with Visual Basic
Using the Script Control in a Visual Basic-based program is straightforward. After you install the control on your system, you need only add a reference to it in the Components dialog of the Visual Basic design environment. This will cause a new icon to appear on the component toolbox. The Script Control is invisible at runtime and exposes very few properties at design time (see Figure 1). |
Figure 1: Script Control Properties |
The Language property defaults to VBScript, but can be changed to JScript or any other language that supports the ActiveX scripting specifications.
The Timeout property defaults to 10,000 milliseconds; this is the time after which the control raises an error if the script hasn't completed its execution, and is therefore useful to catch logic errors such as endless loops. The AllowUI property defaults to True, which allows the control to show its own message boxes and error messages. The UseSafeSubset property defaults to False, which lets scripts access "unsafe" statementsthat is, script commands that would raise a security warning in Microsoft® Internet Explorer (unless you've lowered its security level). All the default values are fine for experimenting with the control. Just place an instance of the control on a form, add a command button, and use the ExecuteStatement method to carry out a simple task: |
|
Admittedly, this example isn't remarkable because you can do the same thing using standard Visual Basic code. Things become interesting with the Eval function. Can you believe that you can build a powerful calculator with just one line of Visual Basic code? Just try out this statement: |
|
The Script Control can evaluate expressions containing any of the math, string, comparison, and Boolean operators offered by the selected script language. You can exploit this capability to plot functions or manage calculated fields based on expressions typed by the user or read from a data file at runtimeall without having to write a complex expression evaluator from the ground up.
The Script Control really shines when you feed it complete procedures rather than single lines of code. You add the procedures with the AddCode method, then execute the procedures using the Run method. You can exploit this capability to extend your cheap calculator with support for predefined math constants such as pi, as shown in Figure 2. The embedded procedure, EvalExpr, takes no arguments and is executed with the Run method. The Run method can take a list of arguments, if needed. The arguments must match the parameter list of the called procedure. The running program is shown in Figure 3. |
Figure 3: Cheap Calculator in Action |
The code you pass to the AddCode method must comprise one or more procedures. You cannot pass incomplete routines such as a Sub statement that is not properly closed by an End Sub statement. I found that you can also pass a group of executable statements that are outside any procedure; these statements are executed immediately instead of being stored in the internal memory of the control. Error Trapping Strategies Error trapping is important when handling expressions entered by users. In most cases you have no control over the code entered by users at runtime, so your program must handle both syntax errors and runtime errors. You can do this with standard Visual Basic code: |
|
The documentation states that the Visual Basic Err object is not affected by the Script Control's runtime errors, but I found this to be incorrect. In fact, you can get information on the error by testing either the Err object or the Script Control's Error object. The two objects have most properties and methods in commonsuch as Number, Description, and Clearbut the latter also provides information about where the error occurred in the script code (in its Line and Column properties) and the source line that caused the error (in the Text property). For example, you can use the Err object to detect any error, and then use the Error object to retrieve more detailed information: |
|
The Column and Text properties are only meaningful for syntax errors; when a script runtime error occurs they always return zero and a null string, respectively.
If you prefer to catch errors in your Visual Basic-based programs using the On Error Goto line statement (instead of the On Error Resume Next statement as I've used in the samples so far), whenever the Script Control raises an error the control flow jumps to the error handler. In this case it's up to you to understand if you have trapped a syntax error or a runtime error. You can make the distinction by testing the Error object's Source property, which returns "Microsoft VBScript compilation error" and "Microsoft VBScript runtime error," respectively. A third possibility exploits the Script Control's Error event, which enables you to handle errors raised anywhere in a form module: |
|
The regular Err function in Visual Basic always returns zero when invoked from within an Error event procedure (because the error hasn't been reported to Visual Basic yet), so you must use the Script Control's Error object. Dealing with an error in the Script Control's Error event procedure doesn't cancel the error in Visual Basic, so you have to protect the Script Control's AddCode and Run methods with a property On Error statement or the error will terminate the Visual Basic-based application: |
|
If you've installed the Microsoft Script Debugger or Visual InterDev, a script runtime error will cause a message box asking if you want to debug the application to appear. If you accept, you can see which statement causes the error, and watch or change the current value of script variables. You can't modify the running VBScript code, but you can single-step into it or jump over the section of code that causes the error.
You should stay clear of the End command. It kills the application that is hosting the Script Control; in other words, it terminates the Visual Basic IDE without giving you the opportunity to save the project under development. If you want to end execution of the script, use the Detach All Processes command in the Debug menu orif you're debugging more than one processinvoke the Processes command and select the individual process to be detached. The capability to trace a piece of script code is so useful that VBScript includes a special Stop command, which invokes a script debugger if one has been installed in the system. (The corresponding JScript command is Debugger.) This is a bit like adding persistent breakpoints to the script code, so remember to delete them before delivering the script to your customers. While the debugger is active, the main application is frozen. After a few seconds (10 seconds is the default timeout period), the Script Control will complain, stating that the script is taking too long, and will ask if you want to terminate it. You don't really have to reply to this message box, so you can simply leave it in the background. Alternatively, you can prevent the Script Control from showing this message by setting its Timeout property to -1 (this is the value of the NoTimeout symbolic constant). However, I've found that setting this value in the Properties window at design time doesn't work in Visual Basic 5.0 and corrupts the main program in Visual Basic 6.0. The only reliable solution is to add the following line of code before running the script: |
|
Extending the Script Language A Visual Basic-based application can share data with the Script Control in a very efficient manner thanks to the AddObject method, whose purpose is to expose an object created by your application to the Script Control. For instance, if you want the script to share a couple of values with your program, you need to create a class module named SharedClass: |
|
Then you create an instance of this class in your Visual Basic code and pass the Script Control a reference to it: |
|
The first argument of the AddObject method is the name that will be used in the script code to reference the object. From this point on, the script can access the same instance of the class owned by your program, and can read and modify the values of its properties.
For example, the following code shows how you can pass a variable to the script (k in this case), and retrieve a value set by the script (result). |
|
Notice that the EvalFN function requires one argument, and you have to provide it in the Run method. The shared object is accessed by the script using the sh name, but you may use the same name for the object in both the program and the script. This approach improves the readability of script code and can ease the migration of code from a Visual Basic-based program to a VBScript routine, and vice versa.
You aren't limited to sharing properties. In fact, the AddObject method provides an effective way for your scripts to call back into your program. You do it by adding Subs and Functions to your shared class: |
|
Thus the script can invoke a routine in your app as a method of the sh object: |
|
Remember that VBScript only recognizes Variants, so anything passed from the script to your methods is stored internally as a Variant value. If you don't take this detail into account, you might incur a type mismatch error. There are basically two ways to avoid this: declare parameters using the ByVal keyword (as in the AddItem routine shown earlier), or declare parameters as Variant values. The latter is the only feasible approach when you must pass a value by reference: |
|
Things become more exciting when you pass an object to the AddObject method and specify that the object is to be considered global in the script, which you do by passing a third argument equal to True: |
|
A global object doesn't need to be referenced explicitly when invoking its properties and methods, which means that the properties in SharedClass now appear to the script as regular variables, and that the methods in the class can be now invoked as regular VBScript procedures and functions. In other words, you are extending the script language using routines written in Visual Basic! For instance, VBScript doesn't natively support collections, but you can create and pass them from your Visual Basic-based program: |
|
I prepared a sample program that builds on this capability to create a sort of customizable personal calculator that lets users automate their frequent calculations. Using the menu commands, you can add new fields and set an expression for each of them. You can move them by dragging their frames with the mouse, and resize them by dragging their right-hand borders. Finally, you can save a field layout and reload it during a subsequent session (see Figure 4). |
Figure 4: Customizable Calculator |
I found that you can use shared objects to expose regular Visual Basic forms and controls, a detail that is not mentioned in the Script Control's documentation. This capability extends the potential of the Script Control in real-world business applications and simplifies the structure of the shared class. For example, you can have the shared class expose a TextBox control for your script to manipulate: |
|
Note that the Property Set TextBox procedure is declared using the Friend keyword, which prevents the script programmer from modifying the reference to the textbox. If you are exposing your objects to the outside world of scripting, you should always include some form of security.
Once you know how to expose Visual Basic objects to scripts, there is virtually nothing that prevents you from writing applications that can be completely customized at runtime through scripts. In fact, you only need to expose the reference to a form and the script will be able to access, move, or resize all of its controls. Thanks to the new dynamic control creation capabilities in Visual Basic 6.0, the script code can even create new controls at runtime. You just need to have the shared class expose the ActiveForm object. |
|
Then create new controls from the script using code like this: |
|
The Script Control Object Model
What I've shown so far only scratches the surface of the Script Control's power and flexibility. For starters, I haven't yet mentioned that the control exposes a simple but effective object hierarchy that lets you deal with multiple code modules (see Figure 5).
|
|
After you create a Module object, you can add code to it using the module's AddCode method, and later execute its procedures using the module's Run method: |
|
The Modules collection lets you iterate on all Module objects that are currently defined. This collection always contains at least one module, named Global, which is the module that you implicitly reference when you invoke the AddCode and Run methods of the Script Control itself. In other words, the following lines are all perfectly equivalent: |
|
The main difference between the Global module and the other Module objects that you create explicitly is that all
the procedures in the Global module are public by default (unless you explicitly declare them as private), while members in a secondary module are always private to that module and cannot be invoked from another module. You cannot remove items from the Modules collection, but you can
destroy all secondary modules by executing the Script Control's Reset method, which also clears the contents of the Global module.
Each module exposes a Procedures collection, which lets you iterate on the procedures that are currently defined in that module. Procedure objects expose three properties: Name, HasReturnValue (True if the procedure is a function), and NumArgs (the number of expected arguments). Script Control as an ActiveX Component It is also possible to use the Script Control as an invisible ActiveX component. This might be necessary if you need to encapsulate scripting into a Visual Basic class or a standard .bas module and you don't want your module to rely on a form just to host the control. To use the Script Control in this fashion, you have to reference the control in the References dialog and write the following lines of code: |
|
You cannot add the Script Control to the References dialog until you remove it from the list of installed components. In other words, you cannot use it as a component and as a control in the same application, unless you create an instance of the Script Control using the CreateObject function and then reference it through late binding. When you use the Script Control as a component, you must explicitly set the script language because in this case it is not initialized with a particular language. If you want to intercept events coming from the component, you have to declare and create the instance in two distinct steps: |
|
Oddly, the documentation that comes with the Script
Control seems to suggest that you should use a Variant variable to hold a reference to the control, and all the examples in the documentation use that approach. However, a Variant variable forces you to use late binding, which adds a speed penalty to each call to the Script Control's methods and
properties, and tends to deliver less robust code. Even more important, you can't receive events from a late-bound
object. Therefore, I strongly suggest you use the approach shown here. Wrap-up
The Script Control adds a new dimension to your Windows-based applications, whatever programming language you use to create them. It offers you the capability to write highly customizable software and to carry out tasks that would be very difficult to perform otherwise. As the list of supported Active Scripting-compliant script languages
expands, you can let your power users plug their own
routines into your application using their preferred scripting language. |
http://msdn.microsoft.com/scripting/ |
From the July 1999 issue of Microsoft Internet Developer.