UML软件工程组织

VSA Scripting in .NET
转自:www.codeproject.com 作者:Mark (Code6) Belles

Contents

Introduction

This article will focus on Visual Studio for Applications, or VSA, and explain how it can be integrated with .NET programs. It will also explain a few key areas you must understand in order to successfully integrate VSA into your programs.

Why provide scripting capabilities?

VSA provides an excellent means of providing extensibility to your program by allowing script code to modify your program long after the main object model has been designed and compiled. Allowing your programs to host and run script files is an excellent way to allow others to extend your program in ways you may not have foreseen at its induction. Many times, added functionality is required by end users or high demand clients that require new changes to be recompiled into the old system by its creators. This takes time and money, and is often only necessary because modifications require the use of professional build tools, something many users aren’t skilled at using. However, with a solid object model exposed to a scripting interface, anyone capable of writing macros using a text editor and following an SDK can tweak much of the functionality of your program without the major hassles involved with professional build tools.

What is VSA?

VSA replaces the older technology Visual Basic for Applications, or VBA. VSA allows .NET programs to host and compile scripting languages such as VBScript and Jscript. Using VSA, you can create a scripting engine directly inside your .NET programs, and pass your own object instances and assembly references to the engine and script. Using your own custom object model, you can allow the scripts to modify your program after it has been compiled. Anyone with a text editor can follow up and add custom scripts or change existing ones, without having to bother you to make these changes.

Where can I find additional resources?

There is supposed to be a VSA SDK available for download from the Microsoft Script Center, but I cannot find a link to it. Feel free to look for yourself by starting here at the Microsoft Script Center. The MSDN Library contains some help on the topic but it is limited. As usual, my most valued resource ended up being my old faithful friend Google. Just go out and lookup the various interfaces that I will discuss and you will find plenty of helpful articles.

What assemblies do I need to reference?

The following assemblies are distributed with the .NET Framework and should be located in the GAC, or global assembly cache, on your system. Microsoft.Vsa.dll, Microsoft.Jscript.dll, and Microsoft.VisualBasic.Vsa.dll are included with the .NET Framework 1.1, and are readily accessible. Microsoft.Vsa.dll contains the core interfaces and objects needed to implement your own scripting engine and scripting host.

Required References

The following assemblies are your required references:

  • Microsoft.Vsa.dll
  • Microsoft.JScript.dll
  • Microsoft.VisualBasic.Vsa.dll

What scripting languages can I use?

You will need to decide which scripting language your program will support. Microsoft has supplied two engines, one for JScript and the other for VBScript. You may create your own, but that is beyond the scope of this document. The two classes that you may choose from are as follows. Using these classes, you will be allowed to run JScript or VBScript inside your programs. This is the language that you will use to script your program.

Languages Supported

  • VBScript.NET
  • JScript.NET

Pre-Built Engines

  • Microsoft.VisualBasic.Vsa.VsaEngine
  • Microsoft.Jscript.Vsa.VsaEngine

I've added the references, where do I start?

Once you have located and added references to the assemblies mentioned above, you need to implement the IVsaSite interface to allow your program to communicate with the script engine. Implementing this interface allows the engine to notify you of compilation errors, as well as request information such as object instances from your program as they are needed. This is an added bonus as you can defer the creation of objects until they are needed by the engine.

Interfaces to Implement

  • IVsaSite (fully qualified as Microsoft.Vsa.IVsaSite)

Here is a code snippet from my example project that shows how you would implement this interface:

/// <summary>
/// Provides a means to execute script code
/// using the IVsaSite and IVsaEngine interfaces
/// </summary>
public class VsaScriptingHost : IVsaSite, IDisposable

What is a scripting engine?

A scripting engine is a class that implements the IVsaEngine, as defined in the Microsoft.Vsa namespace. A script engine is capable of loading, compiling, and running script code. A scripting engine must be initialized before it can be used. Once initialized, code items, global items such as object instances, or references can be added to the engine. With my experiences, the order of initialization must occur in the proper sequence in order to avoid errors.

