by Jim Karabatsos - GUI Computing
I have stumbled across a really cool control from Microsoft that I think opens up some very cool possibilities in the way that we distribute the logic in our applications. I have for a long time thought that there was some scope to design systems where validation rules are stored in a database and then sent to a slim-but-not-thin UI layer to actually implement. However, the technology to do really effective validation over and above the typical required/min/max/format combination was just too hard, meaning that it was always left for another day and another project.
Well, it seems that Microsoft has provided a very nice wrapper around its scripting engines (VBScript and ECMAScript a.k.a. JScript) in the form of an ActiveX control. You can download this control from the Microsoft web site at http://www.microsoft.com/scripting and you will need to do so to use any of the code from this article.
I have only just managed to get some time to play with this, so of necessity we will be taking a high-level view of it all. I do intend to do some serious work with this over the coming months, however, and I will keep you posted on my progress. For now, however, we will see how we can use the control to run some script made up at run time, and to use scripting code to control some elements on a form.
Let's start by creating the world's simplest programmer IDE.
Open a new project and add the ScriptControl component to your toolbox. Then place an instance of this control on your form. I renamed the control to "SC" because repeatedly typing "ScriptControl1" quickly becomes irksome.
Place a text control and a button on the form, and make the text control MultiLine with both scrollbars enabled. Set the Text property to be some valid VBScript code, using Sub Main as a starting point. Here is an example of what my form looks like at design time:
The control with the sizing handles is the Scripting control, which will be invisible at run time.
In the Click event of the button, place this code:
Private Sub Command1_Click() SC.Reset SC.AddCode Text1.Text SC.Run "Main" End Sub
Remember, if you have called your control something other than SC, then you will need to adjust the code accordingly.
Go ahead, what are you waiting for? Run the thing. Press the button to run the code, then play with it a while, changing the code and running it. Note that the code behind the button is calling the Main procedure. You could change this to whatever you like, and indeed the control exposes the procedures in a collection that you could interrogate to get a list of callable Subs and Functions.
You will probably want to get some indication when an error occurs. The control raises an Error event and exposed an Error object for this purpose. Try this simple code as a starting point (add it to the form):
Private Sub SC_Error() MsgBox SC.Error.Description End Sub
Try introducing some errors in your script code and see what comes up. Also, browse the properties the Error object property of the ScriptControl as it has some cool functionality that goes beyond VB's Err object.
OK, let's move on. You really want script code to be able to do something with or to the objects that form your application. To play with this in a totally generic way (that is, divorced from any particular application), I created a second form in the application that looks like this:
The red boxes are a control array of shapes called Segment(0..7) with 0 being the top one, then working clockwise, and finally the centre segment is 6 and the dot is 7. It is meant to look like a typical LED display (hey, I am NOT a graphics person!). I called the form frmDigit.
I added the following public method to the form:
Public Sub Toggle(ByVal Index As Integer) If Index > -1 And Index < 8 Then Segment(Index).Visible = Not (Segment(Index).Visible) End If Refresh End Sub
Not exactly rocket science, now is it?
Now, go back to the main form and add a new button and put this code behind it:
Private Sub Command2_Click() Dim sCode As String Dim objLED As New frmDigit objLED.Show sCode = "Sub Cycle(ByVal HowManyTimes)" & vbCrLf _ & " Dim I, J" & vbCrLf _ & " For I = 1 to HowManyTimes" & vbCrLf _ & " For J = 0 to 7" & vbCrLf _ & " LED.Toggle(J)" & vbCrLf _ & " Pause 1" & vbCrLf _ & " Next" & vbCrLf _ & " Next" & vbCrLf _ & "End Sub" & vbCrLf _ & "Sub Pause(ByVal HowLong)" & vbCrLf _ & " Dim T" & vbCrLf _ & " T = Timer + HowLong" & vbCrLf _ & " Do While Timer < T" & vbCrLf _ & " Loop" & vbCrLf _ & "End Sub" Text1.Text = sCode Refresh SC.Reset SC.Timeout = NoTimeout SC.AddObject "LED", objLED SC.AddCode Text1.Text SC.Run "Cycle", 3 End Sub
Notice how we create an instance of the frmDigit form and assign it to an object called LED. Then we give the scripting control a reference to that object by way of the AddObject method. We are using two parameters here. The first parameter, the string, is the name by which script code will refer to the object. The second parameter is the object itself. Look at the code that is being inserted into the script object. It just accesses LED as if it was a pre-defined object in the environment (which, within the scripting context, it is). Again, run this and play with it a bit.
You can add multiple objects into the scripting control and they do not need to be VB objects. You can, for example, add objects to external ActiveX server objects like Excel and Word.
Also notice how I have set the Timeout property to the constant NoTimeout (-1). If you omit this, the script engine complains that the script is taking "longer than expected" although how it knows what to expect is beyond me. You can set the Timeout to a number of milliseconds if you want to give it a clue.
For my last trick, I will dispense with the control altogether. It is possible to create a script control out of thin air, unlike most OCXs. Here is the same code as before, but using a ScriptControl object that is created at run time:
Private Sub Command3_Click() Dim sCode As String Dim LED As New frmDigit Dim oSC As Object Set oSC = CreateObject("ScriptControl") LED.Show sCode = "Sub Main()" & vbCrLf _ & " Dim I" & vbCrLf _ & " For I = 1 To 3" & vbCrLf _ & " LED.TurnOn 3" & vbCrLf _ & " Pause 1" & vbCrLf _ & " LED.TurnOn 6" & vbCrLf _ & " Pause 1" & vbCrLf _ & " LED.TurnOn 0" & vbCrLf _ & " Pause 1" & vbCrLf _ & " LED.TurnOff 0" & vbCrLf _ & " Pause 1" & vbCrLf _ & " LED.TurnOff 6" & vbCrLf _ & " Pause 1" & vbCrLf _ & " LED.TurnOff 3" & vbCrLf _ & " Pause 1" & vbCrLf _ & " Next" & vbCrLf _ & "End Sub" & vbCrLf _ & "Sub Pause(ByVal HowLong)" & vbCrLf _ & " Dim T" & vbCrLf _ & " T = Timer + HowLong" & vbCrLf _ & " Do While Timer < T" & vbCrLf _ & " Loop" & vbCrLf _ & "End Sub" Text1.Text = sCode Refresh oSC.Language = "VBScript" oSC.Reset oSC.Timeout = NoTimeout oSC.AddObject "LED", LED oSC.AddCode Text1.Text oSC.Run "Main" End Sub
This is pretty much the same as the last code, except that the object is created dynamically, showing how this control can be used from development environments that do not support visual tools.
(Hmmm, it occurs to me that you could create an instance of a ScriptControl from a VBScript program. Of course, if you did that, you might disappear into your own navel, so be careful.)
Let me highlight a couple of important points that are emphasised above. First, you cannot early-bind to the control. VB gets very confused, so you need to define the object reference as an Object (or a Variant if you prefer) and use CreateObject as shown.
Second, the Language property (which defaults to "VBScript" when using the sited control) is not initialised with a default value when used this way. That's why I explicitly set it in the code.
Finally, I changed the script to do something different with the display. I added simple TurnOn and TurnOff methods to the Digit form to support this. I spent far more time playing with this ridiculous LED display than I would like to admitů
That's about all for now. This is very cool technology and I am quite excited about the possibilites it opens up. Email me with your own ideas.