The .NET Framework contains classes (such as those in the Microsoft.VisualBasic.Vsa namespace) that allow you to host a scripting engine in your application so that users can script your application. This article will show you how to host the VB.NET engine in your application, compile and run VB.NET code, and deal with syntax errors. It will also show you how to add an object reference so that your clients can program their VB.NET scripts using an object model that you supply for them to call back into your application.
The .NET Framework class that we'll be using is Microsoft.VisualBasic.Vsa.VsaEngine. The code we'll develop will instantiate this class and perform what are probably the three most important tasks required to put it all together.
- Just like any other user of the .NET Framework, your client's VB.NET script will need references to the Framework libraries (the DLLs), so we'll add a few references so that the script code can use the basic types and (in our example) send SMTP mail messages.
- We'll add a reference to an object owned by our application program that hosts the script engine so that the script code can call back into our application and influence its behavior.
- We'll add the required reference to an object that implements the IVsaSite interface. This IVsaEngine.Site property of Microsoft.VisualBasic.Vsa.VsaEngine must be set to an object reference that implements the IVsaSite interface. This is the way that the VsaEngine interacts with our script hosting application so that the engine can, for example, report any syntax errors. The IVsaSite interface is also part of the mechanism that allows us to pass in our own object references for the script to use. We'll discuss this in more detail later, but basically a site is what hosts the VsaEngine.
We won't start with the actual VsaEngine we'll start with our simple custom object that the script will be using, then we'll look at our class that implements IVsaSite, describe the class that uses the Microsoft.VisualBasic .Vsa.VsaEngine, and finally put it all together by reading a VB.NET source file, compiling it, and running it.
The UISReport class is the custom object that we'll supply for the script coder to use, shown in Listing 1. As you can see, it's a simple class that allows its caller to call the Reportit method, passing a string that will be displayed, and it has an integer property that can be set or queried for. This is just enough to illustrate the general mechanism. Later on we'll see how to pass this object into the scripting environment and how the client's VB.NET code can use it.
The VsaSite class is the class that implements the IVsaSite interface, and later on we'll use it to set the IVsaEngine.Site property this is the site that the script engine is hosted with. This class inherits from the Microsoft.Vsa.BaseVsaSite class. You could write your own class that implements all of the IVsaSite interface, but there's a basic implementation in Microsoft.Vsa.BaseVsaSite, which lives in Microsoft.Jscript.Dll. So we'll inherit from it and override the methods that are useful in our scripting environment.
The OnCompilerError method is called by the VsaEngine at compile time when it finds syntax errors in the VB.NET script written by the scripting clients. The class as I've written it contains a StringBuilder object into which we accumulate the errors as they're reported. The error object that's passed into this method (the Microsoft.Vsa.IVsaError object) has properties that identify the syntax error, such as the description of the error, the line on which it occurred, and the actual text of that line. Note that if this method returns a value of False, it will not be called for any subsequent errors (so it will just see the first error). Our class also has a ShowErrors method that we will call to show these accumulated errors if the compilation fails we'll see this in more detail later.
Our JVsaSite class also has an instance of our UISReport class intended for script client use, and the JVsaSite constructor instantiates it; see Listing 2. Notice also that we have an override of the GetGlobalInstance method here in our class, and this is how we supply an object to the script. When the VsaEngine is running the script and it recognizes that an object refers to a global item (more of this later), it calls IVsaSite.GetGlobalInstance passing the name of the item, and our implementation passes back the object that the name refers to. In this case we have just the one global item, so our GetGlobalInstance method returns a reference to the UISReport object that we instantiated in the JVsaSite constructor.
Hosting the Engine
Now we'll get to the meat of the script hosting, which is our EngineHost class in Listing 3. The purpose of this class is to wrap the scripting engine (Microsoft.VisualBasic.Vsa.VsaEngine) and expose methods to use it. The initialization of the engine takes place in the constructor. First we create the engine, and then set various properties and methods. It's here in the constructor that we set the engine's Site property to an instance of the JVsaSite class I described previously.
If you read the documentation on the IVsaEngine.RootMoniker property, you'll see that it is required to be in the form of <protocol>://<path>, where <protocol> and <path> are both strings unique to the host. This property is required for the engine to operate correctly, as is the Site property you'll get exceptions if these properties are not set.
The VsaEngine exposes a collection of items containing code and other references: the Items property. This property is used to create references for the compilation process in much the same way you add references in a Visual Studio.NET project. So we create a reference to the .NET system.dll and explicitly name the assembly with the AssemblyName property. I also wanted the VB.NET script to be able to send mail, so I added a reference to system.web.dll in the same general way. If they're not added, you'll get syntax errors in the sample script (provided with the download). Without a System reference, the compiler won't understand the Process type that I also used in the VB.NET sample script. Without a System.Web.Mail reference, the compiler won't understand the MailMessage type.
The engine also needs an item for the code (the script source), so the constructor also creates an item called "scriptcode" as an IVsaCodeItem, and this reference to the source code is saved in the member variable myScript because we'll need it later to add VB source code to the engine.
Finally in the constructor, we add an IVsaGlobalItem, a global item, and name it UISReport. This is the way we tell the engine that there is a global object model based on the name UISReport. This means that script references to the term "UISReport" will be legal to the script engine at compile time and run time. If you look back at the JVsaSite class and my discussion of GetGlobalInstance, the pieces should now fall into place. We tell the engine that there's a global item, an object model based on the string UISReport, and this causes the engine to call the IVsaEngine.Site.GetGlobalInstance method (passing "UISReport") implemented in our JVsaSite class, where we can return the actual instance of the UISReport class. Note the constructor code that sets the TypeString of our UISReport reference to be "System.Object." This is necessary for VB.NET to compile the source because the compiler needs a reference to a namespace in order to compile an otherwise unknown type. If the UISReport object we were passing to the script was not being supplied by our code but was declared in another assembly (a DLL), we would be setting its TypeString to the actual "Namespace.Type" for the type and passing a reference to the containing DLL, and generally treating the reference the same way that we treat the Microsoft references. The VB.NET script compiler would then be able to give us better errors.
The EngineHost class exposes two methods to compile and run the script. Taking each in the order in which they are to be called, first there is the AddCode method. This takes a string of VB.NET source code and calls the AppendSource method on the script code reference myScript that was created in the constructor.
The script source code is compiled (and perhaps run) by calling the Runit method. This resets the engine and then compiles the script code. Remember the JVsaSite class and the OnCompilerError method? This is where syntax errors are reported to, so if there are any syntax errors, they'll be accumulated by our JVsaSite.OnCompilerError method. If the compilation is unsuccessful, the code in Runit calls the site's ShowErrors method to report the errors in a message box. Clearly, this could be made as friendly as you want for your scripting clients. As you can see in our site's OnCompilerError method, you get the line number and the error message, so if you had some kind of IDE for script clients, you can point them at the exact line number.
Ready to Run
If the VB.NET code has no syntax errors, it means we have compiled code, so let's run it. It turns out that VB.NET doesn't really define a standard program entry point, so in this case we've enforced a convention that there must be a function called "Main," which is the program's entry point. One of the great things about the .NET framework is that it's fairly straightforward to find "Main" (or indeed any other function) by using reflection. The script engine has a property called Assembly that returns the compiled assembly, and like any other assembly, we can use reflection to enumerate all the types in the assembly looking for our "Main." When we find it, we invoke it, and it is at this point that the compiled VB.NET code will start to run.
This is the VB.NET that we're using. It's pretty basic: We fire off Notepad, wait for it to finish, and then send a mail message. The interesting part to notice is how it uses the UISReport object in a natural way, as an object with a property and a method.
In Listing 4, notice that there's an Option Strict Off at the beginning. We need this to compile with late-binding references that the compiler is unable to resolve at compile time our UISReport reference. If you don't set Strict off, you'll get syntax errors. To run this VB.NET code through our script engine, compile the project (you can download it from the magazine's web site) and step through it. It will offer a File Open dialog box that you can use to browse to Module1.vb in the same folder as the generated executable, and you should see Notepad pop up; if you've filled a valid e-mail address and you have a system that has the SMTP Mail Service installed, you can send the mail message. Like all .NET programs, this script code will throw exceptions, so a more robust script language might need some try/catch blocks. The Visual Studio.NET project runs with the .NET Framework 1.0 and with the Final Beta of the 1.1 .NET Framework.
MSDN has a sample called Scriptpad searching the MSDN web site for Scriptpad will get you to the "Script Happens" .NET article by Andrew Clinick.
See http://vsip.summsoft.com/vsa/ for more information about Visual Studio for Applications.
The newsgroup microsoft.public.dotnet .scripting is a useful place to ask questions or browse for answers.
Search MSDN for SSCLI to find the Shared Source Common Language Infrastructure, commonly known as "Rotor." There are implementations of the Vsa namespace here (in the JScript folder) that are useful to understanding how the internals of the Vsa classes work.
Phil Wilson is a developer at Unisys Corp. in California, working with COM, .NET, and Windows Installer technology. He has a BSc in Chemistry from the University of Aston in the UK. You can reach him at [email protected].