
Key Audience: Those needing .NET scripting access to code objects
This post is for all the people out there like me that need to keep scripts created for .NET 1.1 alive today using later versions of .NET, particularly those people who rely on GetGlobalInstance to create global items to attach to objects used in code so that they can be accessed by a script (Like the Response and Request objects are in ASP, or in our case, the Locura Media Server).
Keywords you may be looking for are: IVsaGlobalItem, <engine>.Items.CreateItem, and VsaItemType.AppGlobal.
Introduction
It wasn’t long ago I personally had the task of updating our personal media server to the latest version of .NET for our upcoming redesign.

Step one was simply pulling all the dll's into Visual Studio 2008 and hitting Shift-Ctrl-B (Build All). Now it is common coding policy at VillainousMind, Inc. to treat warnings as errors, keeping with Code Once philosophy. Unfortunately, that lead to the following compiler-created message:
Use of this type is not recommended because it is being deprecated in Visual Studio 2005; there will be no replacement for this feature. Please see the ICodeCompiler documentation for additional help.
Hitting this roadblock was annoying. Not only did Microsoft deprecate the only scripting facilities they built into our current .NET flavor at the time, but they left no easy tutorial on how to convert.
The first thing I did was search for how to use the ICodeCompiler correctly, which lead me to the CodeDomProvider. Typically the suggestion is to use this with predefined inherited classes:

As VBScript.NET is now hard to come by, I had to use CodeDomProvider.CreateProvider(<language>). Once I made it past this series of discoveries, I realized the best thing to do was to take the information I had found and make a test project. The final project can be downloaded freely from the VillainousMind website. You may simply want to use this code and snoop around, using this post simply for reference, or you may want to read the blog and use the code as supporting material. Either way, feel free to use it to work out your own solutions as needed.
The Problem
One of the key aspects of our media server is that we provide robust scripting capabilities for a wide variety of user-customization. In order to apply the customer service golden rule we have to do what is in our power to make existing scripts work with our new engine. Our current engine works much like classic ASP, or the actual ASPX pages in .NET where HTML and script can be combined. In fact, since the Locura Media Server is a web server, many of the same objects are present: Server, Response, Request, etc.
The problem comes in right here: these global scripting objects, objects that are actively used in regular code but must be available to scripts, don't have an easy method of exposure, or at least not any method that I could easily find through Google searches. So here was my dilemma: how do I keep existing scripts viable by providing global access to my code objects?
The Solution
When I finally realized the solution I was glad that we chose VBScript.NET as our default scripting language (of course with 1.1 we had little choice). With VBScript.NET modules were in play meaning I could add global variables. Now other languages may not afford this, but with a simple string.Replace() and static methods, you should be fairly close.
The Test Project Implementation
The Script
For my test project I first scripted a module:
Module GlobalObjects
Public Response As Response
End Module
Next, I had to create an object who's only real job is to set the global object. (There may be a better way to do this, but I could not find a way of setting a global object directly; if you find it, please post a comment.) This object I called Middleman:
Public Class MiddleMan
Public Shared Sub SetResponse(globalResponse As Response)
Response = globalResponse
End Sub
End Class
At this point all existing scripts that need a global Response object should work. For my test project I created a simple test script object to act as the "pre-existing" script. This is the script that actually uses the global Response object:
Public Class Test
Public Sub TestToFile()
Response.WriteLine(String.Format("In script - initial value: {0}", Response.TestValue.ToString()))
Response.TestValue = 2349
Response.WriteLine(String.Format("In script - altered value: {0}", Response.TestValue.ToString()))
End Sub
End Class
Note that I write out Response.TestValue, change it, then write it out again. Later in this article, the code that compiles the script will do something similar so that we can verify that changes to the object in code affect the object in script and vice versa.
The Compilation
First of all, before any of this will work I had to create a separate project containing the object that I wanted available in script. In my sample this is ScriptingTestLibrary which contains the Response class.
For compilation, I want to include this dll in my referenced assemblies:
// Set the standard compiler parameters.
CompilerParameters paramList = new CompilerParameters();
paramList.GenerateExecutable = false;
paramList.GenerateInMemory = true;
paramList.IncludeDebugInformation = false;
// Set the referenced libraries.
paramList.ReferencedAssemblies.Add("System.dll");
paramList.ReferencedAssemblies.Add("System.Windows.Forms.dll");
paramList.ReferencedAssemblies.Add("ScriptingTestLibrary.dll");
// Get the scripts and compile.
string[] scripts = new string[1] { GetScriptFromFile(filename) };
CodeDomProvider provider = CodeDomProvider.CreateProvider(language);
return provider.CompileAssemblyFromSource(paramList, scripts);
Obviously the Compile method I created is limited as it only allows for one script to be passed. Normally this would not be a scalable solution, but as this is only a proof-of-concept program it makes more sense to code for time instead of scalability.
At this point I use the returned CompilerResults object to log any error information or a success message. Then, if everything compiled correctly, I call the Run method which does the work of setting and calling my global objects.
Setting the Global Objects
The last step is setting the global objects in code. The first step is to create an instance of both the entry object, the object that starts the actual script, and the middleman object, the object that sets the global objects.
// Pull the ext object out.
object entryObject = compiledAssembly.CreateInstance(entryClassName);
object middleManObject = compiledAssembly.CreateInstance(MiddlemanClassName);
Next we call the method on the middleman object that sets the global objects. Note that the globalItems collection is a list of GlobalItems structs which contain the object and the method name called on the middleman object. This is passed into the Run method.
// Here is where we hook into our existing object.
foreach (GlobalItem globalItem in globalItems)
middleManObject.GetType().InvokeMember(globalItem.SetMethodName, BindingFlags.InvokeMethod, null, middleManObject, new object[1] { globalItem.Item });
Finally, we invoke the entry method on the entry object.
// Run the entry object and method.
entryObject.GetType().InvokeMember(entryClassMethod, BindingFlags.InvokeMethod, null, entryObject, null);
Testing the code
To make all this work I created a test UI where I can provide the key pieces of information to compile the program and run it. The key to this is in the CompileAndRun method in the ScriptFileCompilerForm object.

