Search The Blog
My Books

New:

My Songs

 

The Art of Unit Testing

Buy PDF or Print book at Manning

Buy on Amazon

Latest Posts
from 5whys.com
Twitter: @RoyOsherove
About this site

TDD in .NET Online Course

TDD and BDD in Ruby Online Course

 

Subscribe!

This site aims to connect all the dots of my online activities - from tools, books blogs and twitter accounts, to upcoming conferences, engagements and user group talks.

« [Article] Make your .Net application support scripting - the practical way | Main | Small Israeli DevDays rants »
Tuesday
Feb172004

Make your .Net application support scripting - a practical approach

Summary

Adding scripting support to your application is one of the most valuable things you can do for your client, letting them add value to your software, and keep it current over time with little or no overhead from the developers. Your users will be able to modify behavior at runtime, change business rules as the market changes and fix subtle bugs as they appear until better fixes come along in the form of compiled code. It is one of the most powerful techniques today employed my many varied business applications. But guess what? Its not very easy to do in .Net. In this article Ill show you how you can use some of the techniques of the past mixed with the .Net framework to add that scripting ability to managed applications, with a touch on a subject that was never considered for scripting: WebServices , including asynchronous calls to them.

 

Scripting in the old days

How was scripting performed in non managed applications? For Visual Basic 6.0 applications, this usually entailed referencing a Microsoft supplied control library the Microsoft Script Control, which provided the developer with a simple object model on which they could call some generic functions. For example Eval(expression) evaluated an expression either in VBScript on in Jscript and returned a result, or Run(ProcedureName,Params) to execute a specific procedure .

The developer could also add pieces of script into the scripting memory at runtime, for example, reading whole script files containing procedures and modules which could then later be called at runtime. Methods such as AddCode(text) took care of this task and were one of the main entry points to scripting applications.

 

Finally, one of the most powerful features in the Microsoft Script library was the ability of the application developer to expose whole object models into the script runtime including all their properties and methods. These objects could later be referenced in the scripts at runtime and manipulated just as if they were declared in the script itself. This allowed amazing features for many applications. The client could literally program against an API exposed by the application runtime, an API that could be just as powerful as the application that was hosting it.

This amazing feature was achieved using the AddObject(name,object) method of the script control and will forever be remembered in my heart as one of my favorite development tasks over the years.

Examples from nowadays

Many applications today take advantage of this ability. For a simple example take a look at an automated build tool called FinalBuilder, or another build tool called VisualBuild Pro. Both of these tools provide a programmable API which exposes events such as BeforeBuild and AfterBuild which can be handled in either VBScript of Jscript. These are perfect examples of when you would need to allow user customization of your application and scripting is a perfect way of doing this.

 

Scripting options today

Today in .Net there are basically two ways to go about adding scripting to your managed application:

  • You could use reflection and CodeCompiler objects at runtime, build .Net code on the fly, compile it and run it. This is a relatively well known method, yet rather costly to do and hard to maintain and debug. Its also too sophisticated to master in the short time spans developers are expected to churn out software these days. There have been some third party attempts (some successful) in providing a generic scripting environment for .Net. Alintex Script is one of those third party products that Im guessing uses this technology. Its still hard to accomplish on your own. Harder than it should be.
  • You could use the Microsoft.Vsa namespace object model to create in memory .Net scripts and compile them at runtime. This is the “preferred“ way to do this but it leaves a lot to be desired in terms of usability and object model design. In short, it's hard to understand and hard to learn and maintain.
  • You could look back and use the good old Microsoft Script Control in your managed projects. This is the solution Ill be focusing on in this article. The main reasons to use this solution are:
    • Its very easy
    • It holds the 80/20 rule. 80% of the features you would look for in scripting your application can be provided using it.
    • Its been tried and tested over a long period of time
    • Easier to maintain, even if a developer with less experience comes along, its easier for them to understand and maintin, since its a pretty small and simple amount of code overall, compared to the first method.

Using the MSScript control in .Net

Its pretty easy to add basic scripting to your application. The first steps will usually be:

  • reference the Microsoft Script Control library
    • In your project (Ill assume its a Winforms project) open the references dialog and select the COM tab.
    • Find the Microsoft Script Control reference and add it to your project
  • Create a script object to work with
    • Declare an object of type MSScriptControl.ScriptControlClass and instantiate it
    • Initialize the scripting language to be used with the object (Either VBScript or Jscript. Ill assume VBScript for the rest of this article
  • Try the simplest scripting task
    • Add a multiline textbox to your main form
    • Add a button with Execute caption and use this code in the Click event of the button:

    Private Sub cmdExecute_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) _

