Share via


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.

MIND

Visual Programmer

Add Scripting to Your Apps with Microsoft ScriptControl

George Shepherd

Q

In the October 1999 issue of MSJ, you covered the workings of Microsoft® Active Scripting in detail. While I found the suggestions very useful for my project, it seemed to me that writing all that site code was a bit much. Isn't there already a prepackaged site that I can use somewhere?

Darcy Berger

 

A

Yes, it turns out that you don't have to write all the scripting plumbing code yourself. There's already an ActiveX® control available from Microsoft that has all the site plumbing wrapped up for you, aptly named the Microsoft ScriptControl. Before going over the ScriptControl, let's review the Active Scripting architecture.

Active Scripting Redux

      There are two sides to the Active Scripting story: Active Scripting engines and Active Scripting hosts. An Active Scripting engine is a COM component that implements the scripting interfaces and understands how to parse the syntax of a certain scripting language. Microsoft provides scripting engines for VBScript and JScript®. Because the Active Scripting mechanism is hidden behind COM interfaces, you can include multiple disparate scripting engines in your application. In fact, you don't even need to use scripting engines from Microsoftâ€"you can use one from another vendor or write your own.
      An Active Scripting engine is just like any other COM object: you create an instance of the object using CoCreateInstance and acquire interfaces through QueryInterface. Active Scripting engines usually implement the following interfaces: IActiveScript, IActiveScriptDebug, IActiveScriptParse, IActiveScriptStats, IObjectSafety, IRemoteApplicationDebugEvents, and IVariantChangeType.
      There are two ways to get script code into the scripting engine. The first way is to ask the scripting engine to load the script text from some persistent medium (through one of the IPersistXXX interfaces). Alternatively, you can represent the scripting text as a BSTR and load it into the engine using IActiveScriptParse.
      Active Scripting engines are hosted by a program or component that can conjure up an Active Scripting site. Active Scripting depends upon bidirectional communication between the Active Scripting engine and the Active Scripting host, as many other COM-based technologies do. For scripting to work, the host needs to implement an interface named IActiveScriptSite so that the scripting engine has a way to call back to the host.
      The scripting engine uses the host's IActiveScriptSite interface to call on the script site from time to time to perform three main functions. The first function gets information about the objects being scripted. The second function informs the site about changes in the state of the scripting engine. The third function informs the site when there's an error running the script. An Active Scripting host is responsible for instantiating one of the available scripting engines and instructing the engine to run a script. The host then adds script code to the Active Scripting engine and asks the engine to execute the code. The host also has the option of creating COM objects and adding them to the Active Scripting engine's namespace so that the objects may be used within the script code.

The ScriptControl Object

      Incorporating the scripting functionality into your application means somehow implementing IActiveScriptSite and connecting it to the Active Scripting engine. If you're a C++ developer with some time on your hands, that's not a big deal. In fact, that's how I created the site in the October 1999 issue of Microsoft Systems Journal. However, if you're a C++ developer with very little time on your hands, or you're working within a language that doesn't allow you to implement interfaces like IActiveScriptSite, then you'll probably want to have someone else write the plumbing code for you.
      Enter the Microsoft ScriptControl. The ScriptControl is an ActiveX control that already implements the plumbing code to make scripting work in your application. The ScriptControl lives inside a DLL named MSScript.ocx. The Microsoft ScriptControl is available for download at https://msdn.microsoft.com/scripting/scriptcontrol/default.htm.
      You can use the OLE viewer that comes with Visual C++® to peer into the control. It has 23 interfaces hanging from itâ€"mostly the standard ActiveX control interfaces for embedding the control somewhere. The main interface for talking to the ScriptControl is IScriptControl (see Figure 1). A description of each property and method of IScriptControl can be seen in Figure 2.
      Notice that IScriptControl has methods for managing the entire scripting process, including setting the language, allowing or disallowing the script to participate in the UI, adding code and objects to the script, and retrieving the various parts of a script. The methods and properties in IScriptControl describe the control's object model.

The ScriptControl Object Model

      In addition to holding the Active Scripting site required by the scripting engine, the ScriptControl maintains its own internal object model. The object model consists of module and procedure collections (see Figure 3).

Figure 3 ScriptControl Object Model
Figure 3 ScriptControl Object Model