How do I create and initialize the engine?

Creation and initialization of a scripting engine is quite simple. The following steps are a guideline from my experiences that determine the order of operations for initializing a new scripting engine. The order is not concrete, but I have experienced errors when these steps are executed out of the order specified.

1. Creation of the Engine

Once you are ready, and have determined which type of scripting engine you will use, simply create an instance of the desired class. This new instance will be used as your scripting engine. I typically use a variable of type IvsaEngine, but you may use the actual type of engine in your code.

// use the vbscript engine provided in the assembly Microsoft.VisualBasic.Vsa.dll
_engine = new Microsoft.VisualBasic.Vsa.VsaEngine();
 
    // or
 
// use the jscript engine provided in the assembly Microsoft.JScript.dll
_engine = new Microsoft.JScript.Vsa.VsaEngine();

2. The Root Moniker

A unique name called a root moniker is used to identify each scripting engine. The root moniker is in the format “protocol://path” and should be unique to the host. You can use whatever you want as the root moniker, but try naming it something relative to your program. For example, try “yourCompany://yourProgram/language/engineCount”. This will allow easier identification of the engines during debugging and inspection.

// set the root moniker used by the engine
// (ex: "MyApp://MyVsaEngine/Instance#X")
_engine.RootMoniker = rootMoniker;

3. The Site Reference

Each scripting engine will need to communicate with an IVsaSite instance. The scripting engine interface contains a property called “Site” that needs to be assigned your implementation of the IVsaSite interface.

// set the site used by the engine to ourself
// as we are the host and will need to communicate with the engine
_engine.Site = this;

4. The Initialization

After the root moniker and site have been set, you must call the InitNew method on the engine. This prepares the engine to have new items added to it. Items such as code items, reference items, or global items. It must also occur before you set the root namespace of the engine.

// readies the engine to add new code items
_engine.InitNew();

5. The Root Namespace

In addition to the root moniker, you will need to specify a root namespace. All types created by the engine will be nested into this root namespace. When the engine runs a script, it will compile the script into an in-memory assembly. You will have access to all of the types created by the script, and the types loaded by the engine. You will need to use the root namespace you specified when the engine was created to gain access to the types you are looking for.

// set the root namespace used by the engine
_engine.RootNamespace = rootNamespace;

How do I add assembly references?

The first things that need to be addressed after the engine has been created and initialized are any references that may be needed by your scripts. References can be added to the engine by adding items of type VsaItemType.Reference. Any type that will be used in the script must have a reference supplied to the engine, otherwise your scripts will break. Just like in your program, you cannot use a type if you do not have a reference to its assembly.

Add an item to create an Assembly Reference

You may create as many or as few assembly references as you want, however, without creating references to the Types in use, you will run into errors when the script executes. To create an assembly reference, add an item of type VsaItemTypes.Reference to the engine with its “AssemblyName” set to the name of the assembly. For example, to add an assembly reference to the System.dll assembly, the following code would be necessary:

/// <summary>
/// Creates an IVsaReferenceItem item using the current IVsaEngine
/// </summary>
/// <param name="itemName"></param>
/// <returns></returns>
public virtual IVsaReferenceItem 
       CreateReferenceItem(string itemName, string assemblyName)
{ 
    Debug.Assert(itemName != null && itemName != string.Empty);

    // assert the item is unique  
     this.AssertEngineItemUnique(itemName);
 
     // create a new reference item
     IVsaReferenceItem item = 
        (IVsaReferenceItem)_engine.Items.CreateItem(
            itemName, VsaItemType.Reference, VsaItemFlag.None);

    // set the item's assembly name
    item.AssemblyName = assemblyName;

    return item;
}

Be mindful to add all of the references that your scripts might need. References such as System.dll or Mscorlib.dll are critical to the successful execution of your scripts. No references are added by default for you, you must take it upon yourself to add the needed references. Your scripts will be limited by the references it has access to.