Handles cmdExecute.Click

 

        txtResult.Text = script.Eval(txtResult.Text)

    End Sub

    • script is the name of the script control object and txtResult is the name of the TextBox object.
    • Run your application.. When the window opens, write the following in the textbox:  2*2. Then press Execute. See what happens.
    • Now try writing Msgbox Hello, Scripting world! and press execute

 

Congratulations. Youve just added simple scripting abilities to your application. Sure, its not much, but hopefully youve seen just how powerful you client can get when using scripting.  You should really get a good grasp of VBScript and its abilities to see all the things you can do. But wait.

 

Were not done yet. This is only the beginning. Now its going to get really interesting.

 

Exporting your own .Net objects to the party

One of the things most people dont realize is that eve though this is managed code, you can still expose your own object model through the script control. Yes, even manages classes. In this next lines Ill show you how to do just that.

  • In the Form_load event, after initializing the script control, add the following line:

script.AddObject("form", me, True)

Youve just told the script

-         Expose an object from my application under the name of form.

-          This object is me.

-          Yes, please expose all its members for scripting as well.

  • Now, change the call to script.Eval into this code:

script.ExecuteStatement(txtResult.Text)

Instead of evaluating an expression, youre telling the script control to execute a statement. Now you can do some cool stuff.

  • Run the program and write the following in the text box:

Form.left+= 200

  • Press the execute button  and see what happens.

 

As you can see, .Net has no trouble exposing managed objects for scripting just as easy as it accommodates for COM objects in the program (underneath it wraps the calls to the COM objects in specialized wrapper objects that do all the COM calls in a low level manner, but we dont care about that since its done automagically for us)

 

Some guidelines for adding code at runtime

Theres a better way that to just execute a random procedure the user write everytime. A real application would have a bit more organized manner in which scripting would be implemented. Here are some guidelines:

  • Use the script.AddCode(text) method to add chunks of VBScript code (usually from script files with a .vbs extension) when the application initializes. These chunks can contain helper methods or methods that were changed by the user of your application.
  • You can call specific methods that were loaded in your script from any other method in your script. For example, if you know you have a SayHello method written in the vbs file you load at startup, your user can go ahead and write code that calls this procedure (they just have to know its there documentat you APIs).
  • You can call preloaded methods from your managed code as well using the script.Run(procedureName, paramArray params()as object) method. This allows you to call various user events on predefined application entry points. For example, if you want your users to customize what happens whenever your application shuts down, you can have an empty OnShutDown method in a script file. You will call this method before your application shuts down using the script.Run() method. If your client wants to customize what happens before shutdown, they can simply add VBScript code to the empty body of the method in the script file.
  • You can also send parameters to methods you call dynamically. These parameters can be full blown objects or simple strings and integers. You choose. Your user can accepts the objects in the method parameters and manipulate them just as regular objects. You dont have to expose them before you do this.
  • Since this is .Net and you have all the object oriented power in your hands, you should consider creating a custom class that inherits from the ScriptControlClass class and use that class as your scripting engine. Your custom class will use the constructor to initialize stuff such as the scripting language, loading various script files and adding support for various helper methods you might need in your application to do with scripting.

Where are we?

Weve seen so far that you can expose your own objects to script and you can evaluate various expressions. This is about as far as you can get with scripting in the unmanaged world. Now Ill take you a little further and see how .Net helps us add scripting to something which seems totally out of whack webservices.

 

Calling a webservice from script

As weird as it may sound, your user can call methods on a webservice that your application uses, just as easy as it would call a regular objects. How is this possible? Well, Ive already shown how to ws.BeginHelloWorld expose a managed object to script by using the script.AddObject() method. Exposing a webservice is just as easy. You do it exactly the same way. This is possible because when you use a webservice in your own applications you are essentially using a proxy object, a mediator between your application and the real service. This proxy looks and acts just like the real service but it lives in your own application domain and forwards all calls to the webservice when needed. This means that the webservice instance just another object that can be exposed and used in script much like any other object.

However, there is one caveat:  calling a webservice might take some time to accomplish; meanwhile your application will wait for the script to execute. The script in turn will wait for the webservice to execute and everyone will be looking at you asking why this is so darned slow.  To make this problem go away we can use the same technique we use in a real managed application: calling the service asynchronously.

 

Asynchronous invocation from script

How do we accomplish calling a webservice asynchronously today?

Lets assume our simple webservice has but one method HelloWorld which returns a string. In managed code we would call it like this:

    Public Sub CallWebService()

        Dim callback As New AsyncCallback(AddressOf AsyncWsCallback)

        wsObject.BeginHelloWorld(callback, Nothing)

    End Sub

 

    Public Sub AsyncWsCallback(ByVal ar As IAsyncResult)

        txtResult.Text = wsObject.EndHelloWorld(ar)

    End Sub

 

