Welcome to MSDN Blogs Sign in | Join | Help

Hook Custom adapters to VSTA pipeline

We have seen how we can use the VSTA provided pipeline to load add-ins and enable various scenarios.  There may be some cases when we want to extend the existing VSTA pipeline to enable new kind of contracts, to allow any of our custom types to cross the contract boundary (if we don’t care about any of the type isolation requirements!) or just for reasons like performance where a custom adapter could give better performance than the generic VSTA mechanism. Let’s see how the existing VSTA pipeline could be extended to hook up custom adapters. By default the Pipeline entry point views for VSTA pipeline are IEntryPoint and IExtendedEntryPoint

namespace Microsoft.VisualStudio.Tools.Applications.Runtime

{

    [AddInBase]

    public interface IEntryPoint

    {

        void FinishInitialization();

        //

        //

        // Parameters:

        //   serviceProvider:

        void Initialize(IServiceProvider serviceProvider);

        //

        void InitializeDataBindings();

        //

        void OnShutdown();

    }

}

namespace Microsoft.VisualStudio.Tools.Applications.Runtime

{

    public interface IExtendedEntryPoint : IEntryPoint

    {

        object GetEntryPointObject();

    }

}

For us to differentiate our custom pipeline from the standard VSTA pipeline we will go ahead and define a new view. But as we want to reuse most of VSTA pipeline we would derive our view from the VSTA view as below. We define a method in the view that enables the integration code to hook custom adapters when needed. Define the view in an assembly that can be shared between hosts and add-ins.

    [AddInBase]

    public interface INewEntryPoint : IExtendedEntryPoint

    {

        void HookCustomAdapters();

    }

 

In my case I just defined the interface in the assembly which has the common interface IService as mentioned in the earlier posts. You could also create a new assembly that holds the definition for these interfaces. Now after we have the view definition in place we would have to define pipeline adapters to make sure we have a valid System.AddIn pipeline that could be loaded. Again this pipeline would be an extension of VSTA pipeline as we want VSTA pipeline to handle all requests unless they are specialized for certain cases (the cases that introduce our new custom adapters).

So to define a pipeline we have to define following four components

1.     Contract

2.     Add-In Side Adapter

3.     Host Side Adapter

4.     View

Let’s define a contract that would work with our already defined view. In this case I defined the following contract which again defines from the IEntryPointContract2 to use the existing VSTA infrastructure.

    [AddInContract]

    public interface INewEntryPointContract:IEntryPointContract2

    {

        void HookCustomAdapter();

    }

That would work with our new view. The Contract is defined in a new assembly contracts.dll (this would just ensure our pipeline components are deployed as per System.AddIn pipeline protocol). Let’s create Host Side Adapter that uses our new view. Our host side adapter for this new pipeline will derive from VSTA host side adapter this would make us reuse VSTA pipeline semantics for activation. (We would need reference to Microsoft.VisualStudio.Tools.Applications.HostAdapter.v9.0.dll)

    [HostAdapter]

    class HostSideAdapter:EntryPointHostAdapter

The Host Adapter would implement the view so that it could be act as a handle to the integration code in the host object model.  The adapter would define the constructor that accepts the contract which would help us communicate across contract boundary.

public HostSideAdapter(INewEntryPointContract entryPointContract)

            : base(entryPointContract)

        {

            if (entryPointContract == null)

                throw new ArgumentNullException("contract");

 

            this.handle = new ContractLock<INewEntryPointContract>(entryPointContract);

           

      

        }

As our Contract is derived from IEntryPointContract2 we can initialize the state for the VSTA adapter by passing the contract to the base adapter as above.

Let’s implement the view methods now in this case

  public void HookCustomAdapters()

VSTA pipeline provides TypeInfrastructureManager, it could be considered as a type that provides all type services in VSTA pipeline. It’s exposed as a protected property in VSTA host side adapter.  Now before we start looking at the implementation of HookCustomAdapters let’s create a scenario in our simple object model as in previous posts that would use custom adapters.

We define a new interface that will be used by add-ins and host through custom adapters rather than VSTA pipeline

   public interface INameCollection

    {

        string GetName(int index);

    }

This interface should be again in an assembly that can be shared between add-ins and host. We could have different interfaces for host and add-ins let’s keep that discussion for another post. In this case added the interface to the same assembly that holds our  INewEntryPoint  interface. We need a method to be added in the host object model. I modified the host object model class as below.

