Introducing Visual Studio for Applications
January 16, 2001
Those of you who follow Windows Script have probably been wondering what role script plays in the .NET world, and perhaps even what the script team has been doing for the last year or so. After all, the script team has released major upgrades to the script engines nearly every six to 12 months, but it's been a long time since Version 5.0 shipped, which was the last major upgrade to Windows Script technologies. Today is a very exciting day for the Windows Script team, and more importantly the bigger team that we are now part of, with the announcement of Visual Studio for Applications (VSA). VSA represents two years of work from the existing Windows Script team and the Visual Basic for Applications (VBA) group to create a product that takes the best of Windows Script and VBA to create an application-customization technology designed specifically to take advantage of .NET.
The Urge to Converge
Prior to VSA, Microsoft had two technologies for integrating a language into applications that would allow the application to be customized after the application had shipped. This was achieved by hosting a language engine, be it a Windows Script engine (such as Visual Basic Scripting Edition or JScript) or VBA. Once the engine had been loaded, the application would load the appropriate customization code (a.k.a. script) and provide an object model for the script writer to use. While the concept was identical, the implementation for a Windows Script engine or VBA was very different. So if you integrated Windows Script, there was no easy way to upgrade to VBA or vice versa. You had to integrate VBA using completely different integration interfaces, so you'd be starting from scratch. In addition, you'd probably have to integrate both so that existing script code would still work alongside the new VBA code.
The different integration technologies were only half the problem for people who integrated VBScript into their applications. VBScript started as a strict subset of the Visual Basic language, but because it was a different code base, differences in implementation crept in. And because VBScript was evolving at Internet pace, the languages diverged. As VBScript started to mature, the requests we were getting for the language were bringing it closer and closer to Visual Basic, and in some cases we added features in VBScript before they were added to Visual Basic (the Class End Class syntax for example).
As we looked into the new features people were asking for—types and better error handling for example—it became clear that VBScript would eventually evolve into real Visual Basic. Rather than try to implement two versions of the same language, we sat down with the Visual Basic team and worked out how to converge the languages. This means that the new Visual Basic Script engine is true Visual Basic. It has exactly the same compiler as Visual Basic, so any code you can use in Visual Basic you will be able to use in the script engine. In addition to language features that use the Visual Basic compiler, script code is now fully compiled, as is any Visual Basic .NET code.
The Visual Basic .NET language includes some of the concepts that were initially introduced in VBScript, but it represents a considerable evolution of the language. As a result there are considerably more powerful language features, but it isn't 100 percent backward compatible with VBScript. This means that existing code will not always run in the new Visual Basic Script engine. We felt that the advantages provided by the new Visual Basic language outweighed any compatibility issues. However, compatibility is a major concern for us, and you will still be able to host the existing VBScript engine as well as the Visual Basic Script engine to allow your existing script code to continue running just as it does today.
Currently the technology is available through an early adopter program for developers who are interested in using VSA. A more wide-scale beta is planned to ship in the spring with Visual Studio .NET and the .NET Framework. If you are interested in becoming an early adopter or have questions about the early adopter program, please contact Summit Software, Microsoft's authorized agent for VSA licensing and support, at VSASupport@SummSoft.com.
Customization in a .NET World
The primary reason for developing a unified script hosting technology was to allow the development of customizable .NET applications. Customization is a key asset to add to your applications, because it allows your customers to change the behavior of the application to meet their requirements. This is probably not news to you, since there are so many of you integrating VBA and Windows Script into your applications today, and even more people are actually writing scripts within those applications. The key difference to application development today is the distributed nature of the application. Code can run on the client or the server, and a business process may include code running on both. To provide a customization technology for distributed applications, it's important that the customization code can scale from client to server execution.
Scaling to server execution is a major focus for the development of the architecture of VSA. To achieve this, we took some of the scalability features from today's script engines (flexible threading model, lightweight runtime) and some of VBA's strengths (compilation, full strength languages), and built the new system around that. This means that there is a definite split between run-time and design-time capabilities. The runtime for VSA is script for the .NET Framework, so all the code your users write will be run using a script engine. This is vital for the server because any customization code must impose as little overhead to the server code as possible to ensure that the server can scale to meet the usage needs of the application. The script engines will continue to be freely able to be licensed, just as VBScript and JScript are today. As a result, there is a set of integration interfaces for the script engines (IVSA) and the VSA Design Time (IVSADT). Your application only needs to use the design-time interfaces if you wish to allow the user to use the VSA integrated development environment (IDE).
How Do I Use Any of This Stuff?
To explain how to use the new script engines and the VSA IDE, I'll create a simple .NET application for use on the infamous (and fictitious—no association with any real company, organization, product, domain name, e-mail address, logo, person, places, or events is intended or should be inferred) fabrikam.com. Fabrikam.com has experienced explosive growth in usage, and as a result has built a user management system to deal with all the new users. It turns out that this user management system is so great that, in an effort to generate some revenue, fabrikam.com has decided to sell it as a product. Since user management systems have specific requirements that are not part of the fabrikam user management system, rather than provide a different system to each customer, the developers at fabrikam integrated VSA into their application so that the application can be customized to meet the customers' requirements. To make it easier for the customer to write the customization code, the user management system provides the VSA IDE, which provides a great environment for writing and debugging the customization code.
The user management system is written as a set of .NET components that run behind an ASP.NET system. All the logic of the application is encapsulated in the components, with the ASP.NET pages providing the HTML user interface for the application. All access to the user data stored in a SQL Server database is through the business components.
Figure 1. User management system architecture
To enable the application to be customized, the business components host a script engine that will be used to run any customization code for the component. When the component is loaded the system will load the customization code that is relevant for the current user. One of the key features that VSA brings from Windows Script is that the code can be stored wherever needed, be it in the file system or in a database. The run-time interfaces require just a string or a bytestream (for precompiled code). By not imposing a storage model, the application developer can make the code distribution process transparent to the user. The user saves his or her code from the IDE, and the application hosting the design time gets the source code, which it then saves to wherever the application stores the customization code. The VSA SDK comes with a mechanism to make this storage process a little easier. I'll go into that in more detail later on in this column.
Once the customization code has been written and saved to the applications code store, the next time the application runs it will load up the customization code. In the User management system's case, it will load the code when the user business component is instantiated on the server. The user business component exposes an object model that the script code runs against. In this case, the user business component fires a new event whenever a user is created. The customization code handles the event; for example, whenever a user is added to the system, an e-mail is sent to the user to validate the e-mail address and to send a greeting. You could argue that the system should do this by default, but it is a demo after all. The user business component and all the other business components in the system provide a rich object model that provides hooks for customization.
Figure 2. User management integrating VSA
Enough Conceptual; Let's See Some Code
I hope you have a good conceptual overview of the application and how it enables customization. To make it easier to integrate the script engines and the design time, there are a number of classes and samples that are available with the VSA SDK. I'll use these classes for the examples. I've used C# to write this application, but it could easily be written in Visual Basic.NET, JScript.NET, or any .NET programming language.
The integration classes are designed to be reasonably self-contained, so it was important that they integrate with the application's storage mechanisms without imposing any requirements on the storage format. It was important that the storage mechanism allow both local and remote access—in particular over HTTP to ease firewall issues—to the code store. This is achieved by defining a code provider interface, ICodeProvider. ICodeProvider is a reasonably simple interface that allows source and compiled code to be stored and retrieved. To plug in to the integration classes, you need only a component that implements the ICodeProvider interface. To use the code provider with the integration classes, you either pass in an instance of the code provider running locally or a URL. If a URL is provided, then the integration classes will call the code provider through SOAP over HTTP.
Integrating the Runtime
In order for your application to be able run any customization code, it needs to host a script engine. To illustrate this, I've included the code for the user business component used in the user management system. Because it's a demo, it's a pretty simple class. So simple, in fact, that it doesn't actually store any information in a database. But I figured you'd all seen enough ADO code recently. The runtime class was designed to be easy to use and provide an abstraction of the run-time integration interfaces. Essentially all you need to do is tell it where to go get the customization code, provide an object model, and call the run method. Those of you familiar with the script control should see some similarity in design simplicity, I hope.
The first thing the class does is create an instance of the runtime class, m_Runtime, which will be used to access all the functionality of the script engine. You'll notice that the constructor of a runtime class takes two arguments, Name and Moniker. Name identifies the script engine; this is useful when you have more than one script engine in your application. Moniker provides a unique identifier for the customization; the format of the moniker is based on a standard URI, protocol://data, but the content is entirely up to you. For this example, I've used a simple moniker, com.fabrikam://usermanagement/user_customization. The protocol portion of the moniker should be unique, so I've reversed the fabrikam.com domain name because that's guaranteed to be unique. The data portion of the moniker provides the application name, usermanagement, and then a folder, user_customization. In reality the moniker you use in your application is likely to be more useful. For example, you might decide to store different customizations for different customers, so you could change the moniker to include the currently logged in user name. In addition to providing a unique identifier for the customization, the code provider for the application uses the moniker to retrieve the customization code. In essence, the code provider translates a moniker into the relevant query language for the code store. For example, a code provider could translate a moniker into a SQL query.
m_Runtime = new Runtime("user","com.fabrikam://usermanagement/user_customization");
Once an instance of the runtime class has been created, load the customization code. To do this, the runtime class needs an instance of a code provider. Since this is a component running on the server and performance is key, the code provider is created locally rather than having to communicate over HTTP. The runtime class has a setcodeprovider method that takes an instance of a code provider as its argument. Once the code provider has been set, call the Load method, which will use the code provider to retrieve the compiled customization code. This is a key feature for the .NET script engines because they allow the loading and saving of compiled code rather than having to interpret the code every time. This should help considerably with performance on the server. It's not required that you load compiled code; you can still load source into the engine using the LoadSource method. When Load or LoadSource is called, the runtime class will call into the provided code provider with the moniker. The code provider uses the moniker to retrieve the code and returns it to the runtime class.
ICodeProvider codeProvider = new DiskCodeProvider(); m_Runtime.SetCodeProvider(codeProvider); m_Runtime.Load(null);
After the code has been loaded into the engine, all that remains before running the code is to add the object model that the code will use. In this example, I'm going to add the object model of the user business component as both an Object and an Event source object. This is achieved by calling the AddObject and AddEventSourceObject methods.
// Add our global and event source objects. m_Runtime.AddObject("HostObject", this); m_Runtime.AddEventSourceObject("User", "UserEvents",this);
Once the code and object model have been added, run the code using the Run method. Calling Run will cause the code that is loaded to bind to the object model and begin running.
That is all that's required to load a script engine: Load the code into the engine, provide an object model, and run it. Not bad for five lines of code. The runtime class provides more control over the engine, such as timeout values for the script, but if it doesn't meet your requirements exactly, it is provided in source form so you can change it to suit your needs.
What about Performance?
The runtime class makes it simple to host a script engine on the server, but loading code every time would impose a considerable burden on the application. The engines are optimized to ensure that compiled code is cached so that code is only loaded when it has changed. Whenever the engine is asked to load code, it will check with the cache to see whether code for the supplied moniker has already been loaded; if so, it uses that version. The IVSA interface and the runtime class provide a method to invalidate the cache entry so that when new code is compiled, you can control the cache and when it gets updated. The key design point was to be able to update the cache without having to take down the server to allow hot updates.
Hosting the Design Time
The application is now set up to run any customization code written by its users, but there's no easy way for users to write the customization code for the system yet. One of the key disadvantages of script was that you had to write your own script editor in order for people to be able to write scripts. Those of you who are familiar with VBA will know what an advantage it is to have an IDE, giving the user Intellisense®, enhanced debugging, etc. Visual Studio for Applications provides an IDE based on the Visual Studio .NET IDE, so you get all the great usability enhancements provided in Visual Studio .NET in your applications development environment, rather than having to build your own from scratch.
VSA provides two ways to integrate the IDE into your application: the VSA IDE or VSA Standalone IDE. The VSA IDE provides a full-fledged development environment in which to edit or debug your VSA projects. The key concept behind the VSA IDE is that you launch it from your Windows application much in the same way as you launch the VBA IDE from an Office application.
When the IDE is launched, your application uses the IVSA Design Time interfaces to load the source code for customization and to provide the application object model. The VSA Standalone IDE provides an integration path that allows more flexibility in what you can do within the IDE because you can build your own packages for integration into the IDE.
A package is a component written using the Visual Studio Environment SDK, and it allows for considerable integration into the Visual Studio shell. For example, you could write your own project system that provided additional features on top of those the VSA project system provides. A bonus of using the VSA Standalone IDE is that you can also use your packages in Visual Studio .NET, allowing your application to be a part of a Visual Studio solution. This is especially useful if many of the people who write the customization code already have Visual Studio, because they can now develop solutions that include your application alongside any Visual Studio projects.
To illustrate hosting a VSA Design Time I'm going to use the VSA IDE since there is less code to get that up and running. Showing you how to write a Visual Studio package would contain enough information for an article in its own right. The VSA SDK provides a set of classes to make it easier to host the design time, and I'll use those in my code sample. A key concept when hosting the design time is that there is a design-time engine that corresponds to a script engine. So if your application uses three script engines, you need to have three design-time engines. Mainly this is so that each engine has its own project system, which corresponds to a separate compilation unit and source store.
The fabrikam.com user management application has a customized application that provides all the tools required to customize the application. If you are familiar with my earlier scripting clinics, then you'll be aware of my lack of user interface design skills. This article continues this fine tradition, so my user interface is somewhat minimalist. In a real application it would contain all the application information/editing tools, along with a way to launch the VSA Design Time.
Figure 3. User management Windows application
When "designing" the user interface for this application, I chose to have two buttons, one to show the IDE and one to save the code. The show IDE button makes some sense, but the save code button is there to show that you get to save the code, not the VSA IDE.
The Windows client contains a form and a helper class that derives from the VSA SDK Design Time class. The helper class deals with all access to the VSA IDE. When the form loads, it creates an instance of the helper class. The initialization code of the class just sets the values of two member variables containing the name and moniker of the code to be loaded.
m_Name = "user"; m_Moniker = "com.fabrikam://usermanagement/user_customization";
To keep things simple, this demonstration only contains one instance of a script engine, so only one moniker is required. The real code starts when the user clicks on the Show IDE button. The onclick handler for the button calls through to the LoadVSAProject method in the helper class. The method first creates an instance of the design-time engine in the collection of engines provided by the design time class. The design time class keeps a collection of engines for you, since it's likely that your application, unlike this demo, will have more than one engine being hosted. The engines collection includes a create method:
IVsaDTEngine vsaDTEngine = VsaEngines.Create("Basic.DT", m_Moniker, m_Name,null); IVsaEngine vsaEngine = (IVsaEngine)vsaDTEngine;
Loading the Source
Once the engine has been created, the source code for the engine needs to be loaded. The VSA Design Time engine will call your application back through a site, VSAPersistSite. This site will be used for all persistence of the source and compiled code. The design-time class provides an implementation of the site for you, so you can just create a new instance of the implementation. The implementation provides a mechanism to use code providers just like the runtime class, so a code provider has to be passed into the setCodeProvider method. Since the source code for the user management system is stored on a server that might be behind a firewall, the code provider will be called through a Web Service.
VsaPersistSite persistSite = new VsaPersistSite(vsaEngine.RootMoniker); persistSite.SetCodeProvider("http://codeserver/codeprovider.asmx")
The URL provided to the SetCodeProvider method is the URL for the ASP.NET asmx page that exposes the code provider as a Web Service. (For more information about asmx pages, check out the .NET SDK documentation, Declaring a Web Service.) Once the persist site has been created, to load the source code call the LoadSourceState method on the engine and pass in the persist site. The design-time class will communicate with the code provider and the persist site to load all the source code for the engine.
Adding the Object Model and References
Once the source code has been added to the engine, the object model and references for the code need to be added before the user can edit or run any code. This is done by using the items collection on the design-time engine. An item in an engine can be code, an object or a reference.
IVsaItems items = engine.Items;
The items collection has a CreateItem method, so adding an object or reference is simply a matter of calling that method with an item type. To add a reference to the usermanagement object that will host the code at run time, the code creates a new item on the items collection and then sets the assemblyname for the item. Again to keep the demo simple, I've put the assembly in the root of the C: drive. I certainly don't recommend that you put your code there.
IVsaReferenceItem item = (IVsaReferenceItem)(items.CreateItem("Usermain", VSAITEMTYPE.REFERENCE)); item.AssemblyName = "c:\\usermanagement.dll";
Adding the application object model is a very similar process. Just call the CreateItem method and set the item type to APPGLOBAL. Once the item has been created, set the TypeString of the item to be that of the object being added.
IVsaGlobalItem item = (IVsaGlobalItem)(items.CreateItem("HostObject", VSAITEMTYPE.APPGLOBAL)); item.TypeString = "UserManagement.User";
Show the IDE
To complete loading the source code into the engine, set the startupurl property for the project and call the initcomplete method. The startupurl is key because the IDE will use that property when the user runs the customization code. Since the customization code is running in a script engine hosted in a business component running behind an ASP.NET page on a server, the IDE will launch the user's browser and navigate to the startupurl. The startupurl should be set to the ASP.NET page or a page that results in the ASP.NET page loading. Once the startupurl property has been set, all that remains is for the onclick handler of the show button to call the ShowIDE method on the helper class.
Figure 4. VSA IDE for the user management system
When the IDE shows up to the user, it has loaded the customization code for the user business component in the UserManagement application. In the screenshot above, the project is empty, awaiting the customization code.
The combination script for the .NET Framework lightweight runtime and the Visual Studio for Applications IDE enable you to start building applications that make it easier to meet your customers' requirements, and for your customers to start using your applications as a platform to build solutions. I've only been able to give you a brief overview of what is possible with script for the .NET Framework and Visual Studio for Applications, but as we progress through our beta programs, I'll be writing more on what's possible and I'll go into more depth about how you can utilize the technology in your applications.
As ever, we're very keen to get your feedback on the direction we're taking with script and VSA, so please feel free to comment on this article or contact us at firstname.lastname@example.org The programmability team at Microsoft looks forward to hearing from you.