Using custom adapters with VSTA 2.0 pipeline - Part 2, the ShapeAppBasicCSharpGenericCollectionAdapter sample

ShapeAppBasicCSharpGenericCollectionAdapter sample

This sample shows how to pass a System.Collections.Generic.IList<ShapeAppCSharp. Document > instance from the ShapeAppBasicCSharp host application to its add-in by extending the VSTA pipeline with adapters  The list provided to the add-in is a generic, non-serialized list of Document objects that actually exist in the host application.

To illustrate, here's an add-in example calling this.GetDocuments();:

 

        private void AppAddIn_Startup(object sender, EventArgs e)

        {

                // get list of documents from the host

            System.Collections.Generic.IList<Document> documents = this.GetDocuments();

            Document document = documents[0];

            string dwgName1 = document.Drawings[0].Name;

         }

 

Adding IList<Document> to the ShapeAppCSharp object model

In the ShapeAppBasicCSharp SDK sample, the type Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp. Document is already represented on the proxy side with this type definition (generated by proxygen):

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

namespace Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp

{

    [global::Microsoft.VisualStudio.Tools.Applications.Runtime.HostTypeAttribute("ShapeAppCSharp, Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp.Docume" +

        "nt")]

    public abstract partial class Document : global::System.MarshalByRefObject

    {...}

}

Our design goal is for the Add-in author to pass instances of ShapeApp's host-side Document type (via add-in side proxy Document  type) from host to add-in, within a generic collection: System.Collections.Generic.IList<Document>