public class ServiceProvider

    {

        *old methods / variables omitted for clarity*

 

        public INameCollection GetNames()

        {

            if (collection == null)

            {

                List<string> listStrings = new List<string>();

                listStrings.Add("TemperatureService");

                listStrings.Add("HumidityService");

                collection = new NameCollection(listStrings);

            }

 

            return collection;

        }

 

    }

 

The added method that uses the INameCollection is as above just return a list of strings. Again there is no significance of what the method does. We also add a implementation NameCollection in the host object model that is only exposed as  INameCollection.

  internal class NameCollection : INameCollection

    {

        List<string> _collection = null;

        internal NameCollection(List<string> collection)

        {

            _collection = collection;

        }

 

        #region INameCollection Members

 

        public string GetName(int index)

        {

            if (_collection.Count > index)

            {

                return _collection[index];

            }

 

            return null;

        }

 

        #endregion

    }

 

Now you need to generate proxy for changed object model using method defined in earlier posts. If you don’t want to do the whole process again you could manually change the proxy file as below.

Add the following definition in ServiceProvider class.

        [global::Microsoft.VisualStudio.Tools.Applications.Runtime.HostMemberAttribute("GetNames", BindingFlags = global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.InvokeMethod)]

public virtual INameCollection GetNames() { throw new global::System.NotImplementedException(); }

And following definition in ServiceProviderEntryPoint class

        public INameCollection GetNames()

        {

            return this.RemoteObject.GetNames();

        }

You don’t need to do anything in proxy file for NameCollection type defined in Host Object Model as that’s internal. The interface defined here is only a view for add-ins and host. To cross the contract boundary while working with this INameCollection interface we still need a contract. Define a contract as below in your contracts assembly.

    public interface INameCollectionContract:IContract

    {

        string GetName(int index);

    }

Note that this interface is not marked as [AddInContract] as our INewEntryPointContract because this is not a contract used for activation of add-in pipeline. Now we have our contracts, views and host object model scenarios cooked. Let’s go back to public void HookCustomAdapter() implementation.  In this implementation we will hook our adapter for our INameCollection.

The implementation looks like

  #region INewEntryPoint Members

 

        public void HookCustomAdapters()

        {

            if (this.TypeInfrastructureManager != null)

            {

this.TypeInfrastructureManager.AdapterResolve += new EventHandler<AdapterResolveEventArgs>(TypeInfrastructureManager_AdapterResolve);

            }

 

            handle.Contract.HookCustomAdapter();

        }

 

 #endregion

 

     void TypeInfrastructureManager_AdapterResolve(object sender, AdapterResolveEventArgs e)

        {

            if (typeof(INameCollection).IsAssignableFrom(e.ExpectedType))

            {

e.AdapterToReturn = (INameCollectionContract)(new NameCollectionAdapter((INameCollection)e.ObjectToPack,this.TypeInfrastructureManager));

            }

        }

 

Here we define an event handler that hooks to Adapter Resolve event for TypeInfrastructureManager. The method also called HookCustomAdapters on the contract that will enable add-in side adapter to do any processing if needed for the custom adapter hook up. Look at TypeInfrastructureManager_AdapterResolve you would see we check if the ExpectedType is of type INameCollection we create a specific adapter instance (adapter meant for INameCollection specifically). The adapter just would do impedance match between view and the contract. The adapter definition is as below. Define the following interface in the Host Side Adapter assembly.

public class NameCollectionAdapter:ContractAdapterBase,INameCollectionContract

    {

        INameCollection _nameCollection;

 

        public NameCollectionAdapter(INameCollection nameCollection, TypeInfrastructureManager  infrastructureManager):base(infrastructureManager)

        {

            _nameCollection = nameCollection;

        }

 

 

        #region INameCollectionContract Members

 

        public string GetName(int index)

        {

           return  _nameCollection.GetName(index);

        }

 

        #endregion

 

    }

It just exposes the INameCollectionContract to the add-in side of the pipeline.

Now let’s define Add-In Side Adapter assembly. Here again the adapter would derive from the VSTA add-in side adapter and this time we will just hook up with ProxyResolve event (as we are on add-in side that is consuming the interface).

The definition is as below (you would need reference to Microsoft.VisualStudio.Tools.Applications.AddInAdapter.v9.0.dll)

