Add-ins for Multiple Office Versions without PIAs


In a previous post, I discussed how you could build an add-in for multiple versions of Office, and explained the problems in this approach (and why it is not officially supported). One of the reasons this is not supported is because you end up building an add-in which has dependencies on a later version of the Office PIAs, even though your add-in is sometimes deployed to a machine with an earlier version of Office. The canonical example is where you build an add-in that conditionally uses both Office 2003 (and earlier) CommandBar technology and Office 2007 (and later) Ribbon and task pane technologies, as described in my earlier post. This add-in would normally have a dependency on the Office 2007 PIAs (where the IRibbonExtensibility and ICustomTaskPaneConsumer types are declared). When deployed to an Office 2007 machine, all is good, because the Office 2007 PIAs are present. However, when deployed to an Office 2003 machine – even though the Ribbon and task pane functionality is not used, it is still in the add-in code, and therefore still requires the Office 2007 PIAs. Is there a way around this problem?


Of course, one solution is to deploy the Office 2007 PIAs to the machine with Office 2003, but you then have the follow-on problems of registering multiple versions of the PIAs, and of loading the Office 2007 PIAs into an Office 2003 process. Not good.


Another way around this problem is to remove the dependency on the later PIAs. Because of the high degree of backwards compatibility in Office, you can safely assume that if your add-in works on Office 2003 (with the Office 2003 PIAs), then it should also work on Office 2007 (with the Office 2007 PIAs). So, the only issue is how to get it to work on an Office 2003 machine with only the Office 2003 PIAs present, even though your code uses types such as IRibbonExtensibility that are not present in Office 2003 or the Office 2003 PIAs. So, the question becomes, How can you write a solution which uses types defined in an assembly (the Office 2007 PIA) that is not referenced by the solution?


The answer, of course, is ComImport. For documentation on ComImport, see here and here. PIAs, and interop assemblies generally, can be created by using the Tlbimp.exe utility, which reads a type library and outputs an interop assembly, containing metadata that is the managed equivalent of the COM typelib. ComImport is a pseudo-custom attribute that indicates that a type has been defined in a previously published type library. You can apply this attribute when you want to generate interop metadata manually in source code that simulates the metadata produced by Tlbimp.exe.


Here’s an example. Note that I’m declaring an inner namespace “Office”, so that I can refer to the task pane and Ribbon types as if they were declared in the same namespace as the real Office types – that is, assuming the standard using statement with an alias, eg: using Office = Microsoft.Office.Core;.


Taking custom task panes first, note that ICustomTaskPaneConsumer has a member that takes an ICTPFactory object as a parameter. I therefore have to define ICTPFactory as well. ICTPFactory in turn has a member that takes a CustomTaskPane object, and this derives from _CustomTaskPane, so I need to define these two interfaces also. With Ribbons, the IRibbonExtensibility interface is straightforward, but the signatures of the callback methods that I must define for Office to use for my Ribbon controls tend to take IRibbonControl objects as parameters, so I need to define this interface also.


namespace MyOffice2003AddIn


{


    namespace Office


    {


 


        #region Custom Task Pane


 


        public enum MsoCTPDockPosition


        {


            msoCTPDockPositionLeft,


            msoCTPDockPositionTop,


            msoCTPDockPositionRight,


            msoCTPDockPositionBottom,


            msoCTPDockPositionFloating


        }


 


        public enum MsoCTPDockPositionRestrict


        {


            msoCTPDockPositionRestrictNone,


            msoCTPDockPositionRestrictNoChange,


            msoCTPDockPositionRestrictNoHorizontal,


            msoCTPDockPositionRestrictNoVertical


        }


 


        [ComImport, Guid(“000C033B-0000-0000-C000-000000000046”), TypeLibType((short)0x10c0), DefaultMember(“Title”)]


        public interface _CustomTaskPane


        {


            [DispId(0)]


            string Title { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0)] get; }


            [DispId(1)]


