Writing a COM Add-In Using Visual Studio

Writing a class that implements IDTExtensibility2 is not particularly difficult, but setting up the registry settings for the application you are targeting and creating the setup package for the COM add-in can be tricky. Luckily, Visual Studio provides a wizard that makes writing COM add-ins considerably easier. The wizard creates two projectsone for implementing the COM add-in and a separate setup project for the COM add-in. The COM Add-In Wizard has actually been part of Visual Studio since version 7.0, but you might not have come across it because it is somewhat hidden in the project hierarchy and listed as a "Shared Add-in" project.

The wizard can be found under Other Project Types > Extensibility > Shared Add-in and is shown in Figure 23-6. The only clue that the Shared Add-in project might have something to do with Office is the Office icon included on the Shared Add-in icon.

Figure 23-6. Creating a Shared Add-in project in Visual Studio.

The Shared Add-In Wizard steps you through the process of creating a COM add-in. One of the advantages of a generic interface such as IDTExensibility2 is that it can be used from just about any application that has a COM object model and as a result all the Office applications support loading IDTExtensibility2 COM add-ins. The wizard enables you to select the Office application that you want your COM add-in to load into, as shown in Figure 23-7. If you select the check box next to multiple Office applications, Visual Studio will register your COM add-in in a way that enables the same COM add-in to load in multiple Office applications.

Figure 23-7. Selecting the application host.

Although it is possible to write a single COM add-in that works in all the Office applications, it actually is quite difficult to write and even more difficult to maintain because bug fixing different application behavior often leads to the code in the COM add-in becoming overcomplicated. If you want to be able to share code among COM add-ins, creating a common library called by an application-specific COM add-in provides a more manageable solution.

In this example, the application host for the COM add-in will be Microsoft Word. Selecting Microsoft Word in the wizard will result in the setup project registering the COM add-in in the correct location for Word so that you do not have to worry about dealing with the registry when you run the project. The registry settings for the COM add-in require a name and description, and this is collected in the next step of the wizard, as shown in Figure 23-8.

Figure 23-8. Setting a name and description for a COM add-in.

The final step of the wizard is used to determine the load behavior of the COM add-in and whether the COM add-in will be installed in HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE. As mentioned before, it is preferable to register the COM add-in in HKEY_CURRENT_USER so that it will be visible in the COM add-ins dialog. Leaving the second check box unchecked in Figure 23-9 will ensure this behavior.

Figure 23-9. Setting load behavior for the COM add-in.

After the wizard has finished, a solution is created in Visual Studio containing the two projects, as shown in Figure 23-10. The main COM add-in project is a standard C# class library project that has been pre-populated with the core references required and a class called Connect in a Connect.cs file that has a basic implementation of the IDTExtensibility2 interface. The setup project will create an installer for the COM add-in that will include all the dependencies detected and will register the COM add-in in the registry.

Figure 23-10. The Solution Explorer view of a default COM add-in solution.

 

Changing the COM Add-In Project to Be More Office-Specific

The COM Add-In Wizard will create a project for any application that supports IDTExtensibility2, and as a result it creates a very generic project. The whole point of writing a COM add-in is to integrate with a particular Office application, so the first thing you need to do is add the appropriate primary interop assembly (PIA) for the application the COM add-in is targeting. The COM add-in being built in this example will load into Microsoft Word, so it needs to have a reference to the Word PIA. You will then be able to cast the application object passed in OnConnection to the Microsoft.Office.Interop.Word.Application object defined in the Word PIA.

Adding the PIA for Word to a project is quite straightforward; it is just a matter of adding the reference to the Microsoft Word 11 Object Library. Right-click the WordAddin project node in the Solution Explorer tree view and choose Add Reference. Doing so brings up the Add Reference dialog shown in Figure 23-11. Click the COM tab, and then select the Microsoft Word 11 Object Library from the list. Finally, click OK to add a reference to the Word PIA to your Visual Studio project.

Figure 23-11. Adding a reference to the Word PIA.