The top-level object of the ScriptControl object model is the module. Most scripts are collections of procedures. For example, here's some VBScript code combining variable declarations and some procedures:

  Dim x
  
Dim y

Function AnswerToEverything
AnswerToEverything = 42;
End Function

Sub DoIt

x = 5
y = 6
MsgBox AnswerToEverything
End Sub

 

In Active Scripting parlance, this is known as a module.
      The ScriptControl holds a single global module by default, appropriately named global. It's the first one you'll encounter when iterating through the module collection. By default the global module is where the ScriptControl manages the script code inserted by the client. For example, the functions AddCode, ExecuteStatement, Eval, and Run within the IScriptControl interface act on the main global module. In addition, the Procedures property within IScriptControl exposes a list of procedures held by the global module, and CodeObject exposes an IDispatch pointer representing the public members of the global module.
      The ScriptControl supports both iterating through the module list and adding secondary modules through the IScriptModuleCollection interface, as shown here:

  interface IScriptModuleCollection : IDispatch {
  
[propget]
HRESULT _NewEnum([out, retval] IUnknown** ppenumContexts);
[propget]
HRESULT Item([in] VARIANT Index,
[out, retval] IScriptModule** ppmod);
[propget]
HRESULT Count([out, retval] long* plCount);
HRESULT Add([in] BSTR Name,
[in, optional] VARIANT* Object,
[out, retval] IScriptModule** ppmod);
};

 

Notice that the IScriptModuleCollection behaves like a normal iterator expected by Visual Basic. It exposes IEnumVariant through the _NewEnum property, and has Item and Count properties. This means developers using the control within Visual Basic can use the module collection within a For...Next loop. The modules collection is made of objects implementing the IScriptModule interface, which is shown in Figure 4.
      Notice that IScriptModule includes AddCode, Eval, Execute, and Run functions, just like IScriptControl. In addition, the IScriptModule interface exposes a Name property (the name of the module), an IDispatch pointer exposing the module's public functions, and a collection of procedures, just like IScriptControl. I'll look at managing the code shortly.

Procedures

      The second layer within the ScriptControl's object model consists of procedures. Just as with modules, ScriptControl defines the procedures through a collection and an individual interface. Here's the IScriptProcedureCollection interface:

  interface IScriptProcedureCollection : IDispatch {
  
[propget]
HRESULT _NewEnum([out, retval] IUnknown** ppenumProcedures);
[propget]
HRESULT Item([in] VARIANT Index,
[out, retval] IScriptProcedure** ppdispProcedure);
[propget]
HRESULT Count([out, retval] long* plCount);
};

 

      IScriptProcedureCollection simply provides functions for iterating through the collection of procedures. It doesn't include an Add function (as IScriptModuleCollection does). Instead, you add procedures to a module through the module's AddCode method.
      Each procedure is described through the IScriptProcedure interface like so:

  interface IScriptProcedure : IDispatch {
  
[propget]
HRESULT Name([out, retval] BSTR* pbstrName);
[propget]
HRESULT NumArgs([out, retval] long* pcArgs);
[propget]
HRESULT HasReturnValue([out, retval]
VARIANT_BOOL* pfHasReturnValue);
};

 

IScriptProcedure's Name property exposes the procedure's name. NumArgs exposes the number of the procedure's arguments, and HasReturnValue indicates whether you can assign the value returned by the procedure to the left side of an assignment.

ScriptControl Events

      Besides the incoming functions described by the IScriptControl interface, the ScriptControl exposes two events to the container application via a dispatch interface named DScriptControlSource. These two events are the Timeout event and the Error event. The Timeout event occurs when the time specified in the Timeout property has elapsed. The Error event occurs when the scripting engine detects some sort of error.
      When the client receives an Error event, the client can ask the ScriptControl for the latest error by accessing IScriptControl's Error property (sort of like the Win32® GetLastError function). After accessing the Error property, the client gets an Error object, which implements the IScriptError interface (see Figure 5). A brief description of the interface is seen in Figure 6. I'll revisit IScriptError shortly.