            object Application { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)] get; }


            [DispId(2)]


            object Window { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(2)] get; }


            [DispId(3)]


            bool Visible { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(3)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(3)] set; }


            [DispId(4)]


            object ContentControl { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(4)] get; }


            [DispId(5)]


            int Height { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(5)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(5)] set; }


            [DispId(6)]


            int Width { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(6)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(6)] set; }


            [DispId(7)]


            MsoCTPDockPosition DockPosition { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(7)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(7)] set; }


            [DispId(8)]


            MsoCTPDockPositionRestrict DockPositionRestrict { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(8)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(8)] set; }


            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(9)]


            void Delete();


        }


 


        [ComImport, Guid(“000C033B-0000-0000-C000-000000000046”)]


        public interface CustomTaskPane : _CustomTaskPane


        {


        }


 


        [ComImport, Guid(“000C033D-0000-0000-C000-000000000046”), TypeLibType((short)0x10c0)]


        public interface ICTPFactory


        {


            [return: MarshalAs(UnmanagedType.Interface)]


            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]


            CustomTaskPane CreateCTP([In, MarshalAs(UnmanagedType.BStr)] string CTPAxID, [In, MarshalAs(UnmanagedType.BStr)] string CTPTitle, [In, Optional, MarshalAs(UnmanagedType.Struct)] object CTPParentWindow);


        }


 


        [ComImport, Guid(“000C033E-0000-0000-C000-000000000046”), TypeLibType((short)0x10c0)]


        public interface ICustomTaskPaneConsumer


        {


            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]


            void CTPFactoryAvailable([In, MarshalAs(UnmanagedType.Interface)] ICTPFactory CTPFactoryInst);


        }


 


        #endregion


 


        #region Ribbon


 


        [ComImport, Guid(“000C0396-0000-0000-C000-000000000046”), TypeLibType((short)0x1040)]


        public interface IRibbonExtensibility


        {


            [return: MarshalAs(UnmanagedType.BStr)]


            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]


            string GetCustomUI([In, MarshalAs(UnmanagedType.BStr)] string RibbonID);


        }


 


        [ComImport, Guid(“000C0395-0000-0000-C000-000000000046”), TypeLibType((short)0x1040)]


        public interface IRibbonControl


        {


            [DispId(1)]


            string Id { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)] get; }


        }


 


        #endregion


    }


}



How do I arrive at these ComImport declarations? One way is to use the ildasm.exe utility to read the Office 2007 PIA (that is, Office.dll) to get the metadata, although this approach requires you to do further work to massage the metadata into the appropriate code definitions. Another way is to use Lutz Roeder’s Reflector, and this approach pretty much provides you the code definitions without any further work. It should go without saying that it is essential to get the correct GUIDs on each of the types.


 


With these interface type definitions in place, I can use normal expressions in my add-in code to implement custom task panes and Ribbons. For example, I can implement ICustomTaskPaneConsumer like this:


public class TaskPaneImpl : Office.ICustomTaskPaneConsumer


{


    private Office.ICTPFactory ctpFactory;


    internal Office.CustomTaskPane ctp;


 


    public void CTPFactoryAvailable(Office.ICTPFactory CTPFactoryInst)


    {


        try


        {


            ctpFactory = CTPFactoryInst;


            ctp = ctpFactory.CreateCTP(


                “MyOffice2003AddIn.SimpleControl”,


                “SimpleControl”, Type.Missing);


        }


        catch (Exception ex)


        {


            MessageBox.Show(ex.ToString());


        }


    }


}


…and I can implement IRibbonExtensibility as shown below. Note that I have to add the Ribbon customization code manually – because this is an Office 2003 add-in, and Visual Studio won’t allow me to add a wizard-generated Ribbon item to an Office 2003 project. This code assumes I have a simple RibbonX.xml resource which defines one ToggleButton.


[ComVisible(true)]


public class RibbonX : Office.IRibbonExtensibility


{


    public string GetCustomUI(string ribbonID)


    {


        return Properties.Resources.RibbonX;


    }


 


    public void OnToggleTaskPane(


        Office.IRibbonControl control, bool isPressed)


    {


        Globals.ThisAddIn._taskPane.ctp.Visible = isPressed;


    }


}



..and then I can use these implementations in my add-in like this:


public partial class ThisAddIn


{


    internal TaskPaneImpl _taskPane;