The Connect class that is created by the wizard contains untyped code, so a few changes need to be made to make it more Word aware. By default, the project sets up two member variables within the class that are of type object. The addInInstance variable can be redeclared as type Microsoft.Office.Core.COMAddin object as defined by the Microsoft Office 11.0 Object Library PIA. After you have typed the addInInstance variable as a COMAddin object, you can use it to determine the registry settings for the COM add-in, such as the GUID, the ProgID, and the description. It also has a Connect property of type bool that can be set to false to disconnect the COM add-in.

The applicationObject member variable is also of type object. Because this COM add-in will only ever run inside of Word, it can be safely redeclared as type Microsoft.Office.Interop.Word.Application. Making this change will make developing considerably easier and safer. After changing the declaration of the applicationObject variable, all that remains is to change the assignment lines within the OnConnection method to cast the application argument from object to Microsoft.Office.Interop.Word.Application and the addInInst to Microsoft.Office.Core.COMAddin. Listing 23-3 shows the redeclaration of the addInInstance and applicationObject variables along with the new casts in OnConnection.

Listing 23-3. Strongly Typing applicationObject and addInInstance

public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { applicationObject = application as Microsoft.Office.Interop.Word.Application; addInInstance = addInInst as Microsoft.Office.Core.COMAddIn; } private Microsoft.Office.Interop.Word.Application applicationObject; private Microsoft.Office.Core.COMAddIn addInInstance;

 

Setting the Start Action

The COM add-in is now almost ready to go. The last problem to solve is that the project is set to start up a new instance of Visual Studio rather than Word. This is easily solved by changing the debug settings for the project so that the project will start up winword.exe rather than devenv.exe. To do this, bring up the properties for the project by double-clicking the Properties project item in the Solution Explorer window, and then select the Debug tab. Doing so brings up the dialog with a Start Action section, as shown in Figure 23-12.

Figure 23-12. The Start Action section of the Debug tab.

The start action for the project should be set to Start External Program. This value needs to change to the location of the Word process on your machine, typically C:Program FilesMicrosoft OfficeOffice11winword.exe. Now when you run the project, Word will be started rather than a new instance of Visual Studio.

Word will reuse existing running instances of Word when you run the project. This can cause problems with COM add-in development. If an instance of Word is already running when you run the project, the debugger will attach to that running instance, but your COM add-in will not be loaded into that already running instance. A way to ensure that the COM add-in will always be loaded in a new instance of Word is to pass the command-line switch /w, which will cause Word to always start a new instance.

Excel automatically creates a new instance if you start it up at the command line, so there is no need to do this trick for Excel. Outlook is a single-instance application without the ability to override this behavior, so when programming against Outlook applications you need to shut down Outlook after every run of the project.

My COM Add-In Project Doesn't Work AnymoreWhat Happened?

A common issue that occurs in COM add-in development goes like this. "I just pressed F5 on my COM add-in project and nothing happens! My COM add-in doesn't appear to load. What's the deal?" Office has a system to protect itself from COM add-ins that fail. When you understand the system, you will better understand how to protect against your COM add-in not loading.

Office automatically disables a COM add-in if it detects that it crashed the host application while starting up. When the Office application loads and starts a COM add-in, it puts a sentinel in the registry associated with the COM add-in that it is loading. It then calls the COM add-in's OnConnection and OnStartupComplete methods. If the COM add-in successfully returns from these two methods, Office removes the sentinel in the registry and everything works fine. If the COM add-in crashes in OnConnection or OnStartupComplete or if you stop debugging and kill the Office process before OnConnection or OnStartupComplete return, the sentinel is still sitting in the registry. When you relaunch the Office application, Office detects that a sentinel got left in the registry on the last run and it disables your COM add-in.

It is very easy to have this happen during developmentyou might be stepping through code invoked by your OnConnection or OnStartupComplete entry point and you get to a line of code and say to yourself, "This line of code is completely wrong." You then stop debugging and change the code and press F5 to rerun the COM add-in. But on the second run the COM add-in does not work. Office detects the sentinel in the registry left over from the last run when you killed the process in the middle of OnConnection or OnStartupComplete and it disables your COM add-in.

