Contents
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.
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.
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.
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.
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.
The following assemblies are your required references:
- Microsoft.Vsa.dll
- Microsoft.JScript.dll
- Microsoft.VisualBasic.Vsa.dll
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.
Microsoft.VisualBasic.Vsa.VsaEngine
Microsoft.Jscript.Vsa.VsaEngine
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.
IVsaSite (fully qualified as Microsoft.Vsa.IVsaSite )
Here is a code snippet from my example project that shows
how you would implement this interface:
public class VsaScriptingHost : IVsaSite, IDisposable
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.
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.
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.
_engine = new Microsoft.VisualBasic.Vsa.VsaEngine();
_engine = new Microsoft.JScript.Vsa.VsaEngine();
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.
_engine.RootMoniker = rootMoniker;
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.
_engine.Site = this;
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.
_engine.InitNew();
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.
_engine.RootNamespace = rootNamespace;
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.
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:
public virtual IVsaReferenceItem
CreateReferenceItem(string itemName, string assemblyName)
{
Debug.Assert(itemName != null && itemName != string.Empty);
this.AssertEngineItemUnique(itemName);
IVsaReferenceItem item =
(IVsaReferenceItem)_engine.Items.CreateItem(
itemName, VsaItemType.Reference, VsaItemFlag.None);
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.
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.
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:
public virtual IVsaCodeItem CreateCodeItem(string itemName)
{
Debug.Assert(itemName != null && itemName != string.Empty);
this.AssertEngineItemUnique(itemName);
IVsaCodeItem item =
(IVsaCodeItem)_engine.Items.CreateItem(
itemName, VsaItemType.Code, VsaItemFlag.None);
return item;
}
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?!
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:
IVsaGlobalItem item =
(IVsaGlobalItem)_engine.Items.CreateItem(
“mainForm”, VsaItemType.AppGlobal, VsaItemFlag.None);
string typeName = typeof(System.Windows.Forms.Form).FullName;
item.TypeString = typeName;
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…
_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.
public object GetGlobalInstance(string name)
{
foreach(DictionaryEntry entry in _globalItemLookupTable)
if (string.Compare(entry.Key.ToString(), name, true) == 0)
return entry.Value;
return null;
}
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.
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:
_engine.Compile();
_engine.Run();
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.
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!
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:
public event ScriptingHostEventHandler AssemblyReferencesNeeded;
public event ScriptingHostEventHandler GlobalItemsNeeded;
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:
VsaScriptingHost[] hosts = _factory.Create(@"MyScriptingHost",
@"Scripting", true, Application.StartupPath);
foreach(VsaScriptingHost host in hosts)
{
host.AssemblyReferencesNeeded +=
new ScriptingHostEventHandler(OnAssemblyReferencesNeeded);
host.CompilerException += new
ScriptingHostCompilerExceptionEventHandler(OnCompilerException);
host.GlobalItemsNeeded += new ScriptingHostEventHandler(OnGlobalItemsNeeded);
}
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!!!
private static void OnAssemblyReferencesNeeded(object sender,
VsaScriptingHostEventArgs e)
{
e.Host.CreateAssemblyReferences(Assembly.GetExecutingAssembly().Location);
}
private static void OnCompilerException(object sender,
VsaScriptingHostCompilerExceptionEventArgs e)
{
Debug.WriteLine(e.Exception);
}
private static void OnGlobalItemsNeeded(object sender,
VsaScriptingHostEventArgs e)
{
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.
|