Using the ScriptControl

      There are several ways to use the ScriptControl. Of course, the easiest way is to drop it into a Visual Basic-based program as an ActiveX control. When running the ScriptControl as an ActiveX control, you can put it right in your form and exercise the functions to your heart's content. To learn how to use the ScriptControl, see the article "Exploring the Microsoft Script Control" by Francesco Balena in the July 1999 issue of Microsoft Internet Developer.
      If you're a C++ developer, the easiest way to use the ScriptControl is to insert it into your project using the Project | Add To Project | Components and Controls menu item. This causes Visual Studio® to go out, read the ScriptControl's type library, and create C++ class wrappers around the IScriptControl, IScriptModule, IScriptModuleCollection, IScriptProcedure, IScriptProcedureCollection, and IScriptError interfaces. Including the ScriptControl in your application via the component gallery yields six wrappers for the previously mentioned interfaces: scriptcontrol (.h and .cpp), scripterror (.h and .cpp), scriptmodule (.h and .cpp), scriptmodulecollection (.h and .cpp), scriptprocedure (.h and .cpp), and scriptprocedurecollection (.h and .cpp).
      My sample program for this column is a simple dialog box that exercises the ScriptControl (see Figure 7). The application lets you add functions and procedures to a script, evaluate single expressions and statements, and add objects to the scripting namespace.

Adding Code and Objects

      Including the control in your project (using the component gallery) automatically inserts the ScriptControl wrapper files into your project form. Just drop the control into your app (or create it programmatically), and you can pound away on the control. For example, adding code to the script is simply a matter of calling the wrapper class' AddCode method and feeding it a CString:

  void CUsemsscriptctlDlg::OnAddcode() 
  
{
UpdateData(TRUE);
m_scriptCtl.AddCode(m_strCode);
ShowProcedures();
}

 

      If you recall from the bare-bones implementation of scripting in the October 1999 issue of MSJ, you had to manually manage a list of named items. All that is now done by the ScriptControl. Figure 8 contains code that looks up an object's CLSID from a ProgID, creates the object, and then adds the object to the ScriptControl's namespace.

Iterating Through Procedures

      Once you've added code to the scripting control, it's often useful to get a list of the procedures and functions added. You can do this through the IProceduresCollection available from each module. Figure 9 shows how to retrieve a list of the functions and procedures that have been added to the global module.

Executing Code

      There are two ways to execute code within the main module. The first way is to call the ScriptControl's Run function. I'll look at doing that in a future column since MFC's ScriptControl wrapper doesn't support the function. Another reason for procrastinating is that Run uses a variable argument list.
      The second way to execute code is to call the ScriptControl's Execute function. Be aware that this method precludes using arguments in your function and procedure signatures:

  void CUsemsscriptctlDlg::OnExecutestatement() 
  
{
UpdateData(TRUE);
m_scriptCtl.ExecuteStatement(m_strStatement);
}

 

Catching Scripting Errors

      Of course, not all scripts are created perfectly, and they'll have bugs and errors within them. The ScriptControl's Error object returns detailed information about what exactly went wrong within your script.
      To implement error checking, simply set up an event handler for the Error event. Whenever the ScriptControl calls your application, just turn around and get the Error object from the ScriptControl. Figure 10 shows how to fetch error information from the ScriptControl. It shows one way of responding to the ScriptControl's error event. Figure 11 shows a message box displaying an error in the sample program.

Conclusion

      Despite its limitations (such as restricted data types and generally lower performance), scripting has many advantages over compiled code. Scripting code is generally more flexible because you can easily change it on the fly and rerun it without recompiling everything. In addition, scripting codeâ€"which you'll often see in HTML and ASP pagesâ€"is readily available in the Internet computing environment.
      In many cases, it's useful to add scripting to your application. For example, if your app performs some sort of simulation or modeling, scripting provides a way for users to program your application. In addition, scripting provides a great environment for stress and regression testing.
      In Windows®, adding scripting capability to your application is more or less a matter of providing an ActiveScriptSite implementation and managing the script code (that is, managing the named objects). If you don't have time to write that plumbing code, the Microsoft ScriptControl provides the generic functionality, and is a really quick way to get scripting into your app.
      In this column I discussed the structure of the ScriptControl and some ideas for using it within an MFC application. In a future column, I'll go over hosting the ScriptControl within an ATL-based application.
George Shepherd is an instructor with DevelopMentor and a Software Engineer at Stingray Software. George is coauthor of MFC Internals (Addison-Wesley, 1996) and Programming Visual C++ (Microsoft Press, 1998).*

From the June 2000 issue of MSDN Magazine.