|
|
Title | Use VSA to let the user execute scripts in Visual Basic .NET |
Description | This example shows how to use VSA to let the user execute scripts in Visual Basic .NET |
Keywords | VSA, Visual Studio for Applications, script, VB.NET |
Categories | Controls, Software Engineering |
|
|
Remember the Microsoft Script Control? It lets you execute script code within a Visual Basic 6 application relatively easily. You pass the control any objects that you want to expose to the script, give it the script code, and call its Run method.
You can still use the Microsoft Script Control in Visual Basic .NET but it seems a bit wrong using VBScript to manipulate a Visual Basic .NET application. There should be a .NET version of the control that executes VB .NET code instead of VBScript.
Unfortunately Microsoft didn't build that control. Instead they gave us VSA (Visual Studio for Applications). Like many other tools in Visual Basic .NET, it provides more (possibly unnecessary) flexibility than the VB 6 version but at least it's a lot more complicated. So grab your preferred caffeinated beverage, take a deep breath, and read on.
First download the latest version of VSA (Visual Studio for Applications) from Microsoft's Web site and install it.
Next create the project that will expose methods for the user to manipulate. This example uses a form named DrawingForm. It provides a public subroutine named MakeSegment that creates a new Segment entry. The form draws these Segments when its PictureBox raises a Paint event.
DrawingForm also provides a public ReDraw method to let the script redraw the picture after creating Segments.
The details of these methods aren't important to scripting so they aren't shown here. See the code for the details.
IMPORTANT: VSA uses an assembly to get information about the object model that it will expose to scripting. If you use Visual Basic .NET (as opposed to C#), this assembly cannot be in an EXE, it must be in a DLL. For that reason, this example builds the DrawingForm and some related objects in a separate DLL project.
The following code shows the entire main program. It simply makes a DrawingForm and runs it. Everything else is contained in the DrawingForm's module.
|
|
Module SubMainModule
Public Sub Main()
Dim frm As New MainForm.DrawingForm
Application.Run(frm)
End Sub
End Module
|
|
To use VSA, you must build a class that implements the IVsaSite interface. This class helps VSA locate objects it needs and responds to events raised by VSA.
This version stores a reference to a DrawingForm object. An IVsaSite that provided access to more than one object would need to store those objects in some sort of collection or HashTable so it can find them later.
The GetGlobalInstance function takes as a parameter the name of a global object defined for the script and it returns the corresponding object. This example returns its DrawingForm object when the parameter is TheForm. In other words, if the script wants to use an object named TheForm, the IVsaSite returns its DrawingForm object.
VSA calls the OnCompilerError function if there is an error compiling the script. The [error] parameter (talk about dumb parameter names) gives the function information about the error. The OnCompilerError function in this example uses [error] to display information about the error in a RichTextBox on a dlgError form. It displays the error message and the line containing with the incorrect code highlighted in bold red text.
If the user clicks the error dialog's OK button, the OnCompilerError function returns True to tell VSA that it should continue to process the script and call OnCompilerError if it finds other errors. If the user clicks Cancel, OnCompilerError returns False to tell VSA to stop processing the script. This is handy if the script is long and one error causes a lot of others.
|
|
Public Class MyVsaSite
Implements IVsaSite
Public m_DrawingForm As DrawingForm
' Save the DrawingForm.
Public Sub New(ByVal drawing_form As DrawingForm)
m_DrawingForm = drawing_form
End Sub
Public Sub GetCompiledState(ByRef pe() As Byte, ByRef _
debugInfo() As Byte) Implements _
Microsoft.Vsa.IVsaSite.GetCompiledState
Trace.WriteLine("IVsaSite.GetCompiledState()")
pe = Nothing
debugInfo = Nothing
End Sub
' Return an event source instance.
Public Function GetEventSourceInstance(ByVal itemName _
As String, ByVal eventSourceName As String) As _
Object Implements _
Microsoft.Vsa.IVsaSite.GetEventSourceInstance
Trace.WriteLine(String.Format("IVsaSite.GetEventSourceInstance('{0}', " & _
"'{1}')", itemName, eventSourceName))
Select Case (eventSourceName)
Case "TheForm"
Return m_DrawingForm
Case Else
Throw New _
VsaException(VsaError.EventSourceTypeInvalid)
End Select
End Function
' Return a global instance.
Public Function GetGlobalInstance(ByVal name As String) _
As Object Implements _
Microsoft.Vsa.IVsaSite.GetGlobalInstance
Select Case name
Case "TheForm"
Return m_DrawingForm
Case Else
Throw New _
VsaException(VsaError.GlobalInstanceInvalid)
End Select
End Function
Public Sub Notify(ByVal notify As String, ByVal info As _
Object) Implements Microsoft.Vsa.IVsaSite.Notify
End Sub
Public Function OnCompilerError(ByVal [error] As _
Microsoft.Vsa.IVsaError) As Boolean Implements _
Microsoft.Vsa.IVsaSite.OnCompilerError
' Compose the error message.
Dim msg1 As String = _
"Error on line " & [error].Line & vbCr & vbCr
Dim msg2 As String = _
[error].LineText & vbCr
' Display the message.
Dim dlg As New dlgError
dlg.rchError.Text = msg1 & msg2 & _
[error].Description
dlg.rchError.Select( _
Len(msg1) + [error].StartColumn - 1, _
[error].EndColumn - [error].StartColumn + 1)
dlg.rchError.SelectionColor = Color.Red
dlg.rchError.SelectionFont = New _
Font(dlg.rchError.SelectionFont, FontStyle.Bold)
If dlg.ShowDialog() = DialogResult.OK Then
' Continue to report errors.
Return True
Else
' Stop reporting errors.
Return False
End If
End Function
End Class
|
|
When the user opens the DrawingForm's File menu and selects Script, the program displays a dialog where the user can enter some script. Initially the form is filled with a rather complicated example script that draws a cycloid. You can replace this script or introduce errors to see how the error handling works.
If the user clicks OK, the program passes the script code to subroutine RunScript, where the fun really begins.
The code starts by creating a VsaEngine object. This program imports the Microsoft.Vsa and Microsoft.VisualBasic.Vsa namespaces so this is the Visual Basic version of the engine (you'll need to set references to these in Project Explorer's References section). The Microsoft.JScript.Vsa namespace defines another version of VsaEngine that works with JScript instead of Visual Basic.
Next the code creates an instance of the MyVsaSite class we defined, passing its constructor a reference to the current DrawingForm object. This is the object that GetGlobalInstance will return when the script uses TheForm.
Next comes some Black Magic. The program sets the engine's RootMoniker to a string of the form ://. These are just made up strings that identify the engine instance. The value should be a string unique to the server and must not be a standard protocol such as http or ftp. The should be unique on the server so it differentiates this engine from others on the server.
The program sets the engine's Site property to the VSA site we created and calls InitNew.
It then sets the engine's RootNamespace to MyScriptTest. Any code within the script will be contained within this namespace. You'll see what that means in a bit.
The program then calls the engine's RevokeCache method. This flushes the engine's data out of the Global Assembly Cache (GAC) if there is any in there. If you run the program and execute a script, the script may be stored in the GAC. If you execute a new script, the engine will still contain the old script unless you flush it out in this way.
Next the code sets the engine's GenerateDebugInfo property to True to make it call OnCompilerError if there is an error in the script.
Now the program starts building the script and its related objects. It gets a reference to the engine's Items collection. This collection contains references to script code, libraries, global objects such as TheForm, and other stuff.
The program uses this collection's CreateItem method to make a new IVsaReferenceItem object that refers to the system.dll library. It repeats those steps to make a reference to mscorlib.dll and system.drawing.dll. These references allow the script to use the library's routines.
Next the code creates a reference to the assembly containing the DrawingForm class. The currently executing code is in that class so the program uses [Assembly].GetExecutingAssembly.Location to get the location of the assembly. Remember that the Visual Basic version won't allow this assembly to be in an EXE, it must be in a DLL.
The program then makes a reference to a global object. It names the object TheForm so the script can access it using the name TheForm. The type of this object is MainForm.DrawingForm. MainForm is the root namespace of the project containing the DrawingForm class. Note that this code doesn't contain a reference to an actual DrawingForm object, just to information about the object's type. The vsa_site variable created earlier contains the reference to the current form and its GetGlobalInstance function passes the reference to VSA when it is needed.
FINALLY the code loads the script text. It creates a new item named Script. This name becomes part of the namespace containing the script. It sets the item's SourceText property to the script code.
The program then compiles the script. If there are errors, the engine calls OnCompilerError now. The program checks the engine's IsCompiled property and exits if the script did not compile.
The program then calls the engine's Run method to start the engine running. This doesn't actually run any code (I don't know why they called it Run).
After all this preparation, the program is at last ready to execute the script. It creates a Type object that has the type of the engine's Script module. This item has the engine's namespace followed by Script, the name we gave the code item.
The program uses the Type to initialize a MethodInfo object for the Main subroutine defined in the script code, and uses the object's Invoke method to call subroutine Main.
The code finishes by closing the engine.
|
|
Private Sub RunScript(ByVal script As String)
Dim vsa_engine As New VsaEngine
Dim vsa_site As New MyVsaSite(Me)
' Initialize the engine.
vsa_engine.RootMoniker = "VsaExample://Picture/Draw"
vsa_engine.Site = vsa_site
vsa_engine.InitNew()
vsa_engine.RootNamespace = "MyScriptTest"
' Empty the cache in case there is code in the GAC.
vsa_engine.RevokeCache()
' Generate debug information when compiling.
vsa_engine.GenerateDebugInfo = True
' Get a refernece to the Items collection.
Dim items As IVsaItems = vsa_engine.Items
' Load system.dll and mscorelib.
Dim reference_item As IVsaReferenceItem
reference_item = CType( _
items.CreateItem("system.dll", _
VsaItemType.Reference, VsaItemFlag.None), _
IVsaReferenceItem)
reference_item.AssemblyName = "system.dll"
reference_item = CType( _
items.CreateItem("mscorlib.dll", _
VsaItemType.Reference, VsaItemFlag.None), _
IVsaReferenceItem)
reference_item.AssemblyName = "mscorlib.dll"
reference_item = CType( _
items.CreateItem("system.drawing.dll", _
VsaItemType.Reference, VsaItemFlag.None), _
IVsaReferenceItem)
reference_item.AssemblyName = "system.drawing.dll"
' Load a reference to this assembly.
' Note that VB doesn't support EXEs, only DLLs.
Dim assembly_name As String = _
[Assembly].GetExecutingAssembly.Location
reference_item = CType( _
items.CreateItem(assembly_name, _
VsaItemType.Reference, VsaItemFlag.None), _
IVsaReferenceItem)
reference_item.AssemblyName = assembly_name
' Load a global form object.
Dim global_item As IVsaGlobalItem
global_item = CType( _
items.CreateItem("TheForm", VsaItemType.AppGlobal, _
VsaItemFlag.None), _
IVsaGlobalItem)
global_item.TypeString = "MainForm.DrawingForm"
' Load the script code. The name of this item
' must match the script's Module name to get global
' items to work.
Dim code_item As IVsaCodeItem = _
CType(items.CreateItem("Script", VsaItemType.Code, _
VsaItemFlag.None), _
IVsaCodeItem)
code_item.SourceText = script
' Compile the script code.
vsa_engine.Compile()
' If the compilation failed, exit.
If Not vsa_engine.IsCompiled() Then
vsa_engine.Close()
Exit Sub
End If
' Start the engine.
vsa_engine.Run()
' Execute sub MyScriptTest.Script.Main().
' <engine.RootNamespace>.<Module>.<Subroutine>
Dim vsa_assembly As [Assembly] = vsa_engine.Assembly
Dim my_type As Type = _
vsa_assembly.GetType(vsa_engine.RootNamespace & _
".Script")
Dim method_info As MethodInfo = _
my_type.GetMethod("Main")
method_info.Invoke(Nothing, Nothing)
' Clean up.
vsa_engine.Close()
End Sub
|
|
|
|
|
|