So heres what we need to have in script for the async call to work:

-         A call to wsObject.BeginHelloWorld passing in a callback object

-         An instance of an AsyncCallback object

-         A call to wsObject.EndHelloWorld(ar) passing in an IAsyncResult object

 

Can you guess how we manage this?

  • When initializing the script we should also expose an AsyncCallback object that will be used in our script when invoking the webservice. This AsyncCallback object will actually be pointing at a managed method. That managed method will invoke (using script.Run() a script method which will be used to do work when the webservice call ends.

 

  • Heres how to initialize the AsyncCallBack object in the script:

     Dim callback As New AsyncCallback(AddressOf AsyncWsCallback)

     script.AddObject("ws", wsObject, True)

     script.AddObject("callback", callback, True)

 

  • And heres the implementation that does not work of the managed callback

    Public Sub AsyncWsCallback(ByVal ar As IAsyncResult)

        script.Run("OnWsEndHelloWorld", ar)

    End Sub

 

It just invokes a script that does processing when the webservice finishes its work.

I wish this implementation would work out of the box, but it does not. If you use this technique, you cant send the IAsyncResult object as a parameter to your script method. Some very weird COM interop issues will happen so let me save you the trouble. The IAsyncResult object can only be used in managed code in this case. Your only option is to call  ws.EndHelloWorld(ar)  in managed code and then call a script method that processes the results. You do not want to do something like this in the script:

 

Sub OnWsEndHelloWorld(ar)

Msgbox ws.EndHelloWorld(ar)

End Sub

 

This is wrong. Your script should process the outcome of the EndHelloWorld as received from the managed code. Otherwise, as the say in SouthPark, Youre gonna have a bad time.

 

  • So, instead, well do this in our managed code

    Public Sub AsyncWsCallback(ByVal ar As IAsyncResult)

Dim result As String = wsObject.EndHelloWorld(ar)

  script.Run("OnWsEndHelloWorld", result)

    End Sub

 

Which leaves us with this piece of script:

 

Sub OnWsEndHelloWorld(result)

Msgbox result

End Sub

 

 

 

So, the final order of operations is this:

  • Somewhere in the script,  there is a call to ws.BeginHelloWorld(callback,nothing)
  • The callback is triggered in Managed code which picks up the result by calling ws.EndHelloWorld and calling another script method with the results.

 

Conclusion

As you can see, its pretty easy to add scripting support to your managed application, exposing an object model to your users, responding to major events in the application lifetime and even invoking webservices asynchronously.

Start thinking what you can do with all this power for your users today.

PrintView Printer Friendly Version

Reader Comments (3)

hey, when I execute
S.ExecuteStatement("form.left += 200")
I get:
"Syntax Error"
I also tried form.left = 200

May 29, 2011 | Unregistered CommenterMike

I'm using the scriptcontrol.run method, and i'm trying to pass my arguments by reference so that i can manipulate them in the vbscript, but msscriptcontrol is taking them by value. is their anyway to send the arguments by reference?

March 11, 2013 | Unregistered CommenterJoshua Sharp

Mike I am trying to execute a script in vb.net under Visual Studio 2012 Pro. I have a variable that contains the string form.menuitem.disable = true where the menu item is being read from an SQL reader. I need to add security to a main menu in the app. I am having no luck...can you tell me what I am missiing? I get a cast exception error on the addobject line......I have been a week researching....Here is the code I am using:

Imports AxMSScriptControl

Public Class MainMenu

Dim Con2 As System.Data.SqlClient.SqlConnection
Dim cmd2 As System.Data.SqlClient.SqlCommand
Dim dr2 As System.Data.SqlClient.SqlDataReader
Dim sqlstr2 As String
Dim mOpt As Object
Dim Script1 As AxMSScriptControl.AxScriptControl

Private Sub MaintainDailyScheduleToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles itmSchedDaily.Click
Dim Form As New SchedDaily
Form.Show()
End Sub

Private Sub MainMenu_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim Script1 As New MSScriptControl.ScriptControl
Script1.Language = "VbScript"
Script1.AddObject("MainMenu", Me, True)
Con2 = New SqlClient.SqlConnection(My.Settings.Con)
Con2.Open()
sqlstr2 = "select * from Programs"
cmd2 = New SqlClient.SqlCommand(sqlstr2, Con2)
dr2 = cmd2.ExecuteReader()
While dr2.Read
mOpt = "Me." & dr2(1) & ".enabled = false"
Script1.ExecuteStatement(mOpt)
Script1 = Nothing
End While
dr2.Close()
Con2.Close()
Dim Form As New Login
Form.Show()
End Sub

April 29, 2013 | Unregistered CommenterGeorge Buchanan

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
Web Analytics