[AddInAdapter]

    public class CustomAddInAdapter : AddInAdapter, INewEntryPointContract

    {

        private INewEntryPoint entryPoint;

        public CustomAddInAdapter(INewEntryPoint initialEntryPoint)

            : base(initialEntryPoint)

        {

            entryPoint = initialEntryPoint;

 

        }

 

        void TypeInfrastructureManager_ProxyResolve(object sender, ProxyResolveEventArgs e)

        {

            if (e.Adapter is INameCollectionContract)

            {

                e.ProxyToReturn = (INameCollection)(new NameCollectionAddInSideAdapter(e.Adapter as INameCollectionContract));

            }

        }

 

        #region INewEntryPointContract Members

 

        public void HookCustomAdapter()

        {

            this.TypeInfrastructureManager.ProxyResolve += new EventHandler<ProxyResolveEventArgs>(TypeInfrastructureManager_ProxyResolve);

        }

 

        #endregion

    }

 

So now you have add-in side adapter, host side, contract and view assemblies available. Compile these assemblies and copy them in the location that holds VSTA pipeline

Again on my machine this is at

C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline

Copy view assembly that contains INewEntryPoint and INameCollection in

C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline\AddInViews

Contracts.dll in

C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline\Contracts

Similarly add-in side adapter assembly and host side adapter assembly in C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline\AddInSideAdapters and C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline\HostSideAdapters folders respectively.

To create AddInStore cache and make sure pipeline is valid rebuild the vsta pipeline

AddInUtil.exe   –PipelineRoot:” C:\Program Files (x86)\Common Files\microsoft shared\VSTA\Pipeline” –Rebuild

AddInUtil.exe is available in v3.5 framework folder under windows folder on your machine.

Now we have pipeline defined with proxy & host object model changed.

One additional change which we need to do to make activation work is to modify the EntryPoint class in proxy class to use the new entry point interface.

  [global::System.AddIn.Pipeline.AddInBaseAttribute(ActivatableAs = new global::System.Type[] { typeof(INewEntryPoint) })]

    public partial class ServiceProviderEntryPoint : INewEntryPoint

    {

        #region INewEntryPoint Members

 

        public void HookCustomAdapters()

        {

            // (We never call this in our add-in side adapter so no need to implement it)

            throw new System.NotImplementedException();

        }

 

        #endregion

Notice the change above to the new interface. Now let’s change the integration code to use the new pipeline. This could be done by just switching the view type in the integration code. Here we would load our first add-in with new pipeline. See the change where we call the hookcustomadapters to hook custom adapters for our specific interface.

 

            Type havType = typeof(INewEntryPoint);

 

            *code omitted for clarity*

      

            if (addins.Count > 0)

            {

 INewEntryPoint addin =     addins[0].Activate<INewEntryPoint>(AddInSecurityLevel.FullTrust);

                      addin.Initialize(serviceProvider);

                     addin.HookCustomAdapters();

                     addin.InitializeDataBindings();

                     addin.FinishInitialization();

            }

            else

            {

             Console.WriteLine("Add In could not be discovered");

            }

 

The add-in code could test the usage with the following change in the add-in class

        TemperatureService tempServ = (TemperatureService)this.GetService("TemperatureService");

        Console.WriteLine(tempServ.Temperature.ToString());

        HumidityService humidServ = (HumidityService)this.GetService("HumidityService");

        Console.WriteLine(humidServ.Humidity.ToString());

        AddInService service = new AddInService();

        this.RegisterService("MYADDINSERVICE", service);

        INameCollection collection = this.GetNames();

       Console.WriteLine(collection.GetName(0));

So we were able to use the new interface with custom adapters which we hooked to VSTA pipeline.

 

 

Posted by Sandeep Bhatia | 2 Comments
Filed under:

IEntryPoint or IExtendedEntryPoint

If you see the definition of the two interfaces defined in Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll you would notice that IExtendedEntryPoint is derived from IEntryPoint. In most of the host add-in applications add-in is the consumer of the host object model, IEntryPoint was derived from that perspective and it could be used in most of the scenarios where add-in is the consumer of the host object model. But for the scenarios where host also needs to know the add-in object model. IExtendedEntryPoint is the right choice as it provides an additional method on an interface GetEntryPointObject that can be used to get handle to the host object model tree on the add-in side. An example of such a scenario is macro recording. In Macro recording host needs to enumerate the functions on the add-in side and expose these as macros. This could be only enabled by a method that provides a mechanism to query for the object model on the add-in side thats where the GetEntryPointObject comes into picture.

 

 

Details of Integration

We saw in the earlier posts how simple it is to enable an application to load add-ins and enjoy all the benefits that come with loosely coupled usage of the host object model.  CLR Add-In framework is the base for implementation of VSTA 2.0. Refer to blogs.msdn.com/clraddins for details around the Add-In framework.  To keep it simple and straightforward lets dive into the details of the integration code that was provided in the earlier post so that we could understand the workings of VSTA in a better way. The integration code below starts with defining a class Program with a Main method. It defines two instance variables

serviceProvider: As VSTA is based on CLR Add-In framework, this brings up the fact that VSTA is based on a single generic System.AddIn based pipeline. Once the pipeline is activated by standard CLR Add-In activation methods we need a mechanism for the host to hook into this pipeline. One way VSTA pipeline provides these hooks is through its entry point interface (IEntryPoint and IExtendedEntryPoint). This interface at its core is just a view (in terms of CLR Add-Ins terminology) that is used to get handle to the activated add-in. Once this interface is obtained from the initial activation of the add-in Initialize method provides a mechanism of registration of an IServiceProvider implementation from the host.  This service provider is basically responsible for providing two main services TypeMapProvider service and HostItemProvider service. We will look at the role of these services in a while. serviceProvider instance object holds a reference to the service provider.

vstaPipelinePath:This is the relative path under the commonfiles folder where the VSTA pipeline is laid as part of the install. This is under

C:\Program Files\Common Files on x86 machines and

C:\Program Files (x86)\Common Files on x64 machines. 

Hence the absolutePath of the pipeline is as computed in the code below

string commonPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonProgramFiles);