How do I load the script into the engine?

After you have created all of the assembly references, you now need to add the script to be run by the engine. This can be done by adding an item of type VsaItemTypes.Code and setting its “SourceText” property to the contents of the script. This should be done before any global items are added to the script. Loading the script from a file is the most extensible means in my opinion, however you are not limited to this means. You could supply the script body from any source available, such as a resource file, or even in your own source code. If you choose to load the script from a file, simply use a StreamReader or another .NET framework class to read the contents of the file into a StringBuilder or String variable. After the script has been loaded from the file, simply set the “SourceText” property using the loaded script.

Adding the code item that will contain the script

A code item must be added to the engine before you can supply the item with the script to be executed by the engine. The following is an example of how you might add the code item to the engine:

/// <summary>
/// Creates an IVsaCodeItem using the current IVsaEngine
/// </summary>
/// <param name="itemName"></param>
/// <returns></returns>
public virtual IVsaCodeItem CreateCodeItem(string itemName)
{
    Debug.Assert(itemName != null && itemName != string.Empty);
 
    // assert the item is unique
    this.AssertEngineItemUnique(itemName);
    
    // add a code item to the engine
     IVsaCodeItem item = 
        (IVsaCodeItem)_engine.Items.CreateItem(
            itemName, VsaItemType.Code, VsaItemFlag.None);    
     return item;
}

How do I publish my own objects for use in the scripts?

This is perhaps the most interesting part of hosting your own scripting engine. Many large scale applications allow scripts to modify the objects that are running the program. Using VSA, you can publish your own object instances from your program to be used by the scripts. How cool is that?!

Adding the global item that will be used to publish your own object instance

To publish your own object instances and make them available to the script, you will add items of type VsaItemType.AppGlobal to the engine. As you add these global items to the engine, the name that you supply the item will be the name of the variable, or object instance, in the script. So take care when naming these items as it will affect their usage in the script. Take for example, you want to publish a form instance to a script. If you name the global item “mainForm”, then it will be accessible in the script as a global variable named “mainForm”. You may then call methods upon the variable just like you would in your program. To close the form, you might use mainForm.Close(); if you were using JScript.

After you add the code item, it is necessary to set the item’s “TypeString” property to the fully qualified type name of the object you are publishing. So again, if you were publishing a Form object, you might use the following code to set the item’s TypeString.

Here's some sample code that demonstrates how you might publish one of your object instances to be available to script:

// create a new global item
IVsaGlobalItem item = 
    (IVsaGlobalItem)_engine.Items.CreateItem(
        “mainForm”, VsaItemType.AppGlobal, VsaItemFlag.None);
 
string typeName = typeof(System.Windows.Forms.Form).FullName;       
 
// set the item's type string to the instance's type's fullname
item.TypeString = typeName;

How does the engine find the object instance for a published object?

Now, you might be wondering how the script is going to gain access to the actual instance you want to use? No where did you specify any variable instance yet to be used for this “mainForm” variable. The key is in the fact that the scripting engine communicates back with your program using the IVsaSite interface. Whenever the engine encounters an item of type VsaItemType.AppGlobal, it will call back on the IVsaSite interface to retrieve the object instance for the item.

At first, this seems a bit odd, but it does have its advantages. Because you are not required to pass the object instance to the engine when the global item is created, you can defer the object creation until the engine calls for it. This may not be useful, but under the right conditions, it could prove extremely useful to keep a small memory footprint and reduce resource consumption. This does present a problem for the implementer of the IVsaSite interface. How can you return the instance of the object when then engine calls for it, since it will ask for it using the name you used when you added the item?

This can be a simple case of using a Hashtable to store the object instances by their item name when they are added to the engine as a global item. You could create some other lookup or creation mechanism, but for the purposes of simplicity, we will assume that you will store the object instance in a Hashtable when you add a global item to the engine. Something like this…

// cash item in globalItemTable
_globalItemLookupTable.Add(itemName, instance);