    private RibbonX _ribbon;


 


    protected override object RequestService(Guid serviceGuid)


    {


        if (serviceGuid == typeof(Office.ICustomTaskPaneConsumer).GUID)


        {


            if (_taskPane == null)


            {


                _taskPane = new TaskPaneImpl();


            }


            return _taskPane;


        }


        else if (serviceGuid == typeof(Office.IRibbonExtensibility).GUID)


        {


            if (_ribbon == null)


            {


                _ribbon = new RibbonX();


            }


            return _ribbon;


        }


        return base.RequestService(serviceGuid);


    }


}



Using this approach means that I no longer need to reference the Office 2007 PIAs in my add-in, which allows my add-in to work on an Office 2003 machine as well as on an Office 2007 machine, without having to deploy Office 2007 PIAs to the Office 2003 machine.

Excel2003AddInRibbonTP.zip

Comments (10)

  1. John MacFadyen says:

    In my project I also see code from another developer  "TypeLibType((short)0x1040)" being used on one of the interfaces.  What does that specific value translate to?  In this case it is with the IBrowser interface, which I assume is part of the platform SDK.

  2. Garry Trinder says:

    John – the TypeLibTypeAttribute is applied when a type library is imported (and only when the method’s TYPEFLAGS evaluate to non-zero). The attribute is designed to be used by tools that need to know how the original TYPEFLAGS were set. The CLR does not use this attribute. This is documented here: http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.typelibtypeattribute.aspx. The TYPEFLAGS values are documented here: http://msdn.microsoft.com/en-us/library/f6964swx.aspx.

    Working backwards from 0x1040, this equates to the bitwise combination of TYPEFLAG_FDUAL | TYPEFLAG_FDISPATCHABLE, which indicates that the type is a dual dispatch interface.

  3. Having to rely on the correct version of PIAs to be deployed for implementing Office add-ins has become

  4. A Developer says:

    I believe the IRibbonControl is somewhat incomplete. It should be

       [ComImport]

       [Guid("000C0395-0000-0000-C000-000000000046")]

       public interface IRibbonControl

       {

           [DispId(1)]

           string Id { get; }

           [DispId(2)]

           object Context { [return: MarshalAs(UnmanagedType.IDispatch)] get; }

           [DispId(3)]

           string Tag { get; }

       }

    By the way, it is also possible to ComImport IDTExtensibility2, and then there is no need to reference and deploy extensibility.dll.

  5. Hey Andrew, there is another tool that works better than reflector for generating the requisite interop code. I actually used it quite extensively for addins I developed after TEO and if I ever do TEO 4 I will probably use all local interop code rather than reference the PIA’s. I know you guys did your best but the PIA situation is a mess.

    Anyway, the tool is called Aurigma COM to .NET which is no longer developed or supported but it basically does what tlbimp does except it generates source code and it creates slightly better and more customizable code than tlbimp.

    Also worth mentioning for people that take this approach – they should get Adam Nathan’s ".NET and COM the Complete Interoperability Guide" which is extremely valuable. For .NET developers unfamiliar with the way events are handled by tlbimp’d code, there are much more efficient ways of handling it.

    http://www.aurigma.com/Products/COMtoNET/

  6. Garry Trinder says:

    A Developer – yes, you’re right, I missed off the trailing members of IRibbonExtensibility. Actually, I originally intended to talk about how you could do exactly that in your ComImport (that is, omit members you’re not using) but then decided to leave that explanation to another day. Omitting trailing members that you’re not using works just fine, btw.

  7. Garry Trinder says:

    Josh – thanks for the information about Aurigma COM to .NET. It’s a pity this is no longer supported. Also, I wholeheartedly agree with you about Adam Nathan’s book.

  8. Continuing on from my earlier posts on building add-ins for multiple versions of Office , avoiding the

  9. Forrest Guo says:

    在我的上一篇文章中讨论了如何避免依赖OfficePIAs,方法是使用ComImport重新定义主程序的OM接口。有人(一个开发人员)指出我实际上省略了trailing两个成员的IRibbonCont…

  10. There are at least 9 different ways to start or connect to an Office app programmatically in managed