Download Code: CsJScriptDemo.zip 46KB
I didn't find much on the 'net when I was looking for ways to do this so here is my solution for adding basic scripting functionality to a C# application with an object model. This method uses CodeDOM to generate and compile a JScript.net class on the fly, using the classes and interfaces from the C# application. It uses .Net 2.0, and a Visual Studio 2005 solution is provided.
For the purposes of this exercise, I have a windows forms application as shown below. The application has a Listbox bound to a collection of items on the left, a Textbox to type scripts into on the right and a Button to execute the script.
For the object model I have 2 objects:
- Application has a Title property that changes the title of the main form, and a generic collection of Item objects which is bound to the list on the left.
- Item has a single property Text.
Jumping into the code, the Item and Application objects are shown below. Note that both objects inherit from JSObject to get the associative array behaviour that you would expect in javascript, but it is not required. The Application constructor has a reference to the Form object so that we can change the text.
public class Item:Microsoft.JScript.JSObject
{
private string _text;
public Item(string text)
{
_text = text;
}
public string Text { get { return _text; } set { _text = value; } }
}
public class Application:Microsoft.JScript.JSObject
{
private Collection<Item> _items;
private System.Windows.Forms.Form _form;
internal Application(System.Windows.Forms.Form form)
{
_items = new Collection<Item>();
_form = form;
}
public Collection<Item> Items { get { return _items; } }
public string Title { get { return _form.Text; } set { _form.Text = value; } }
}
The main form has an instance of the Application object which will be passed to the script when it is executed.
private Savage.CsJScriptDemo.Model.Application _application;
public Form1()
{
InitializeComponent();
_application = new Savage.CsJScriptDemo.Model.Application(this);
UpdateList();
}
private void UpdateList()
{
this._listBox.DataSource = null;
this._listBox.DataSource = _application.Items;
this._listBox.DisplayMember = "Text";
}
To execute the script I have a single method on an interface called ICompiledScript.Go which takes the Application instance. You can change this or add more methods that with different arguments and return types if you need to. The JScript will be compiled into a class that implements that interface, so that we don't need to use invoke to call the methods.
ICompiledScript compiled = ScriptCompiler.Compile(_scriptTextBox.Text);
compiled.Go(_application);
UpdateList();
I'll refer you to the source code attached for the entire ScriptCompiler class because CodeDOM is quite verbose as you can see from the fragment below:
//could easily be C# or VB
CodeDomProvider codeProvider = new JScriptCodeProvider();
//Namespaces
CodeNamespace codeNamespace = new CodeNamespace("some.arbitrary.name.space");
codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
codeNamespace.Imports.Add(new CodeNamespaceImport("Savage.CsJScriptDemo.Model"));
//Scripting Class
CodeTypeDeclaration codeTypeDecl = new CodeTypeDeclaration(className);
codeTypeDecl.IsClass = true;
codeTypeDecl.BaseTypes.Add(typeof(object));
codeTypeDecl.BaseTypes.Add(typeof(ICompiledScript));
codeNamespace.Types.Add(codeTypeDecl);
//...snip
There are no special tricks in the ScriptCompiler class, it creates a class that implements ICompiledScript, adds a constructor and the Go method containing the script provided. The assembly is then compiled in memory and an instance of the class returned.
There are some quirks in this method that you'll need to experiment with for your application e.g.
The case of toString() is ToString() so either implement the other method or use a regex to replace the offending text before compiling the script.
You will run into code access security permissions at some point if you are accessing files or UI elements etc.
Your JScript class cannot inherit from a C# class but you can implement interfaces.
Because you are referencing the C# assembly, all the public classes exposed in that assembly are available to the JScript (if the user knows the namespaces) so be careful what you make available. You could also use regex to check for those kind of things before you compile.