Creating Customizable Web Services
September 10, 2001
Note The code sample download originally included with this article is no longer available.
Earlier this year, I introduced Microsoft® Visual Studio® for Applications (VSA) and Script for the .NET Framework, and how you could use these technologies to make your .NET application customizable. Defining what a .NET application exactly is can be somewhat of a thankless task, since everyone seems to have a slightly different idea of what an application is and whether or not it's .NET. However, one technology is common among most definitions of a .NET application—Web Services. So this month I'll show how to use VSA and Script for the .NET Framework to build a Web Service that is customizable, and in particular, how you can take advantage of Web Services to help build an infrastructure to enable your users to build and deploy the script code that will customize the Web Service.
Why Build Customizable Applications?
Before going into the technical details of using Visual Studio for Applications to build customizable applications, I thought I'd step back and look at why building customizable applications is so important and appropriate when building a .NET application. If you're already sold on the idea, then feel free to skip this section.
When designing and writing an application, you always try to meet your users' requirements, providing them with a program that solves their problems and generally makes their life easier. However, despite all the effort you put into gathering requirements and involving users throughout the building of the application, it's pretty much a forgone conclusion that your finished program will not meet all user requirements. Requirements have a knack for changing over time (darn it!)—and once users start to use your program in earnest, they'll inevitably think of new things they'd like your application to do to help them.
Deciding which user requirements and problems to solve becomes doubly difficult if you're trying to write software that's going to be used by more than one company. For example, if you're writing an order processing system, what are the chances that you'll be able to provide the correct business rules for 100% of your customers? There is really no way a generic piece of software can meet everyone's requirements without some customization to the product.
How you choose to make your application customizable can have a significant impact on your users, on your ability to upgrade, as well as on the salability of your product. You have a number of options open to you (in no particular order of importance):
- Provide no customization ability
I'll assume that this isn't too palatable an option for all the reasons I've just gone through.
- Provide the source code to your application so that your customers can change it to meet their requirements
This offers some benefits, in that it essentially gives your customers complete flexibility and control over how your product is implemented. This may be a viable and attractive option. However, a number of issues arise with this approach, including problems with upgrading to newer versions and protecting your intellectual property. I won't go into the intellectual property debate (enough people are discussing this on the Internet without me jumping in), but the upgrade issue impacts all of us.
For example, let's say you provide the source code to version 1.0 of your product, and some of your users change the implementation of a particular component. (I'll use the tax calculation component of an order processing system as an example.) Let's say that now the internal implementation is different, and the external interface of the component is changed to integrate with the customer's in-house invoicing system. The good news is, the customer now has successfully managed to integrate your system with theirs and they're happy.
In the mean time, you begin work on version 2.0 of your product, and the customer is interested in buying it because of the new functionality you've added. You come to install version 2.0 of the product, but find that because the tax calculation component has been changed, there is no easy way to upgrade—other than trying to merge the changes you added to the component with those made by the customer.
On the surface this doesn't seem to be a major issue, but it becomes an issue very quickly when you multiply the changes throughout the system. Imagine if small changes had been made throughout the entire system to meet the customer's requirements. Providing an upgrade from 1.0 to 2.0 becomes incredibly difficult if you want to keep all the customizations that the customer has made to the system. In fact, it can quickly lead to upgrade paralysis, whereby the customer is stuck with an old version of your software, irrespective of whether they want to upgrade to the latest version—and you're stuck trying to support many different versions of your software.
- Provide a scripting solution for your application
Building scripting into your application provides a mechanism to customize your application without having to necessarily expose the source code of the application to your customers. It also allows you to design specific customization points in your application. Designing the customization points for your application is probably the most important (and sometimes most time consuming) part of your application's design, if you are to maximize your investment in customization. A well thought out customization design will not only save time when customizing your system, but will also enable you to have some control of what gets customized and when.
A major advantage that the scripting approach has over source code is that the customizations can survive product upgrades. For example, script code that changed the implementation of the tax calculation component will continue to work when version 2.0 of the component is installed (assuming that the object model is backwards compatible of course), since the customization is part of the script and not the entire implementation of the component.
Visual Studio for Applications provides a mechanism to build on the benefits of integrating Script for the .NET Framework into your application. Namely, it provides a full-featured, integrated development environment for your customers to write and debug scripts.
- Provide a way for customers to plug components into your application
This is an interesting option and provides many of the advantages of the scripting solution, but can provide a number of challenges to you, the application developer and customizer of your application. This option is somewhat analogous to the "add-in" model provided by Microsoft® Office and Visual Studio. Your application provides an integration interface components need to adhere to, as well as a mechanism for loading the add-in component into your application at the appropriate time.
Implementing this approach can make considerable sense if the majority of people doing the customization of the applications are experienced developers who routinely create components and understand interfaces well. For this reason, I think this is a complimentary feature to scripting and shouldn't be seen in opposition to adding scripting to your application. That said, there are a number of challenges involved in this approach when developing an application—in particular the amount of work involved in implementing the solution. You need to develop an integration interface, a mechanism for loading the components into your application, and provide documentation on the integration mechanism. This doesn't necessarily need to be difficult, but it's something you wouldn't need to do if you chose the scripting route.
Developing a Customizable Web Service
To illustrate how to use Script for the .NET Framework and Visual Studio for Applications to build a customizable Web Service, I've built a simple calculation Web Service for use on fabrikam.com. The Web Service implementation is deliberately simple, so that I can concentrate on showing how to integrate scripting, rather than dwelling on the code required for the tax calculation. The Web Service has 2 methods, CalculateTax and CalculateDiscount, and will be called by the order processing system in the fabrikam.com e-commerce Web site. The service itself is implemented entirely in Microsoft® Visual Basic® .NET, but could be equally well developed in any .NET language. The Web Service hosts a Script for the .NET Framework engine to run scripts to customize the implementation, and to assist the script writer, provides an object model that represents the state of the request coming into the Web Service.
The Web Service is designed to be called from other Web applications, both on the server and the client (using the Internet Explorer Web Services Behavior). Since the usage is designed to be purely Web based, I thought it was important that the integration of the Visual Studio for Applications Integrated Development Environment (VSA IDE) be callable from a Web interface as well. To accomplish this, I've built a simple system that uses XML and MIME file mappings to allow the VSA IDE to be launched from a Web page hosted in a Web browser. More on how this works later.
The sample uses the Visual Studio for Applications Software Development Kit (VSA SDK) to host both the Script for the .NET Framework engine and the VSA IDE. One of the key design points of the VSA SDK is the ability to allow the application hosting VSA to determine the storage format and location of the script code the user writes. To accomplish this, the hosting classes that ship with the SDK provide a mechanism for application developers to plug in their own persistence mechanism. This is achieved by writing a component, known as a code provider that implements a persistence interface, ICodeProvider, defined in the VSA SDK. The hosting classes in the VSA SDK will use the code provider for all persistence and retrieval of script code in the application, both at design time (from within the VSA IDE) and at runtime when the script is loaded.
Since the person writing the script in the VSA IDE is likely to be on a different machine than the one on which the script code is going to be stored and run, it is important that code providers be callable remotely. So, the VSA SDK is designed to use code providers remotely, as Web Services using SOAP over HTTP. This example application uses a code provider as a Web Service for the VSA IDE, and locally as a .NET component when loading the code at runtime to customize the Web Service.
Figure 1. Architecture of Fabrikam.com Calculation Web Service
Now that you hopefully have a general understanding of how the system hangs together as a whole, I'll get into the details of the implementation.
Implementing the Web Service
The calculation Web Service is implemented as a standard .NET Web Service, calculate.asmx, with the two methods defined as functions in a class with the WebMethod attribute applied. The WebMethod attribute tells the ASP.NET runtime to expose the method, so it can be called by a Web Service—nothing special so far.
To enable the Web Service to be customized, a script engine needs to be loaded to run any script code written by the user. In my last column, I used the Script for the .NET Framework interfaces directly, and loaded the source of the script code every time. This was fine, since it was a client application, and the performance of the script code wasn't the primary concern of the application. Since this is a Web Service that will be used on a server, however, runtime performance is key. For this reason, I'm going to use the ability of the Script for the .NET Framework engines to load precompiled code—in particular the light-weight script engine that can just load precompiled code (I referred to this last month as the Loader). Luckily the VSA SDK provides a runtime integration class that makes hosting the engine a snap, so I've used that to integrate the script code into the Web Service.
The VSA SDK runtime class is designed to provide an abstraction of the IVsa interfaces, and to provide an efficient way to integrate the engines into your application. Since it's likely that your application might integrate the script engines in a large number of components, we tried to ensure that the amount of code required to use the class be kept to a minimum. I was particularly keen that integration could be done in 3 lines of code. (Three lines of code seemed to be the sweet spot for minimalist coding. Although why 3 lines was the important number has been lost in the mists of the VSA design process.)
The constructor of the Calculate Web Service creates an instance of the runtime class and code provider, which will be used to load the compiled script for the component. I will cover how the code provider is implemented later on in the article, but it is a class that implements ICodeProvider and is already included in the project, so it's just a question of creating a new instance.
The runtime class has a number of overloaded constructors. In this case, I've used what I believe is the going to be the most commonly used constructor, which takes the name for the customization and the moniker to be used. The moniker is probably one of the most important concepts in VSA, since it is vital for both the Script for the .NET Framework and VSA IDE. Essentially, the moniker is a unique identifier for the script code, and as such you should be careful about how you construct it. The moniker is also important for saving and retrieving any code through the code provider. A code provider essentially translates a moniker into the storage mechanism for your application. For example, the code provider in this example translates
com.fabrikam://calculate into vsa projects\calculate folder hierarchy under the current working directory.
Once the instance of the runtime class has been created, all that is left to do is to provide the instance of the code provider that the class is going to use to retrieve any script code, tell the class to load the code, and then run the code—all in three lines of code! (OK, so I wasn't counting the creation of the instance of the code provider or the class, but they don't count on my lines of code meter.)
Try ' Create a new instance of the code provider myCodeProvider = New DiskCodeProvider() ' Create a new instance of the runtime class myRTClass = New Runtime("calculator", "com.fabrikam://calculate") ' Set the code provider to the instance of the disk code provider myRTClass.SetCodeProviderLocal(myCodeProvider) ' Load the compiled code myRTClass.Load(Nothing) ' Run the code myRTClass.Run() Catch e As Exception ' Throw a particularly useless exception telling users that something went ' horribly wrong Throw New Exception("Unable to load the customization") End Try
The Web Service is now ready to run whatever script the user feels fit to throw in it's direction, but so far we haven't provided any object model for the script to write against. The runtime class provides two ways to add objects to the engine: AddObject and AddEventSourceObject. AddObject adds the object to the global scope of the engine, but the script code can't respond to any events fired by these objects. AddEventSourceObject adds an object to either a class or a module, and not surprisingly, events sourced from this object can be scripted.
To keep things simple, the Calculate Web Service has one object that fires events called CalculateObjectModel (catchy name eh?). I'll be upfront with you here, there's a bug in Beta 2 whereby the runtime class checks to see if the object instance you provide is not null. It would make sense to add the eventsourceobject to the engine in the constructor, but I wanted to use a constructor on the object to set some internal read-only properties, based on values passed into the Web method. Since those values aren't available at construction time, I had to move the addeventsourceobject calls into each method. The final release of Visual Studio for Applications doesn't have the check for the value of the instance, which would allow me to pass in an empty instance and then create the instance when the methods are called.
Getting the Object Model Right
When designing the Web Service, I tried to think about how the user would customize it to meet their needs. As such, I spent some time designing the object model. The model I eventually went with was one where an event is fired before and after the tax calculation. I felt that this would give more flexibility, since it allows for the majority case—implementing your own tax calculation routine—and also allows the script author to do some post-calculation processing should they wish to.
Once I'd decided on the basic pre- and post-processing event model, I starting thinking about how I could implement the object model to make the scriptors life easy—without impacting the external design of the Web Service. An important design point is to ensure that the Web Service remain essentially stateless in actual implementation while yet providing an object model that makes accessing the internal state of each Web method call easy from the script. To do this, I created a new class that implemented the object model the script writer would see, and used that from within each Web method.
To illustrate, I will explain how I implemented the CalculateTax method on the Web Service. This method is pretty simple. It takes an amount, and the state for which the tax is to be calculated for, as arguments. (I know this is a very U.S.-centric method, but it's a demo after all.) The amount and state information isn't persisted outside of the method, to ensure that the Web Service remains essentially stateless in implementation. Stateless programming has significant benefits for scalability, but doesn't necessarily make programming against easy—especially if you are not an experienced programmer, which is quite often the case for those writing the script to customize your application.
To assist the script writer, I created a simple object model that exposes the state presented in the Web method as read-only properties (the values are set in the constructor of the class) and a number of read/write properties that can be used to return the result of the script customization. In addition to properties, the object also exposes the event model that the script writer will use to code against. I used a simple naming convention BeforeEventName and AfterEventName for the pre- and post-processing events.
Public Class CalculateObjectModel ' members vars for holding prop values Private m_amount As Decimal Private m_state As String Private m_taxAmount As Decimal Private m_discount As Decimal Private m_custID As Integer ' Define the events Event BeforeCalculateTax() Event AfterCalculateTax() Event BeforeCalculateDiscount() Event AfterCalculateDiscount() ' Constructor when being used for setting the amount and state Public Sub New(ByVal amount As Decimal, ByVal state As String) m_amount = amount m_state = state m_custID = Nothing End Sub ' Constructor when being used for setting the amount and custID Public Sub New(ByVal amount As Decimal, ByVal custID As Integer) m_amount = amount m_custID = custID m_state = Nothing End Sub ' Bunch of properties to make it easier for the scripter to write code Public Property Discount() As Decimal Get Return m_discount End Get Set(ByVal Value As Decimal) m_discount = Value End Set End Property Public Property Tax() As Decimal Get Return m_taxAmount End Get Set(ByVal Value As Decimal) m_taxAmount = Value End Set End Property Public ReadOnly Property Amount() As Decimal Get Return m_amount End Get End Property Public ReadOnly Property state() As String Get Return m_state End Get End Property Public ReadOnly Property CustID() As Integer Get Return m_custID End Get End Property End Class
Providing the Object Model to the Sscript
A well-designed object model is great, but it's of no use if it's not provided to the script engine, and the events on the object aren't fired in the correct order. The CalculateTax method in the Web Service doesn't actually do a huge amount in my example, other than to create an instance of the object model, fire the BeforeCalculateTax event on the object model, return the tax amount from the object, and fire the AfterCalculateTax event. Obviously a real Web Service would do a bit more inbetween firing the events.
During the implementation of the method, I ran into an interesting challenge, which I think many of you will also run into when you start to write your object models. Namely: How do I raise an event in a new instance of a class? Typically you'd use the RaiseEvent method to fire the event, but unfortunately the code that fires the event is not running in the instance of the object model, so there needs to be some level of indirection between the object model and the calling code.
Initially I thought this would be pretty simple to solve—just implement a FireEvent method on the object model and pass the name of the event you want to fire, and that would be that. This works well, but with one unfortunate sideeffect: the FireEvent method shows up to the script writer, which could lead to some pretty interesting deadlock situations. Imagine if the event handler for BeforeCalculateTax called the FireEvent method with BeforeCalculateTax as the event name—much endless looping and gnashing of teeth would ensue. Luckily, this can be easily sidestepped by making the FireEvent method internal, or "friend" in Visual Basic parlance, which means that the method is only visible to code running in the same assembly and therefore invisible to the script code.
<WebMethod()> Public Function CalculateTax(ByVal amount As Decimal,_ ByVal state As String) As Double ' Create an instance of the object model for the script ' and set the amount and state in the constructor CalcOM = New CalculateObjectModel(amount, state) Try ' Work around for VSA Beta 2 not being able to take an empty instance ' of an EventSourceObject before the engine is run myRTClass.AddEventSourceObject("CalculateCustomization", "CalcOM", CalcOM) ' Need to reset the engine to ensure the new eventsourceobject is available myRTClass.Reset() ' Run the code again myRTClass.Run() Catch e As Exception ' Throw the exception Throw e End Try ' Call the FireEvent method on the object model to fire the ' BeforeCalculateTax event CalcOM.FireEvent("BeforeCalculateTax") ' Of course in reality you would probably do some generic calculation here ' Add $1 to the tax to show that something happened CalcOM.Tax += 1 ' Call the FireEvent method on the object model to fire the ' AfterCalculateTax event CalcOM.FireEvent("AfterCalculateTax") ' Return the amount of tax calculated from the object model Return CalcOM.Tax End Function
When the Web Service is called, the script code will actually implement the tax calculation, and the service will return the result from the script through the Tax property on the object model. All that remains now is to implement the ability for the script writer to actually write the script and save it to the server.
Providing a Development Environment for the Script
One of the major advances that Script for the .NET Framework and VSA provides over Microsoft® Windows® Script is the full-featured VSA IDE, which provides a first-class editing and debugging environment for script writers to create their customization scripts. The fabrikam.com Calculation Web Service takes full advantage of the VSA IDE for both editing and debugging the script code. Integrating the VSA IDE into your application is achieved by using the design time engine for the script language you want to use. (In Visual Studio for Applications Version 1.0, we only had time to do a design time engine for Visual Basic .NET.)
The key design point for the Design Time engine was that it should have the same interface as the Script engine (IVsa) for managing things like code items and objects (so that you don't have to learn different hosting interfaces for common functionality). Yet it should additionally implement a set of Design Time interfaces (IVsaDT) to provide access to the VSA IDE. The VSA SDK provides a Design Time integration class that makes integrating the VSA IDE easier, much in the same way as the runtime class makes it easier to host the script engines.
Since the Web Service doesn't have a dedicated Windows client, I designed a mechanism whereby the VSA IDE can be instantiated from a Web browser. Since the VSA IDE is controlled by a set of interfaces, calling the IDE from script within a Web page wasn't an option. Windows Script can only call objects through IDispatch, and the VSA IDE can do potentially unsafe things (like write and read from disk), so this wouldn't work within the security framework for the vast majority of Web sites. To provide access to the VSA IDE from within a browser, I've taken advantage of the MIME handling feature of Internet Explorer to create a .NET Windows Forms application, interpret an XML file (generated from a web server), and use the IVsaDT interfaces to launch the VSA IDE. Before I go into the details of how the program does this, it's important to explain how Microsoft® Internet Explorer MIME handlers help in this situation.
Internet Explorer uses the built-in Windows capability to allow MIME content types to be associated with applications. MIME content types are a superset of the familiar file extension mapping that you're probably very familiar with in Windows. I created a MIME content type, VSA Config file, and associated the content type with the extension .vsa. Using the File Types editor available from the folder options control panel applet, I associated the MIME content type with the .NET application that hosts the VSA IDE. As a result, whenever a .vsa or VSA Config file MIME content type is downloaded, the VSA IDE host application will be launched, will interpret the XML from within the file, and use the information contained in the XML to load the script code and show the VSA IDE. This approach certainly isn't rocket science, and in many respects it is extremely low tech. Nevertheless, it provides an extensible and elegant solution to hosting the VSA IDE from within a Web browser.
There is one obvious issue with this approach, however: How do you get the MIME mapping and the application onto your users' machines, in the event they download a .vsa file before they've set up your application? Luckily, Windows provides a great solution to this, but it does require your users to be on an intranet with a domain server in order to work.
If your machines are connected to domain, you can advertise MIME and file extension handlers so that when a user downloads a file for a file type that has an advertised handler, and they don't have the software installed, Windows will automatically install the handler for them. This means that you can guarantee that your users will always get the VSA IDE to show up when they download a .vsa file type. For more information about setting up this solution, check out Step-by-Step Guide to Software Installation and Maintenance.
Hosting the VSA IDE
Now that we've seen how the MIME handlers can provide a mechanism to assist with hosting the VSA IDE from a Web browser, lets take a look at how the .NET Windows Forms program actually uses the VSA SDK Design-Time class to talk to the VSA IDE and the code provider to retrieve and save the script source and compiled code.
The key to the host application in this example is the XML contained in the .vsa file. The XML schema we chose to use for the example provides all the information required to load the script code and the object model for the script. Obviously, we could have hard coded it into the application, since the object model of the Web Service is reasonably well known. We thought it was important, however, to try to develop a generic system that you could use as a basis for your future Visual Studio for Applications projects. For instance, if we were really developing this for fabrikam.com, having a solution that we could reuse for future projects would be a good thing.
Example XML schema
<application name="calculator" targeturl="http://localhost/Fabrikam/default.aspx" moniker="com.fabrikam://calculate" language="Microsoft.VisualBasic.Vsa" codeProviderURL="http://localhost/fabrikam/codeprovider.asmx" > <reference name="Fabrikam" assembly="C:\\Inetpub\\wwwroot\\fabrikam\\bin\\fabrikam.dll" /> <class name="Calculate" > <event name="calculateObjectModel" type = "fabrikam.calculateObjectModel" /> </class> </application>
The main part of the schema is the application element. This contains all of the application-scoped information, including the moniker and the name of the customization, which should be familiar from the runtime integration. The design time introduces a number of concepts in addition to the runtime—in particular, targetURL and the URL for the code-provider Web Service. The targetURL is used when the user runs their code from within the VSA IDE. The VSA IDE can't just run the code, since your application is actually going to host the script code the user is writing, and not the IDE.
To deal with this, the IDE will call back to the hosting application to do whatever is required to load the script code. Since this example shows how to use script in a Web Service, the targetURL is set to the URL of a Web page that calls the calculate Web Service. This means that running the code in the VSA IDE will cause the calculate Web Service to be launched, and any breakpoints the user has set in his/her script code will cause the IDE to debug their script when the Web Service is run.
The VSA Design-Time class uses a code provider to deal with persistence of the script code, but instead of using a local instance, it will call the code provider as a Web Service. This is a key design point for the VSA SDK. We wanted to ensure that persistence of code be as open as possible, and specifically that it be able to deal with remote code stores that could be behind firewalls, since this is a key scenario for us. To call a code provider through a Web Service, all you need to do is use the SetCodeProviderRemote method and provide the URL for the Web Service that implements ICodeProvider. In the example, I have the code provider implemented in codeprovider.asmx. Of interest here, the Calculate Web Service uses the code provider locally. That is, it does not create the instance through SOAP, but uses the same implementation as the Web Service.
Setting up the Design Time Engine
The information from the application element is used when creating the Design Time class. The Design-Time class is designed somewhat differently from the runtime class. It needs to be able to allow the host application to manage many engines, since the VSA IDE will be used to edit code throughout the system, and there could be more than one engine used in the system to provide customization. For example, if there were two Web Services in this solution, then there would be a script engine in each Web Service. The VSA IDE would have to show both script projects. As a result, the Design-Time class provides an easy way to create multiple design time engines. Rather than providing an abstraction over the interfaces on the engine, it just passes back an instance of engine, which you then program against by means of IVsa and IVsaDT. To illustrate this, I'll walk through the code required to get a design time engine up and running.
The XML schema is parsed in the Load event of the Windows Form for the hosting application. To parse the XML, I've used the XML parser that ships as part of the .NET Framework, and simply iterated through the attributes of the application element, and stored the values for later use with the Design-Time class. To provide some user feedback of what is going on, the program uses the Windows Form Progress Bar control and sets the progress value as attributes are read. This is pretty simple, but quite effective.
' Open the XML file passed in on the command line Dim xml As XmlDocument xml = New XmlDocument() xml.Load(args(1)) Dim node As XmlNode node = xml.SelectSingleNode("application") Me.ProgressBar1.Value = 5 ' Ignore whitespace ' myXML.WhitespaceHandling = WhitespaceHandling.None Me.ProgressBar1.Value = 10 ' Get the name custName = node.Attributes.ItemOf("name").Value Me.ProgressBar1.Value = 15 ' Get the targeturl targetUrl = node.Attributes.ItemOf("targeturl").Value Me.ProgressBar1.Value = 20 ' Get the moniker moniker = node.Attributes.ItemOf("moniker").Value Me.ProgressBar1.Value = 25 ' Get the engine language language = node.Attributes.ItemOf("language").Value Me.ProgressBar1.Value = 30 ' Get the Code Provider URL codeProviderURL = node.Attributes.ItemOf("codeProviderURL").Value Me.ProgressBar1.Value = 35
Once all the information for the application element has been parsed, the program has enough information to start using the Design-Time class. In the fabrikam.com example, I created a class that extended the VSA Design-Time class to make the integration simpler. I certainly recommend that you do something similar when you use the Design-Time class. The class has a constructor that takes all the information contained in the application element, so it's just a matter of creating an instance of the class and passing all the information into the constructor.
' Create an instance of the webhost class myhost = New dthost(Me, custName, moniker, targetUrl, language, codeProviderURL) Me.ProgressBar1.Value = 60
The constructor for the dthost class uses the codeprovider URL to setup the code provider for the Design-Time class to load the script code. As a precaution, the class checks to see if the URL is null and if it is use the local code provider. Once the code provider has been set up, the Design-Time class has all the information needed to load or create a VSA project, so it's safe to create a new design time engine. The Design-Time class was designed to make it easy to deal with multiple engines by providing a collection of engines, VsaEngines. This collection has a method, create, that will return a new engine and add it to the collection. In this example, to keep things simple, there's only one engine, but having the collection should make your life just a little bit easier.
'------------------------------------------------------------------ ' Set the code provider for this engine '------------------------------------------------------------------ If "" = strCodeProviderURL Then SetCodeProviderLocal(New DiskCodeProvider()) Else SetCodeProviderRemote(strCodeProviderURL) End If
Once the code provider has been set, the hosting class will check to see if there's already an existing project for the moniker provided. This is pretty simple to achieve by calling the LoadEngineSource method on the base VSA SDK DT class, and catching any exceptions. If the project already exists, then we're pretty much done, since the project contains all the information required for object model and references. However if a project doesn't exist, then we need to remove the old engine from the collection, and go ahead and create a new engine from scratch. The new engine will be used from now on to add code, object model, and references too. At this stage, there's nothing else that we want to do to the engine to initialize, so the code calls the InitNew method on the engine. This tells the engine that initialization is done, and that it can go ahead and finish the creation of the new engine.
Try '------------------------------------------------------------------ ' Attempt to Load our engine '------------------------------------------------------------------ LoadEngineSource(strMoniker, Nothing) '------------------------------------------------------------------ ' If successful set newEngine to False '------------------------------------------------------------------ newEngine = False Catch e As Exception '------------------------------------------------------------------ ' Load Failed this is a brand new engine '------------------------------------------------------------------ VsaEngines.Remove(strMoniker) dtEngine = VsaEngines.Create(strMoniker, strLanguage, Nothing) engine = dtEngine engine.InitNew() '------------------------------------------------------------------ ' Set the engine name '------------------------------------------------------------------ engine.Name = strCustName End Try
In the host application, there's a simple check on the NewEngine property on the instance of the dtClass. If there is a new engine, then we need to add references and object model to the engine; otherwise, nothing needs to be done, because the project has already been loaded.
Adding References and Object Model to the Engine
At this point we have a DT engine ready for adding the source code, object model, and references required for the project. The XML schema used by the host application contains information about all the references that will be added to the VSA project, so that's the first thing that needs to be added to the engine. Adding a reference to a Design-Time engine is exactly the same as with Script for the .NET Framework engine. Both engines implement IVsa. It's just a matter of calling CreateItem with an item type of VsaItemType.Reference and then setting the AssemblyName on the item to be the path of the assembly. The XML contains both the name and path of the reference to be added, so this is pretty straightforward. To make this easier the dtHost class exposes this functionality by means of an AddReference method, which takes the name and path.
'Get the References nodeList = node.SelectNodes("reference") For Each subNode In nodeList refName = subNode.Attributes.ItemOf("name").Value refAssembly = subNode.Attributes.ItemOf("assembly").Value myhost.AddReference(refName, refAssembly) Next
Once all the references are added to the engine, all that remains to be done is to add the object model that the script writer will use when coding. Adding objects to the engine is achieved by calling the AddEventSource method on a code item in the engine. In the XML used in the example, all the objects are event-source objects, which are added to class items. To assist with the implementation, the dtHost class provides an abstraction of the code item creation and adds event-source objects.
Dim nodeList As XmlNodeList nodeList = node.SelectNodes("class") Dim classNode As XmlNode Dim stepit As Integer Dim eventNodeList As XmlNodeList stepit = 20 / nodeList.Count For Each classNode In nodeList className = classNode.Attributes.ItemOf("name").Value myhost.AddClass(className) eventNodeList = classNode.SelectNodes("event") Dim eventNode As XmlNode For Each eventNode In eventNodeList eventName = eventNode.Attributes.ItemOf("name").Value eventType = eventNode.Attributes.ItemOf("type").Value myhost.AddEvent(className, eventName, eventType) Next Me.ProgressBar1.Value = Me.ProgressBar1.Value + stepit Next
Object Model Implementation and Performance on the Server
Deciding how an object model is added to the engine is very important when running script on the server, where speed and scalability of the script code are of great importance. A VSA engine can add objects in two ways: as a global object (which can't fire events) and as event source objects. Global objects are, not surprisingly, scoped globally in the script code, and as a result are statically defined. Static objects are fine when running in a single-threaded environment, but there are important considerations to be aware of in a multi-threaded environment.
In a multi-threaded environment, such as code running on an ASP.NET server, there could potentially be many instances of one script running at any one time, and any static variables declared in the script will be shared amongst all the scripts. So, if a global object were in the script, then all instances of the script running would share the same instance of the object.
To illustrate the potential problems with this, imagine if there were a global object defined in the script with a simple string property called "title." The first instance of the script sets the title property to be "Hello World," and then returns the value of the property. This works just fine when there is only one instance of the script running. Now imagine the same scenario again, but this time in between setting the property and returning the value, a second engine loads up with the same object model but a slightly different script. The first line of the second script sets the title property to be "hi from script 2". Since the instance of the object model is shared amongst the script when the first script returns the value of the property, it's going to get "hi from script 2." Things get even worse if the scripts both try to access the property at the same time—not a pretty sight.
Luckily, there's a way, context isolation, to deal with static variables in a multi-threaded environment. Context isolation ensures that the static variables are not shared amongst all instances, and Visual Studio for Applications uses this mechanism to ensure that you don't run into the contention issues I described above. Context isolation manages this by creating a copy of the static variable for each thread. The script code running on each thread sees the static variable as it would before, but it sees the copy that context isolation provides for the thread.
Unfortunately, context isolation doesn't come without a cost. Creating a copy of each static variable, and providing an infrastructure to deal with all the copies, incurs a considerable performance penalty. In our performance test suites, we've seen up to 50% performance degradation compared to using instance variables.
Avoiding the use of static variables, and so avoiding their performance impact, would appear to be the way to go on the server. Luckily, this is something that we put some thought into for Visual Studio for Applications, since creating customization code that runs efficiently on the server was our number one priority for this release. There are two ways that you can add object model to VSA that will result in statically declared variables: global objects, and event source objects contained in modules. However, event source objects added to class items result in instance variables, which have none of the performance problems.
If you're wondering why event-source objects in modules have to be static; the reason is that all variables declared in modules are static. So—and hopefully this won't come as much of a surprise—I highly recommend that you use event-source objects in classes for the entire object model that you provide for scripts to be run on the server. If you do use static objects, then VSA ensures that the declaration of the variables contains the ContextStatic attribute, which tells .NET to create the Context isolation so that no script code will fall into the contention problems.(It will just be much slower than it needs to be.) Hence all the event-source objects in the sample are added to classes to set a good example.
Me.ProgressBar1.Value = 90 myhost.InitCompleted() Me.ProgressBar1.Value = 100 Button1.Visible = True
All that remains to do now before using the engine is to tell it that initialization is complete by calling the InitCompleted method.
' Everything is set for the engine so call InitCompleted dtEngine.InitCompleted()
Showing the IDE
The VSA Design Time engine now has all the information required to allow the user to start editing the script, so the host application needs to tell the engine to make IDE visible. Luckily, this is super simple to accomplish by means of the ShowIDE method on the engine. To make this a little more robust, the dtHost class has a Show method that just calls the ShowIDE method, but wraps it in a try catch block to capture any exceptions and throw the appropriate exception to the application using the dtHost class. After showing the IDE, the host application minimizes itself so the user can see the VSA IDE.
'Show the VSA IDE myhost.Show() 'Minimize myself so the user can see the VSA IDE Me.WindowState = FormWindowState.Minimized
Writing the Script Code
The great thing about being able have a full-featured development environment for writing the scripts for an application is that you no longer have to build your own (which typically ends up being some variation on a text box). Also, writing the script is so much easier, since you don't have to worry about what objects are available to you, because the IDE provides all the information for you.
When the VSA IDE is first shown to the user after the show call, the user will be presented with all the project information that was added from the XML and a code window showing the class that was added to the project.
Figure 2. VSA IDE showing the project and class
In order to write script code to customize the calculation object, the script writer must select the event he or she wishes to script. To do so is pretty simple in the VSA IDE; just select the calculateObjectModel from the object list at the top of the code editor, and then select the event from the event list.
Figure 3. Object list box
Figure 4. Event information
Selecting the event will cause an event handler to be added automatically into the class, ready for the script author to write the script.
Figure 5. Event handler added
Accessing the object model that the application provides is pretty simple as well, since the VSA IDE provides full Microsoft® IntelliSense® support on the objects provided to the VSA engine. The user just needs to type the name of the object and a period, and the VSA IDE provides a list of all the members of the object. This won't be new to you if you've been using Visual Basic for Applications (VBA), but to a script user, this is a major step forward.
Figure 6. Member information for the calculateObjectModel
Saving the Code
Once you are happy with the code you have written, you need to be able to save it. One of the key advantages of Visual Studio for Applications and Script for the .NET Framework is that the application that is hosting the script gets to decide where the code is stored. All the developer has to worry about is hitting the Save button. When you hit Save, the VSA IDE will call back to the hosting application and provide all the code you have written, so that the host application can save it. Where your application chooses to put the code in your application is entirely up to you. Many people choose to store it in a database. To keep things simple, the fabrikam.com Calculate Web Service stores all its code on disk in a folder in the fabrikam vroot.
The VSA SDK takes care of most of the infrastructure required to save the code by using the code provider to actually do the persistence. All your code needs to do is decide whether to save the source and compiled form. Typically, saving the compiled code is something that you would want to do, especially when writing server customizations. In the example code, the dtHost class provides a SaveVsaEngine method that first saves the source, compiles the source (just in case it isn't already), and then saves the compiled state.
Sub SaveVsaEngine() Try '------------------------------------------------------------------ ' Save source state '------------------------------------------------------------------ SaveEngineSource(engine.RootMoniker, Nothing) '------------------------------------------------------------------ ' Compile and save the customization code '------------------------------------------------------------------ If engine.Compile() Then SaveEngineCompiledState(engine.RootMoniker, Nothing) End If Catch e As Exception MsgBox("Error Saving Engine") ' & e.StackTrace) End Try End Sub
When the SaveEngineSource and SaveEngineCompiledState methods are called on the VSA SDK DT Class, the class calls the code provider PutSourceCode and PutBinaryCode methods, by means of SOAP, to store the code.
Running the Code
The script code has been written and saved to your code store by your code provider. All that remains to be done is to run the code and see if it works.
Since the script code is running on the server, ensuring that it is run in the correct manner is very important. Your application will be running the script, so the VSA IDE relies on the host application to ensure that the script is run correctly. The XML schema definition included a targetURL attribute in the application element to help solve this problem.
The dtHost class in the example uses the value of the targetURL to tell the VSA Design-Time class which URL to launch when the user tries to run or debug the script. The Design-Time class implements the IVSADTSite, which is called back by the VSA IDE when the user runs the code and the URL is launched. When the URL is launched, the user enters the information required to call the Web Service and submits it. When it is submitted to the Web server, then the Web Service will be instantiated, which in turn loads the compiled script code and runs the script code.
Debugging the script pretty much comes for free, since you don't have to do anything more than is required to run the code. The only difference from just running the script is that the script writer selects the lines they wish to break into and runs the code. The run again causes the URL to load, and the script gets loaded when the Web Service is called; the VSA IDE is ready to break into the code at the breakpoints when the script code is running.
Figure 7. Debugging the script code
Storing and Retrieving the Script Code
Throughout this article, I've talked about how the VSA Runtime and Design-Time classes use code providers to store and retrieve script code, but I haven't gone into any detail on how to implement a code provider. To do the code provider mechanism justice would require a full scripting clinic (something I plan to do in a future column). I will, however, go over the basics here, so you can see how the code provider in the example was used.
A code provider is a .NET component that implements the ICodeProvider interface and translates monikers into the storage format that the application uses. For example, a code provider could translate a moniker into a set of queries in a SQL Server or XML document. The interface was designed to allow for the loading, saving, and deleting of both source and binary code. The design was kept simple and stateless, so that components could be called locally or as a Web Service.
The code provider in the example for this article stores all the script code on disk, to keep things simple for setup purposes. All script source and binary is stored in the vsa projects folder in the fabrikam vroot on the Web server. The code provider uses the application information in the moniker and creates a folder in the vsa projects folder so
com.fabrikam://calculate results in vsa projects\calculate. When the code provider is called from the Design-Time Hosting Class, the VSA Project, code items, debug information, and the compiled form will be stored in the folder.
Figure 8. Code store for fabrikam
When the application runs, the runtime Hosting class will use the code provider to load the binary compiled script from the calculate folder.
Visual Studio for Applications and Script for the .NET Framework provide a powerful way for you to build applications that you or your customers can customize to meet changing requirements. I hope this article gives a good introduction into how to build a customizable .NET application. In particular, I hope you've seen how you can apply VSA to creating customizable Web Services and providing access to the VSA IDE from within a Web browser. I'd like to take this opportunity to thank Wayne King, a tester on the VSA SDK team, who took some of my vague whiteboard design ideas and implemented them so well for the example. As ever, we would really like to hear your feedback, so feel free to contact us on the VSA newsgroups, or at firstname.lastname@example.org.