string pipelinePath = System.IO.Path.Combine(commonPath, vstaPipelinePath);

//C:\Program Files\Common Files\ Microsoft Shared\VSTA\Pipeline

 

Once we have the pipeline path with us, we could activate the add-in using AddInStore (System.AddIn.dll) type passing the view and the pipeline path as two of the parameters. Other two parameters are Add-In path and namespace qualified name of the add-in entry point class.

Post activation we need to provide our implementation of service provider. The instance is created in the method below InstantiateServiceProvider. As discussed above the creation of the instance provides specifying two service’s HostItemProvider service and HostTypeMapProvider service which should implement IHostItemProvider and ITypeMapProvider service respectively. Both of these interfaces are defined in Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll.

IHostItemProvider: This interface is a means for the host to provide the host objects to the add-in. add-in initialization code would generally use this interface to get the handle to root object. You could see the entry point class implementation in the proxy class generated using proxy-gen as discussed in the earlier posts.

ITypeMapProvider: VSTA provides isolation of the add-in side types of the pipeline and the host side of the pipeline using contracts. Contracts are a restrictive boundary and only specific types can cross the contract boundary to maintain the type isolation in the CLR pipeline. (Refer to http://blogs.msdn.com/clraddins/archive/2007/02/27/restrictions-on-contracts.aspx) hence mapping between types on the add-in and the host side are maintained using unique string identifiers also referred as canonical names. ITypeMapProvider is an interface through which host can plug in string to type mappings. To facilitate host integration these mappings are generated as part of proxy code generation process via the host object model. (HostTypeMap.cs generated as part of proxy-gen)

 

Posted by Sandeep Bhatia | 0 Comments
Filed under:

Exposing a service through an Add-In

In the previous post we did a walkthrough on how to do simple runtime integration with VSTA 2.0. In this post we would see if how the sample can be modified to support a scenario where an Add-In exposes a service and another Add-In consumes it. I modified our simple OM in the previous blog to add two methods one that accepts new service registrations and other that returns the service when queried by the caller (In this case our Add-In). How these methods are implemented and exposed are just host specific details and we won’t concern ourselves with these details as of now.

    //this is simple class that will interact with addins.

    //different addins will use this provider to query for services!

    public class ServiceProvider

    {

      

       private Dictionary<string, object> _registeredServices = new Dictionary<stringobject>();

 

        public object GetService(string ServiceName)

        {

            if (ServiceName == "TemperatureService")

            {

                return new TemperatureService();

            }

 

            if (ServiceName == "HumidityService")

            {

                return new HumidityService();

            }

 

            return null;

        }

 

        //Added methods

        public void RegisterService(string serviceIdentifier, object serviceObject)

        {

            if(!_registeredServices.ContainsKey(serviceIdentifier))

            {

                _registeredServices.Add(serviceIdentifier, serviceObject);

            }

        }

 

        public object GetRegisteredService(string serviceIdentifier)

        {

            return _registeredServices[serviceIdentifier];

        }

 

    }

As we have changes our OM we will have to regenerate the proxy dll by following the steps mentioned in the previous post. After we have the new proxy make sure your previous add-in works as expected as a sanity check for the changes made.

Now let’s create a service object for our add-in. Here I modified the Add-In to add a class that will act as a service type here and would be exposed by the add-in

public 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());

            //Service registration

            AddInService service = new AddInService();

            this.RegisterService("MYADDINSERVICE", service);

           

        }

    }

 

    //Service Class

    public class AddInService

    {

        public void HandShake()

        {

            Console.WriteLine("Hello World");

        }

    }

 