Now that you have a way to lookup the instance by its item name, all you have to do is wait for the engine to call you back and return the object when it asks for it. Your specific implementation of the IVsaSite interface will dictate exactly how this lookup process will work. Only trial and error combined with careful design will lead you to the most useful implementation for your needs. Using a lookup table is pretty basic and not all that complex, so it’s easy to setup and debug. Here’s how this lookup appears in my example project. This code can be found in my VsaScriptingHost class.

 /// <summary>
/// When the engine calls back the IVsaSite to ask for a global item,
/// return the instance if we've cached it previously
/// </summary>
/// <param name="name">The name of the global item
/// to which an object instance is requested</param>
/// <returns></returns>
public object GetGlobalInstance(string name)
{
    // enumerate the items in our global item lookup table
        foreach(DictionaryEntry entry in _globalItemLookupTable)
 
        // carefully comparing the item names
        // to the name of the item requested by the engine
        if (string.Compare(entry.Key.ToString(), name, true) == 0)
             // and if we find it, return the object instance to the engine
                 return entry.Value;
 
    // and finally if we couldn't find any instance
    // for that name, just don't worry about it
    return null;
}

What objects should I publish to the scripts?

Just because you can publish objects for the scripts to program against, doesn’t mean that you should. The more objects that you expose, the more resources you will use, and the slower things will get. Just keep that in mind as a rule of thumb. Try and keep this in mind as you publish objects for use in the scripts. Try to only publish objects that you know will be useful in the scripts. Only you can determine what is or is not useful for your scripts.

Publishing your own object model in this fashion allows the scripts to easily modify your existing program. Things like changing menu items or responding to program events, suddenly become trivial script code. Publishing a well thought out object model for the scripts to program against, effectively supplies the hooks needed to extend your program in the future.

How do I run the scripts?

There isn’t much left at this point. You’ve added your assembly references, you’ve loaded the script and added the code item, and finally, you’ve added your own objects so that the script can program against them. All you need to do at this point is compile the script and run it! You can accomplish this easily by calling the following methods upon the scripting engine:

// compile the script
_engine.Compile();
 
// run the script
_engine.Run();

How can I tell if there are errors in the script?

The scripting engine will call you back using the IVsaSite interface through the OnCompilerError() method. Any errors encountered during compilation, or during script execution, will be captured and directed to this method. Depending upon the error, you will need to take the appropriate actions to handle the error. It’s been my experience that once you get an error, there’s not much you can do but correct the script and try again. Make sure you use decent amounts of structured exception handling in your script files to prevent errors from reaching this method.

Reviewing what we have learned

So in the interest of wrapping things up, and getting you on to running your own scripts using VSA, let’s review some of the major points we’ve covered. Visual Studio for Applications, or VSA, replaces the older technology known previously as VBA, or Visual Basic for Applications. VSA is not limited to only VBScript, any .NET scripting language can be used. You may have to create a custom engine for scripting languages if you are trying to use some language other than VBScript or Jscript.

Microsoft has supplied us with two pre-built engines that can be used just for these languages. You will need to install the .NET Framework, and add references to the Microsoft.Vsa.dll and the assembly containing the engine of your choice. The aforementioned engines can be found in these assemblies. The VBScript engine is located in the Microsoft.VisualBasic.Vsa.dll, and the JScript engine is located in the Microsoft.Jscript.dll.

You must first create and then initialize your scripting engine before you can add items to it. Items define assembly references, the script to be executed, and any objects that you want to publish to the script. You will need to implement the IVsaSite interface to enable communication between the script engine and your program. Without this interface, you will not be notified of errors in the scripts, nor can the engine request instances for the published objects when it requires them.

The scripting engines are capable of compiling scripts, and then storing that compiled IL for later use. This can improve performance, but it’s not as extensible as compiling the scripts when your program runs. I would recommend just loading the scripts from a file at runtime. This allows anyone to modify the scripts and make changes to them using a simple text editor. Much easier to modify text than precompiled Intermediate Language.