To accomplish this we’ll add a new GetDocuments() method to the host class ‘Application’ in the sample’s Application.cs (which contains the main object model class’ source code):

   public partial class Application : System.Windows.Forms.IWin32Window

    {

        IList<Document> docs = null;

        public IList<Document> GetDocuments()

        {

            // for simplicity, we’ll return the default document in the IList

            if (docs == null)

            {

                docs = new List<Document>();

                docs.Add(this.document);

            }

            return docs;

        }

. . .

Then we add the host method’s complement into the proxy object model assembly source in the sample’s proxy.cs class, ApplicationEntryPoint:

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

    public partial class ApplicationEntryPoint : global::ShapeAppCSharp.IShapeAppEntryPoint

    {

       /*uses Custom Adapter for System.Collections.Generic.IList*/

        public System.Collections.Generic.IList<Document> GetDocuments()

        {

            return this.RemoteObject.GetDocuments();

        }

. . .

Notice a new entry point interface: global::ShapeAppCSharp.IShapeAppEntryPoint (which we’ll define later), is implemented by ApplicationEntryPoint.

The meta OM method description is included in the abstract proxy class Application:

[global::Microsoft.VisualStudio.Tools.Applications.Runtime.HostTypeAttribute("ShapeAppCSharp, Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp.Applic" +

        "ation")]

    public abstract partial class Application : global::System.MarshalByRefObject, global::System.Windows.Forms.IWin32Window

    {

        /*uses Custom Adapter for System.Collections.Generic.IList*/

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

        public virtual System.Collections.Generic.IList<Document> GetDocuments() { throw new global::System.NotImplementedException(); }

 

. . .

Adding IList<Document> to the VSTA Pipeline

Since generic collections are not supported with the VSTA proxy/runtime, we cannot simply add the type System.Collections.Generic.IList<Document> into the HostTypeMapProvider as one would normally do to support a new type in their object model.

Instead, we must use custom adapters to pass the System.Collections.Generic.IList<Document> type through the VSTA pipeline from host to addIn.  This is accomplished by overriding, or hooking into the standard VSTA pipeline’s view, host adapter, and addin adapter assemblies.

How the VSTA pipeline segments work together

VSTA 2.0 add-ins are System.AddIn add-in assemblies.   System.AddIn add-in assemblies are discovered and activated (loaded) by the host application using a special protocol contained in System.AddIn.AddInToken.Activate, which is called by the host application to initialize the pipeline:  First, System.AddIn loads the add-in, which in turn, loads its ‘view’ pipeline segment (aka:  the ‘proxy’ assembly);  System.AddIn creates the addIn-side adapter ‘contract’ pipeline segment, passing  an instance of the add-in’s view segment;  System.AddIn creates the host-side adapter ‘view’ pipeline segment, passing a remote instance of the addIn-side adapter contract;  Finally, System.AddIn’s AddInToken Activate<IShapeAppEntryPoint> method returns the host-side adapter ‘view’ to the host as an IShapeAppEntryPoint instance, which the host considers to be the add-in it loaded.  This completes the pipeline initialization, with all pipeline segments holding its neighbor segment, linked from addin back to the host.  When the host releases the host-side adapter instance, the pipeline segments are disposed of in reverse sequence and the add-in unloads from memory.

I created the pipeline segment assemblies using the System.AddIn Tools and Samples pipeline generator utility, which is fed this contract definition to create the rest of the pipeline segments:

namespace ShapeAppCSharp.Contracts

{

    [AddInContract]

    public interface IShapeAppEntryPointContract : IContract

    {

        void HookCustomAdapter();

    }

}

After the bare bones pipeline is generated, we edit all the segments to implement the standard VSTA pipeline IEntryPointContract2.

Contract:

namespace ShapeAppCSharp.Contracts

{

    [AddInContract]

    public interface IShapeAppEntryPointContract : IEntryPointContract2

    {

        void HookCustomAdapter();

    }

}

Notice that the contract segment now implements IEntryPointContract2

View:

namespace ShapeAppCSharp

{

   

    [System.AddIn.Pipeline.AddInBaseAttribute()]

    public interface IShapeAppEntryPoint : IExtendedEntryPoint

    {

        void HookCustomAdapters();

    }

}

IShapeAppEntryPoint interface type is the view of the pipeline ‘seen’ by both the host and add-in.

Host-side Adapter:

namespace ShapeAppCSharp.HostSideAdapters

{

   

    [System.AddIn.Pipeline.HostAdapterAttribute()]

    public class IShapeAppEntryPointHostAdapter : Microsoft.VisualStudio.Tools.Applications.EntryPointHostAdapter, ShapeAppCSharp.IShapeAppEntryPoint

{

       //Implement view: IShapeAppEntryPoint

        public void HookCustomAdapters()

        {

            if (this.TypeInfrastructureManager != null)

            {

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

            }

            _contract.HookCustomAdapter();

        }

 

Notice that the Host-side adapter is a view that derives from VSTA’s EntryPointHostAdapter and implements a view IShapeAppEntryPoint .  This is the face of the VSTA pipeline seen by the host.

 

AddIn-Side Adapter:

namespace ShapeAppCSharp.AddInSideAdapters

{ 

    [System.AddIn.Pipeline.AddInAdapterAttribute()]

    public class IShapeAppEntryPointAddInAdapter : Microsoft.VisualStudio.Tools.Applications.AddInAdapter, ShapeAppCSharp.Contracts.IShapeAppEntryPointContract

    {

       public virtual void HookCustomAdapter()

        {

            if (this.TypeInfrastructureManager != null)

            {

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

            }

 

        }

 

Notice that the AddIn-side adapter is a contract that derives from VSTA’s AddInAdapter and implements a contract IShapeAppEntryPointContract.  This contract is the segment of the VSTA pipeline seen by the addIn’s view (the ShapeAppCSharp proxy, in proxy.cs).

 

 

AddIn-side View (proxy.cs)

    public partial class ApplicationEntryPoint : global::ShapeAppCSharp.IShapeAppEntryPoint

Notice that the proxy assembly implements the view IShapeAppEntryPoint

AddIn Entry Point (.designer.cs)

namespace ShapeAppCSharpAppAddIn {

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Tools.Applications.ProgrammingModel.dll", "9.0.0.0")]

    public sealed partial class AppAddIn : Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp.ApplicationEntryPoint, ShapeAppCSharp.IShapeAppEntryPoint

    {

 

Notice that the AddIn entrypoint class derives from ApplicationEntryPoint , as expected, and as implemented in the proxy assembly (the pipeline view segment with new GetDocuments() method described above), and explicitly implements the new view ShapeAppCSharp.IShapeAppEntryPoint.

**Note:  Each of these assemblies are completely implemented in the sample.

To load an add-in that implements the ShapeAppCSharp.IShapeAppEntryPoint , the host application must discover and activate (load) those kinds of AddIns:

 

Host application  (VstaRuntimeIntegration.cs)

        private bool LoadAddIn(string addInPath, string startUpClass)

        {

 

            // Update VSTA pipeline.

            string[] warnings2 = AddInStore.Update(AddInStoreExtensions.DefaultPipelinePath);

            if (warnings2.Length > 0)

            {

                foreach (string one in warnings2)

                {

                    Console.WriteLine(one);

                }

            }

            try

            {

                Collection<AddInToken> addInToken = AddInStore.FindAddIn(typeof(IShapeAppEntryPoint), AddInStoreExtensions.DefaultPipelinePath, addInPath, startUpClass);

. . .

                IShapeAppEntryPoint addIn = addInToken[0].Activate<IShapeAppEntryPoint>(AddInSecurityLevel.FullTrust);

 

                // Initialize AddIn by calling into functions of the AddIn EntryPoint instance.

                addIn.Initialize(this.serviceProvider);

                addIn.HookCustomAdapters();

 

 

Notice that the VSTA pipeline is updated by calling AddInStore.Update, to include the new segments copied into the appropriate VSTA pipeline subdirectories.

Notice that the kind of Add-ins to be found and activated must connect with the host application via a host view adapter that implements: typeof(IShapeAppEntryPoint).  Earlier we noticed that IShapeAppEntryPoint view is implemented by our host-side adapter pipeline segment.   

Notice that after the normal addIn.Initialize(), the host calls  addIn.HookCustomAdapters, which calls similar methods from host-side adapter to addIn-side adapter, hooking up the VSTA runtime’s TypeInfrastructureManager  events needed to move the document list (System.Collections.Generic.IList<Document>) from host to  add-in.

 

Execution of this.GetDocuments();

Let’s watch it all go…

 

When, the host application calls addIn.FinishInitialization();, the add-in’s AppAddIn_Startup() event is fired and this.GetDocuments(); executes.  This results in a remoteObject. GetDocuments() call to the host Application. GetDocuments() method.

AddIn code execution:

        private void AppAddIn_Startup(object sender, EventArgs e)

        {

            System.Collections.Generic.IList<Document> documents = this.GetDocuments();

            Document document = documents[0];

            string dwgName1 = document.Drawings[0].Name;

         }

 

Host-side AdapterResolve:

 

When the host application passes a type back to the Add-in, the VSTA pipeline calls TypeInfrastructureManager_AdapterResolve to resolve Host-Side types that are not in the HostTypeMap and pass their host adapters across the AppDomain boundary to the Add-In.  In this example, the type that is resolved and passed is System.Collections.Generic.IList<Document>.  The generic Document list elements are enumerated, wrapped in a list of RemoteObjectAdapter, and passed across the AppDomain boundary via System.AddIn.Pipeline.CollectionAdapters.ToIListContract.

 

        void TypeInfrastructureManager_AdapterResolve(object sender, AdapterResolveEventArgs e)

        {

            if (typeof(System.Collections.Generic.IList<Document>).IsAssignableFrom(e.ExpectedType))

            {

                IRemoteObjectContract[] remoteObjectContractArray = null;

                List<System.AddIn.Contract.Automation.IRemoteObjectContract> remoteObjectContractList = new List<System.AddIn.Contract.Automation.IRemoteObjectContract>();

 

                //create list of contracts from objects

                foreach (object obj in (System.Collections.Generic.IList<Document>)e.ObjectToPack)

                {

                    remoteObjectContractList.Add(new RemoteObjectAdapter(obj.GetType(), obj, this.TypeInfrastructureManager));

                }

                remoteObjectContractArray = remoteObjectContractList.ToArray();

 

                e.AdapterToReturn = System.AddIn.Pipeline.CollectionAdapters.ToIListContract<IRemoteObjectContract>

                    ((List < System.AddIn.Contract.Automation.IRemoteObjectContract >) remoteObjectContractList);

               

           }

 


AddIn-side ProxyResolve:

When the host-side add-in adapter’s TypeInfrastructureManager_AdapterResolve event handler completes, the VSTA pipeline calls TypeInfrastructureManager_ ProxyResolve to explicitly resolve host Adapter contracts to Add-in Side adapters, which yield types defined in the Add-in proxy (also called the AddIn-Side ‘View’).  Then the proxy can pass them to the Add-in. 

 

In this contract the type resolved and passed is System.AddIn.Contract.IListContract<System.AddIn.Contract.Automation.IRemoteObjectContract>.  The generic IListContract is converted to IList of RemoteObjectAdapters, each containing a document element references connected to the host side .  The RemoteObjectAdapters yield their Document objects, which populate a new IList<Document> which the proxy passes to the Add-In, completing this.GetDocuments();.

 

        void TypeInfrastructureManager_ProxyResolve(object sender, ProxyResolveEventArgs e)

        {

            if (e.Adapter is System.AddIn.Contract.IListContract<System.AddIn.Contract.Automation.IRemoteObjectContract>)

            {

                  

                IList < System.AddIn.Contract.Automation.IRemoteObjectContract > remoteObjectContractList =

                        System.AddIn.Pipeline.CollectionAdapters.ToIList<IRemoteObjectContract>(e.Adapter as System.AddIn.Contract.IListContract<IRemoteObjectContract>);

               

                IList<Document> docs = new List<Document>();

               

//create list of objects from adapters

                foreach (RemoteObjectAdapter roa in remoteObjectContractList)

                {

                   System.AddIn.Contract.IContract roc = roa.QueryContract(typeof(System.AddIn.Contract.Automation.IRemoteObjectContract).AssemblyQualifiedName);

                   Microsoft.VisualStudio.Tools.Applications.Samples.ShapeApp.Document doc = TypeServices.ObjectFromContract(roc, typeof(Document), this.TypeInfrastructureManager) as Document;

                   docs.Add(doc);

                }

                e.ProxyToReturn = docs;

            }

        }

 

In conclusion

ShapeAppBasicCSharpGenericListAdapter shows the use of custom adapters, tightly integrated with the ShapeAppBasicCSharp object model types and the VSTA pipeline, to pass a custom type (generic Document  list) across the host/add-in appdomain boundary.  By hooking System.AddIn.Pipeline.CollectionAdapters into the VSTA pipeline, and using RemoteObjectAdapters to marshal Document instances across the AppDomain as list elements, the VSTA pipeline is extended, making it possible for the AddIn to automate a generic collection of non-serializable Document objects on the host-side.  By testing, we confirmed that a new Host API type, IList<Document>, is available to the VSTA Add-In author.  

In the my next post (part 3) we'll demonstrate passing Generic collections from the AddIn to the Host application by reference.


Posted Jan 19 2009, 02:44 PM by Gary
Copyright Summit Software Company, 2008. All rights reserved.