Today's Guest Writer: Eric Faller
Eric is a Software Design Engineer on the Office User Experience team focused on user interface extensibility for Office developers.
Another source of frequently-asked RibbonX questions is around the complexity of writing an add-in in C++. Compared to the ease of use of C# or VB.NET, C++ requires a much deeper understanding of what's really going on under the covers and often involves hand-implementing much of the "magic" that the higher-level languages take care of automatically.
This post covers the details of RibbonX's communication with COM add-ins via the IRibbonExtensibility and IDispatch interfaces and shows an example of creating an add-in with ATL. It's primarily intended for C++ developers, but if you're writing an add-in with .NET you may find it useful to understand what the CLR is automatically doing for you under the hood.
As soon as Office boots up a COM Add-In, it checks if it implements the IRibbonExtensibility interface on its main Connect class via a QueryInterface() call for IID_IRibbonExtensibility (defined in the MSO.DLL typelibrary). If it does, it takes the IRibbonExtensibility pointer and QI's it for the IDispatch interface and saves both pointers off in a safe place.
Note that Office queries the IRibbonExtensibility interface for IDispatch, instead of the main interface. Normally this is unimportant, but it allows complicated add-ins to split their IDispatch interfaces off onto multiple objects if they provide multiple IDispatch implementations. For example, Excel add-ins can provide User-Defined Functions (UDFs) via IDispatch, and they usually won't want to have all of their RibbonX callbacks and UDFs on the same object.
Next, RibbonX will call the IRibbonExtensibility::GetCustomUI() method and get the XML for each type of Ribbon that's currently open. Most applications have only one Ribbon that's open all the time (Word, Excel, PowerPoint and Access), but Outlook has many different Ribbon types, any number of which can be open at a given time. GetCustomUI() can be called at arbitrary points after the add-in boots if the user opens up a new type of Ribbon, so add-ins should not do any extraneous processing inside that function or assume that it will always be called immediately after the add-in boots. GetCustomUI() should simply fetch and return the appropriate XML, without any side effects.
Once the appropriate XML is parsed and applied, RibbonX will invoke the add-in's "onLoad" callback (if it exists), as well as any "get" callbacks (such as getEnabled, getVisible or getLabel). These callbacks are all invoked via the IDispatch pointer that was queried for above.
If you're unfamiliar with IDispatch-based interfaces, you may be curious how it is that Office can call arbitrary C++ functions in an add-in, given only their names. For example, consider a button specified with this XML:
<button id="MyButton" onAction="ButtonClicked"/>
In my add-in I can write a ButtonClicked() function, but once it's complied and linked, the "ButtonClicked" name is optimized away and we're left with just a memory address where the function's code begins. How does Office find and call the function? Obviously there's something magic going on, and it's known as IDispatch.
IDispatch is a COM interface used for "dispatching" function calls to objects when their types are unknown or need to be late-bound. It's the reason that this VBA code works even though the "word" variable is not strongly typed:
Set word = Application
The IDispatch interface contains a whole bunch of methods which you can read all about in the documentation, but the main two to be concerned with are GetIDsOfNames() and Invoke().
The GetIDsOfNames() method provides a mapping between names (strings) and "DISPIDs", which are basically integers that represent functions or properties. With the example button above, Office will call into the add-in's GetIDsOfNames() method and ask "hey, do you implement the ButtonClicked function?", and the add-in with either say "yes I do, and it's DISPID number 2" (for example), or "no, I don't implement that function."
Once the function is found, the IDispatch::Invoke() method is used to actually call the function. Invoke() takes the DISPID of the function, an array of parameters, and gets the return value back. In our example Office will call the add-in's Invoke() method and say "call your ButtonClicked function with this IRibbonControl parameter and let me know how it goes."
Parameters and return values are passed around in VARIANT structs, which are basically big unions that can contain values of many different types. We could go into lots of detail about how to set up and use VARIANTs, but fortunately there are ATL classes that take care of all of this for us so there's normally no reason to worry about them.
That pretty much sums up the high-level overview of how IDispatch works, so let's see it in action and build a simple RibbonX add-in in C++ with ATL.
Building a simple C++/ATL RibbonX add-in
The steps for creating a C++ RibbonX add-in start off pretty much the same as for a C# add-in:
- Open up Visual Studio
- Click "New Project"
- Select "Extensibility" under "Project types" and choose "Shared Add-in"
- Give it a name and click OK:
- Click through the wizard that shows up, making sure to check "Create an Add-in using Visual C++/ATL" and "I would like my Add-in to load when the host application loads."
Now you have an empty C++ add-in. Click "Build Solution" just to make sure that it all compiles OK with no problem.
Next, open up Class View, right-click on your CConnect class and select "Add -> Implement Interface…" In the dialog that pops up, select the "Microsoft Office 12.0 Object Library <2.4>" type library and add the "IRibbonExtensibility" interface from it:
Note: you may have an older type library registered instead (such as "Office 11.0 Object Library") if you previously had older versions of Office installed on the same computer. In those cases you can just browse to the "OFFICE12" version of MSO.DLL and select it manually.
Once you're done with that, Visual Studio should have auto-generated your GetCustomUI() function for you. Delete its "return E_NOTIMPL;" and paste in some valid code, like this:
STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
*RibbonXml = SysAllocString(
L" <tab id=\"CustomTab\""
L" label=\"Custom Tab\">"
L" <group id=\"CustomGroup\""
L" label=\"Custom Group\">"
L" <button id=\"CustomButton\""
L" label=\"Click me!\""
return (*RibbonXml ? S_OK : E_OUTOFMEMORY);
Now, a real add-in would obviously not hard-code its XML like this (embedding it as a resource in the DLL would be much better), but this suffices for our simple demo. Don't do this at home!
At this point we should try to compile the add-in and see our dummy button sitting on the Ribbon. Unfortunately when I tried compiling at this stage, there were several compilation errors in the auto-generated code due to namespace conflicts between the MSO type library and other Windows headers. I did these things to fix it:
- Open up "stdafx.h" and move the #import statement for MSO.dll from the bottom of the file up next to the #import statement for the Extensibility library inside the #pragma blocks (remove any 'no_namespace' annotations from that line as well)
- Add "using namespace Office;" to the top of the Connect.h file.
Now we can build successfully and see our button:
If we click it we get an error saying "The callback function 'ButtonClicked' was not found," which makes sense since we haven't written that function or implemented it via IDispatch yet. Let's use ATL to do that now.
Unfortunately Visual Studio 2005 doesn't seem to have a "New ATL Interface" wizard, but we can get the same thing accomplished by creating a generic ATL class and then deleting the implementation. Click "Add Class…" on the Standard Toolbar and select "ATL Simple Object" in the ATL category. Name the object something like "CallbackInterface" and hit Finish.
Now in Class View we have several new objects: an ATL interface called "ICallbackInterface" and an implementation class called "CCallbackInterface." We don't need the implementation, so go ahead and delete all the CallbackInterface.* files from the Solution Explorer. ICallbackInterface is what we care about and it's defined in our add-in's IDL file.
Back in Class View, right-click on ICallbackInterface and select "Add -> Add Method…" In the Add Method Wizard, add a method named "ButtonClicked" with one [in] parameter of type IDispatch* called RibbonControl:
This parameter is the IRibbonControl object that's passed to all RibbonX callbacks. Since "IRibbonControl" isn't in the parameter type dropdown, we have to go with its base type, which is IDispatch (IRibbonControl is not a type supported by the VARIANT structure). If we need it later, we can always call QueryInterface() on it with IID_IRibbonControl and get it.
Now that our interface is defined, right click on the CConnect class and select "Implement Interface…" again to add ICallbackInterface along with IRibbonExtensibility. Double-click the ButtonClicked function in Class View to be taken to the auto-generated implementation. Swap out its placeholder content with something meaningful, like this:
STDMETHOD(ButtonClicked)( IDispatch * RibbonControl)
// Add your function implementation here.
L"The button was clicked!",
L"Message from ExampleATLAddIn",
MB_OK | MB_ICONINFORMATION);
Now when we compile we should see this MessageBox when we click the button. However, there are a couple of problems left before we can do that, the first of which is "error LNK2001: unresolved external symbol _LIBID_ExampleATLAddInLib." Since our DLL is both the source and consumer of our new typelibrary for ICallbackInterface, we need to link in the MIDL-generated C files for it. In Solution Explorer, add the "AddIn_i.c" file, which is the output from running MIDL on our AddIn.idl file. This new file will inherit the solution defaults for PCH files ("Use Precompiled Headers (/Yu)"), which isn't what we want, so right-click on it and switch the file to "Not Using Precompiled Headers".
The last work item is to set up the COM_MAP to properly route the IDispatch calls to our ICallbackInterface. In Connect.h, switch the IDispatch line in the COM_MAP to ICallbackInterface instead of IRibbonExtensibility:
Once that's all built, try out the add-in and see that it works!
That's basically all there is to making a C++ RibbonX add-in with ATL. Obviously a more complicated add-in would have many more callbacks, but the only additional work would be to right-click on ICallbackInterface and select "Add Method.." for each one. Different types of callbacks have different parameters, so you just need to make sure that your callbacks match the C++-style signatures in the RibbonX documentation. A "getLabel" callback, for example, would have the same parameters, except it would have an additional "[out, retval] BSTR *Label" parameter for returning the label.
For more info about RibbonX, check out the documentation mentioned above, the Developer category on this blog, or the Office Discussion Groups if you have other questions not specifically related to the topics of this article.
Update: Eric has made the resulting Visual Studio 2005 project available for download.