I hope this article has helped you to understand the benefits of VSA, and helped you to understand how you might implement it in your own programs. I have written a small example and assembly that you can download and run/debug at your leisure. It features a scripting host that can run either VBScript or JScript code files. I tended to dislike VBScript so I focused on JScript, and so you will find more JScript examples than VBScript examples. The examples show in a very simple application how to publish objects and modify the program using the scripts. The scripts also show how to respond to events inside the program, directly in the script files!

Understanding the sample

The demo application simply creates a simple form object and publishes it to the scripts. The scripts modify the form by adding some menu items, and wiring up to some of the form's events.

I've created some wrapper classes to help make creating a scripting host and running script files trivial. There are two main classes with which you will work with:

  • VsaScriptingHost
  • VsaScriptingHostFactory

The VsaScriptingHost class exposes methods and events to help you load your scripts. Add objects and assembly references to the scripting host. This class also helps you compile safely and catch exceptions by means of an event. The events you'll need to work with are as follows:

/// <summary>
/// Occurs when the VsaScriptingHost needs Assembly references to be added
/// </summary>
public event ScriptingHostEventHandler AssemblyReferencesNeeded;
 
// <summary>
/// Occurs when the VsaScriptingHost needs Global Items to be added
/// </summary>
public event ScriptingHostEventHandler GlobalItemsNeeded;
 
/// <summary>
/// Occurs when the engine encounters an IVsaError during compilation
/// </summary>
public event ScriptingHostCompilerExceptionEventHandler CompilerException;

In addition to the VsaScriptingHost class, I also created the VsaScriptingHostFactory which helps to create instances of the VsaScriptingHost class. The factory class is capable of determining the type of script engine to use based upon a script file. In other words, JScript (*.js) script files would result in a scripting host instance that is capable of running JScript. This is all quite painless with the factory hiding most of the grunt work and details from you. From the sample code, this is how you might combine these classes to work for you:

// create a new set of hosts
VsaScriptingHost[] hosts = _factory.Create(@"MyScriptingHost", 
             @"Scripting", true, Application.StartupPath);


// wire up to the events of each host
foreach(VsaScriptingHost host in hosts)
{    
    host.AssemblyReferencesNeeded += 
         new ScriptingHostEventHandler(OnAssemblyReferencesNeeded);
    host.CompilerException += new 
         ScriptingHostCompilerExceptionEventHandler(OnCompilerException);
    host.GlobalItemsNeeded += new ScriptingHostEventHandler(OnGlobalItemsNeeded);
}

// execute the hosts
foreach(VsaScriptingHost host in hosts)
    host.Execute();

Using those events as triggers to setup your scripts and apps to be extended by the scripts is also quite trivial. From the sample, here's how I exposed the form instance to the scripts. Also note that I added the starting assembly (*.exe) as a reference to the host, because the form class is found in that assembly. Do not forget to add your assembly references!!!

/// <summary>
/// Occurs when assembly references are needed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnAssemblyReferencesNeeded(object sender, 
                                 VsaScriptingHostEventArgs e)
{
    // add the current assembly as a reference
    e.Host.CreateAssemblyReferences(Assembly.GetExecutingAssembly().Location);
} 
 
/// <summary>
/// Occurs when an error occurs during compilation or execution
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnCompilerException(object sender, 
        VsaScriptingHostCompilerExceptionEventArgs e)
{
    Debug.WriteLine(e.Exception);
}
 
/// <summary>
/// Occurs when global items are needed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void OnGlobalItemsNeeded(object sender, 
                          VsaScriptingHostEventArgs e)
{
    // publish the scriptable form so that the scripts may modify it
    e.Host.CreateGlobalItem("_scriptableForm", _scriptableForm, false);
}

Well, that's about it. Take a look at the source to the host and the hosting factory if you are wondering about the gritty details. I hope this helps you understand how you can add scripting capabilities to your .NET applications.

 

 

版权所有:UML软件工程组织