The first thing is creating a Response object, setting it's test value, and creating a GlobalItem containing this object.
// Set up the global response object and set the initial value.
Response response = new Response(outputFilename);
response.TestValue = 6320;
// Set up the global items.
GlobalItem globalItem = new GlobalItem();
globalItem.Item = response;
globalItem.SetMethodName = setMethodName;
The next step is the meat of this method: The initial value is captured, the script is compiled and run, and the initial value and altered value are output.
// Save the test value, Compile and run it, then print out the altered test value.
int initialTestValue = response.TestValue;
ScriptingEngine engine = new ScriptingEngine(outputFilename);
bool success = engine.CompileAndRun(language, filename, new GlobalItem[1] { globalItem }, entryClassName, entryClassMethod, MiddlemanClassName);
if (success) response.WriteLine(string.Format("In code - initial value: {0}\r\nIn code - altered value: {1}", initialTestValue, response.TestValue));
The rest of the method does nothing more than show the outcome and ask the user is she wants to see the results.
// Show results.
string resultText = "succeeded";
if (! success) resultText = "failed";
if (MessageBox.Show(string.Format("The compilation and run {0}.\r\rDo you want view the output?", resultText),
"Compilation Results", MessageBoxButtons.YesNo) == DialogResult.Yes)
ShowOutput();
Finally, when the program is run and the script compiles and runs successfully, the following output is created showing that the changes to the object in script affected the object in code and vice versa. In other words, this object is one and the same, not a copy.
Build successful
In script - initial value: 6320
In script - altered value: 2349
In code - initial value: 6320
In code - altered value: 2349
There is a bit more to the program, but overall it's pretty straightforward both from a code standpoint and a UI standpoint. Again, the code is freely available from the VillainousMind website.
Summary
Overall, this is a simple solution to implement global objects in VBScript.NET in order to access code objects with script. Its purpose is to make scripts created with VSA compatible with newer .NET methods of scripting using ICodeCompiler (or CodeDomProvider).
This method creates a VBScript.NET module for global objects and a VBScript.NET middleman object to set this object. It also uses a referenced assembly in order to use this global object. The code then uses the middleman object to set the global object which can then be used in script.
Feel free to comment any thoughts or additions to this methodology. I would be happy to hear a similar method for C# using static methods and some type of alias instead of using string.replace, but as it is not needed for my project, it is on a back-burner list.
If you were running across the same issues I was, I hope this was helpful to you.

0 comments:
Post a Comment