We now have an add-in that creates a service object and gets registered with the host. Let’s create another Add-In that will consume this service!

Create a second add-in as the first one referencing proxy and Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dl as did in the first add-in. Add the following code as a consumer of the service in the add-in.

//Second Add-In 

namespace App

{

    public class AddIn2:ServiceProviderEntryPoint

    {

        protected override void FinishInitialization()

        {

            object serviceObject = this.GetRegisteredService("MYADDINSERVICE");

            MethodInfo info =

            serviceObject.GetType().GetMethod("HandShake", BindingFlags.Instance |

                                                           BindingFlags.InvokeMethod |   

                                                           BindingFlags.Public);

            info.Invoke(serviceObject, null);

        }

    }

 

}

Lets add the integration code to load this second add-in now in the integration exe that we had.

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 = typeof(IEntryPoint);                                      

            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");

            }

 

            //Second Add-In loaded below

                     addinPath = @" C:\App\AddIn\bin\Output\AddIn2.dll ";

            addins = AddInStore.FindAddIn(havType, pipelinePath, addinPath, "App.AddIn2");

            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;

            }

        }

    }

 

Running the exe and setting the breakpoint in the second add-in would show that the second add-in is able to invoke the method on the service object.

In the sample here we have worked late bound with the service object using reflection to invoke the method. Let’s now see how we can achieve early bound semantics by exposing an interface to both the add-ins.

Create a separate interface assembly that can be referred by both the add-ins and add the service interface that exposes the appropriate service method here (you will need to refer Microsoft.VisualStudio.Tools.Applications.Runtime.v9.0.dll)

You will have to add a host type attribute to the interface type added and host assembly attribute to the interface assembly as below. Later we would discuss in subsequent posts how these declarative attributes provide a mapping mechanism to isolate types between hosts and add-ins.

[assembly: global::Microsoft.VisualStudio.Tools.Applications.Runtime.HostAssemblyAttribute()]

 

namespace ServiceInterfaces

{

    [global::Microsoft.VisualStudio.Tools.Applications.Runtime.HostTypeAttribute("App, App.IService")]

    public interface IService

    {

        void HandShake();

    }

}

Refer the assembly in both the add-ins and make the following changes in add-ins

First Add-In changed as below

public class AddInService:IService

    {

        public void HandShake()

        {

            Console.WriteLine("Hello World");

        }

    }

 

Second Add-In Changed as below

object serviceObject = this.GetRegisteredService("MYADDINSERVICE");

MethodInfo info = serviceObject.GetType().GetMethod("HandShake", BindingFlags.Instance | BindingFlags.InvokeMethod |     BindingFlags.Public);

info.Invoke(serviceObject, null);

//Use the interface to call the method

((IService)(serviceObject)).HandShake();

 

Now we are able to cast to the interface and make the call directly rather than invoing it through MethodInfo object.

 

 

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.

 

 

Posted by Sandeep Bhatia | 2 Comments
Filed under:

The Start!

Welcome to the blog!! I am a dev on VSTA team (Visual Studio Tools For Applications). I would not only try to use this blog as a instrument to share my insights about VSTA but also try to share any other useful information that could solve a problem out there!

 
Page view tracker