A simple VSTA 2.0 runtime integration
VSTA 2.0 is quick and easy way to build and deploy add-ins for host applications. You could refer to summit website to see how you could get your own copy of VSTA 2.0 SDK. http://www.summsoft.com
Going forward I assume you have the latest bits of VSTA 2.0 SDK installed on your machine and are ready to dive into creation of add-ins for your custom host. Usually end users and VSTA 2.0 integrators or amateur developers who want to integrate VSTA or just want to play around with the same end up getting overwhelmed with the amount of information that is available. One reason for this is that there are multiple ways to achieve the same goal and the samples available are tailored to demonstrate both the integration with light weight IDE and runtime semantics of add-ins. For the purpose of this write up we would focus on just the runtime aspects where we demonstrate how easy / quick it is to have an add-in loaded by the host and start using the host object model in the add-in (and vice versa). To keep things clear and simple and focus on VSTA aspects I would use a simple host object model that is just simple enough to demonstrate VSTA 2.0 runtime Integration. Let’s consider a host that exposes a set of services to its add-ins, add-ins could get loaded query for a given service, use the service and get done. Below is a simple host object model that we need to enable to load add-ins.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
namespace App
{
//this is simple class that will interact with addins.
//different addins will use this provider to query for services!
public class ServiceProvider
{
public object GetService(string ServiceName)
{
if (ServiceName == "TemperatureService")
{
return new TemperatureService();
}
if (ServiceName == "HumidityService")
{
return new HumidityService();
}
return null;
}
}
//some services that host exposes!!
public class TemperatureService
{
public double Temperature
{
get;
set;
}
}
public class HumidityService
{
public double Humidity
{
get;
set;
}
}
//notice that this is not exposed to add-ins
internal class InfrastructureService
{
internal double ServiceId
{
get;
set;
}
}
}
VSTA 2.0 only exposes public types and methods and explicitly implemented interface methods to the add-ins hence in the case above we would not have Infrastructure service exposed to the add-in.
The next step after the host object model is defined / or an existing one is chosen to be exposed to the add-ins, is to generate a proxy for the add-ins to work for. Add-Ins don’t couple to the host tightly (I will try to keep the language simple here we will discuss each term mentioned in detail in future posts) hence to enable add-ins to work with host object model we expose only the host metadata to the add-ins. For all practical purposes this metadata doesn’t have any executable code and any executable code that penetrates into these proxies is for supporting compiler constructs as we go from a tightly coupled model (where an add-in or any piece of such code directly takes dependency on the host object model) to the loosely coupled one. For all practical purposes the reader can assume proxy assembly is just a metadata assembly that VSTA runtime recognizes. VSTA Runtime could be treated as of now like a black box that forwards calls on the proxy assembly to the underlying host.
So let’s generate a proxy for the host object model above. Compile the host object model to a .NET assembly. If the application is a standalone application you would have an exe to work with else it would be a host object model library. In this case I will compile the above class as a library. (Don’t forget to define a main method if you want to use the sample above and have OM as a standalone application)
In my case I just compiled the assembly to C:\App\bin\Output\App.dll
To generate a proxy go to the Proxygen folder in SDK installation on your machine through a command prompt. On my machine it is installed at
C:\Program Files\Visual Studio Tools for Applications 2.0 SDK\2008.02\Visual Studio Tools For Applications\Tools\ProxyGen.
And type in the following command
ProxyGen.exe /l:"C:\App\bin\Output\App.dll" /o:"C:\App\bin\Output\App.xml"
This should generate a XML file also called the descriptor for the object model. Proxygen runs in two phases first to generate an xml descriptor from the object model and second phase to generate a code file out of an xml descriptor file. This is done mainly because Proxygen is just a facilitator for generating proxies hence it never claims to cover all the code generation cases that are possible. The Intermediate XML allows the user to modify final output of the proxy in any way it’s appropriate to support the host scenarios.
Below is the descriptor XML generated in this case
<?xml version="1.0" encoding="utf-8"?>
<Library originalName="App" versionMajor="0" versionMinor="0" xmlns="http://schemas.microsoft.com/vsta/2008/01/ProxyGenDescriptor">
<ManagedLibrary>
<Class originalFullyQualifiedName="App.ServiceProvider" isExcluded="false" isAddInEntryPoint="false">
<Method originalName="GetService" isExcluded="false">
<Parameter originalName="ServiceName">
<Type>
<ExternalTypeReference isInterface="false" type="System.String" />
</Type>
</Parameter>
<ReturnType>
<ExternalTypeReference isInterface="false" type="System.Object" />
</ReturnType>
</Method>
</Class>
<Class originalFullyQualifiedName="App.TemperatureService" isExcluded="false" isAddInEntryPoint="false">
<Property originalName="Temperature" isExcluded="false">
<Type>
<ExternalTypeReference isInterface="false" type="System.Double" />
</Type>
<Get isExcluded="false" />
<Set isExcluded="false" />
</Property>
</Class>
<Class originalFullyQualifiedName="App.HumidityService" isExcluded="false" isAddInEntryPoint="false">
<Property originalName="Humidity" isExcluded="false">
<Type>
<ExternalTypeReference isInterface="false" type="System.Double" />
</Type>
<Get isExcluded="false" />
<Set isExcluded="false" />
</Property>
</Class>
</ManagedLibrary>
</Library>
We see that the public types and methods have been exposed and are outputted to the descriptor. Refer to MSDN documentation if you intend to play more with this descriptor file. Before we generate the code file we need to make sure we choose the entry point in host object model for the add-ins. An entry point could be visualized as a root of the tree if the tree symbolizes the various entities in the host object model. E.g. In our case ServiceProvider is a candidate for an entry point as add-ins can use the host object model only if they have a handle to the ServiceProvider object in the host. Complex object models can expose multiple entry points to work with.
Modify the descriptor XML to specify an entry point as below
<Class originalFullyQualifiedName="App.ServiceProvider" isExcluded="false" isAddInEntryPoint="true">
Save the Xml and to generate a code file out of the descriptor run the following command line.
ProxyGen.exe /i:"C:\App\bin\Output\App.xml" /c:"C:\App\bin\Output\AppProxy.cs”
With this command line you would have two code files generated in your output folder
1. AppProxy.cs
2. AppProxy.HostTypeMapProvider.cs
Now you are all set to create a proxy assembly using these files. Create a class library project in Visual Studio and add AppProxy.cs to the project. You will have to add references to two assemblies to build you proxy assembly as the types in these assemblies are referred by the generated proxy code.
1. Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll
2. System.AddIn.dll
Both assemblies should exist on your machine due to VSTA runtime and .NET Framework install respectively.
Build the proxy assembly and make sure it builds fine.
Now we are ready to create an add-in for our host application using the proxy just generated above.
Create a new class library project and define an empty class in the project which would represent your Add-In class. Derive the class from ServiceProviderEntryPoint (this is defined in proxy assembly created in the previous step and would require referencing the proxy assembly). Override the FinishInitialization method as below.
namespace App
{
class AddIn:ServiceProviderEntryPoint
{
protected override void FinishInitialization()
{
}
}
}
Build the add-in assembly and make sure it builds (you will have to add reference to Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll)
Now we have an add-in that builds against the proxy assembly generated out of our host object model. AddIn class derives from the ServiceProviderEntryPoint and hence represents the ServiceProvider in the host object model which was chosen as an entry point in the descriptor xml.
Let’s interact with our host OM in this add-in something like as below, feel free to use host OM in anyways you would like to. I modified the FinishInitialization method as below
class AddIn:ServiceProviderEntryPoint
{
protected override void FinishInitialization()
{
TemperatureService tempServ = (TemperatureService)this.GetService("TemperatureService");
Console.WriteLine(tempServ.Temperature.ToString());
HumidityService humidServ = (HumidityService)this.GetService("HumidityService");
Console.WriteLine(humidServ.Humidity.ToString());
}
}
Now we have Host in place, Add-In in place that uses the host but how can we enable the host to load this Add-In.
Create an Exe project in visual studio. This step is needed if the host is not a standalone app as in this case if your host was an exe to start with you might want to use the same exe to load the host types and add-Ins.
Modify the Program.cs as below
class Program
{
static IServiceProvider serviceProvider = null;
private const string vstaPipelinePath = @"Microsoft Shared\VSTA\Pipeline";
static void Main(string[] args)
{
InstantiateServiceProvider();
//this invokes the deriver and binds the driver to the listeners
string addinPath = @"C:\App\AddIn\bin\Output\AddIn.dll";
string commonPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonProgramFiles);
string pipelinePath = System.IO.Path.Combine(commonPath, vstaPipelinePath);
Type havType = typeofMicrosoft.VisualStudio.Tools.Applications.Runtime.IExtendedEntryPoint) Collection<AddInToken> addins = AddInStore.FindAddIn(havType, pipelinePath, addinPath, "App.AddIn");
if (addins.Count > 0)
{
IEntryPoint addin = addins[0].Activate<IEntryPoint>(AddInSecurityLevel.FullTrust);
addin.Initialize(serviceProvider);
addin.InitializeDataBindings();
addin.FinishInitialization();
}
else
{
Console.WriteLine("Add In could not be discovered");
}
}
static private void InstantiateServiceProvider()
{
if (serviceProvider == null)
{
ServiceProvider provider = new ServiceProvider();
IHostItemProvider itemProvider = new HostItemProvider(provider);
ITypeMapProvider typeMapProvider = new HostTypeMapProvider();
ServiceContainer container = new ServiceContainer();
container.AddService(typeof(IHostItemProvider), itemProvider);
container.AddService(typeof(ITypeMapProvider), typeMapProvider);
serviceProvider = container;
}
}
}
Add another type definition in the file that would expose the service object.
public class HostItemProvider : IHostItemProvider
{
#region Constructors, Destructors
ServiceProvider _rootObject;
public HostItemProvider(ServiceProvider service)
{
_rootObject = service;
}
#endregion //Constructors
#region IHostItemProvider Members
public object GetHostObject(System.Type primaryType, string primaryCookie)
{
return _rootObject;
}
#endregion //IHostItemProvider Members
}
Remember AppProxy.HostTypeMapProvider.cs that was generated when we generated the proxy class add it to this project.
You will need to refer the System.AddIn.dll and Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll for the project to compile.
You can set the breakpoint in your add-in and start the exe. If everything went fine the breakpoint would be hit and you should be able to communicate with the host.
In future posts i would focus on specific scenarios and details about design and general implementation.