The situation is even worse for unshimmed managed COM add-ins. The sentinel put in the registry for a managed COM add-in is the name of the DLL that bootstraps the COM add-in. In the case of a nonshimmed COM add-in, the bootstrap DLL is always mscoree.dlla component of the CLR. Mscoree.dll acts as a class factory to create COM objects implemented in managed code for a host such as Office that expects a COM object that implements IDTExtensibility2. It bootstraps the CLR into the Office application process, loads the managed COM add-in registered in the registry, and gives the Office application the managed COM add-in class that implements IDTExtensibility2 and through interop makes that class looks like a COM object to Office.

So suppose you have two add-in projectsAddin1 and Addin2both of which are unshimmed. You are debugging Addin1's OnConnection handler and you hit Stop Debugging in the middle of it. This leaves the sentinel in the registry saying not that Addin1.dll crashed Office but that mscoree.dll crashed Office. Now you open the Addin2 project and run it, and because Addin2 is also registered with mscoree.dll as its class factory both Addin1 and Addin2 (and any other unshimmed managed add-ins) will be disabled.

To un-disable a COM add-in that has been disabled, go to the Help > About box of the Office application and click the Disabled Items button. Doing so pops up a dialog that will let you re-enable mscoree.dll for an unshimmed COM add-in or for a shimmed COM add-in whatever your shim DLL name is.

There is a second way your COM add-in can get disabled. If your COM add-in throws an exception in OnConnection or OnStartupComplete code and does not catch it, that exception propagates out to Outlook and Outlook disables the COM add-in by setting the LoadBehavior key to 2 (HKEY_LOCAL_MACHINESOFTWAREMicrosoftOffice<>Addins<>LoadBehavior). There is an easy way to deal with this issue. Always put your code that handles OnConnection and OnStartupComplete inside a try..catch block. Do not leak any exceptions in OnConnection or OnStartupComplete back to Office. To un-disable a COM add-in that has been disabled in this way, you can change the LoadBehavior key back to 3 using regedit.exe or re-enable the COM add-in using the COM Add-ins dialog.

A Simple Word COM Add-In

To really understand what is possible with COM add-ins in Office applications, refer to chapters on the object models of Excel (Chapters 35), Word (Chapters 68) and Outlook (Chapters 911). To show that the COM add-in being developed actually works, let's add some code to the OnStartupComplete method of the COM add-in, as shown in Listing 23-4. The code will use the application object to add a button to the standard command bar in Word and show a message box when a user clicks the button.

Listing 23-4. A Simple Word COM Add-In

namespace WordAddin1 { using System; using Microsoft.Office.Core; using Extensibility; using System.Runtime.InteropServices; using System.Windows.Forms; using Word = Microsoft.Office.Interop.Word; [GuidAttribute("581C28BD-E701-4AC1-BD75-0979BCEEC91E"), ProgId("WordAddin1.Connect")] public class Connect : Object, Extensibility.IDTExtensibility2 { private Microsoft.Office.Interop.Word.Application applicationObject; private Microsoft.Office.Core.COMAddIn addInInstance; private CommandBarButton simpleButton; private object missing = System.Reflection.Missing.Value; public void OnStartupComplete(ref System.Array custom) { CommandBars commandBars; CommandBar standardBar; commandBars = applicationObject.CommandBars; // Get the standard CommandBar from Word standardBar = commandBars["Standard"]; try { // try to reuse the button is hasn't already been deleted simpleButton = (CommandBarButton)standardBar.Controls[ "Word Addin"]; } catch (System.Exception) { // If it's not there add a new button simpleButton = (CommandBarButton)standardBar.Controls .Add(1, missing, missing, missing, missing); simpleButton.Caption = "Word Addin"; simpleButton.Style = MsoButtonStyle.msoButtonCaption; } // Make sure the button is visible simpleButton.Visible = true; simpleButton.Click += new CommandBarButtonEvents_ClickEventHandler( simpleButton_Click); standardBar = null; commandBars = null; } public void OnBeginShutdown(ref System.Array custom) { } void simpleButton_Click(CommandBarButton ctrl, ref bool cancelDefault) { MessageBox.Show("You clicked on the button"); } } }

Категории