Click here to Skip to main content
15,884,085 members
Articles / Programming Languages / C#
Article

Writing a Word Add-in - Part I

Rate me:
Please Sign up or sign in to vote.
4.37/5 (40 votes)
12 Jan 200517 min read 222.5K   92   25
An explanation of what you need to know to write a Word add-in in C#.

Introduction

Microsoft Word, along with the rest of the Office applications, has an API that can be called by a program to perform operations on the app’s document. If you’ve given this API any thought, you’ve probably considered it something only power users writing VBA scripts would use.

However, that’s not the case: in Word, the powerful API calls the app to perform just about any action the app can perform on its own. Further, you can write an add-in in C# that does everything a regular C# program does. That’s right—the entire .NET Framework is available to your add-in. You end up with a C# program that not only has all of .NET’s functionality, but also Word’s functionality too.

The first question you face when you create a Word add-in is which of two approaches to use: Visual Studio Tools for Office (VSTO) or an add-in that extends the IDTExtensibility2 interface. According to Microsoft, VSTO isn’t for commercial add-ins. (And I’m not sure of when you would want to use it. In fact, when I asked Microsoft P.R. for examples of when you would use VSTO – they could not give me examples either.) However, you should know it is an option. For more information about VSTO, visit MSDN.

In this article, I’ll show you how to create an IDTExtensibility2 Word add-in. To avoid duplicating details the Microsoft Knowledge Base articles explain at great length, I’ll refer you to the articles instead. I’ll also cover the bugs I ran across and the workarounds for them.

First, to create the Word add-in, read Knowledge Base article 302901. Note: as I demonstrate in this article, you don’t create a Microsoft Office System Projects project in Visual Studio. That’s where you go to create a VSTO project.

Now that you have your Word add-in, you can play around with it. However, you’ll soon discover you can’t turn on themes for buttons the way you do for a Windows application. But a solution exists. Knowledge Base article 830033 comes to the rescue. Once again, when you follow the instructions, turning on themes for buttons works perfectly.

Visual Studio performs a bit of magic so it can run your add-in in the debugger. There is a long explanation for this that will appear in Part II of this article.

So create your initial add-in, build it, and install it. Next, go to the Solution Explorer in Visual Studio, right-click on your project, then select Properties. In the Properties dialog, select Configuration Properties | Debugging, and go to the Start Application property. For that property, set the location of WINWORD.exe (for example, it’s C:\Program Files\Microsoft Office\OFFICE11\WINWORD.EXE on my system). Now, you can run or debug from Visual Studio.

I also recommend you never again run the installer on your development machine. Windows seems to get confused by having registry entries for both running the add-in from the debugger and running it as a standard Word add-in. Even if you uninstall the add-in, it appears to cause problems. And if you have to rename your add-in, create a new one with the new name instead, then copy the other files over. Renaming an existing add-in will break because the add-in won’t have any of the registry settings made during the creation process.

Write Your Add-In

Now you’re ready to start writing your add-in. The first step is to look at the documentation for the API so you can see what you can do. You look in the MSDN library. The documentation isn’t there. You try the Word online help. The documentation isn’t there. You search on Microsoft.com, Google, and post in the newsgroups … and the answer is: there is no documentation. Documentation exists for VBA, but not for C#.

It gets worse. The VBA documentation is only available through Word’s online help; the documentation on MSDN is incomplete. And the format for the documentation is likely one you’re not used to, making it hard for you to drill down to the info you need.

You’ll also come across properties that don’t exist. In those cases, you’ll have to call get_Property(). But again, this approach isn’t documented, so you’ll have to try it and see if it works. (Refer to the MSDN article, “Automating Word Using the Word Object Model”.) But wait—there’s more. The .NET API is a COM API designed for VB. So a method that takes two integers, such as Range(int start, int end) is actually declared as Range(ref object start, ref object end). You have to declare two object (not int) variables, assign int values to them, then pass them in. Yet, the int values are not changed by the call and only an int can be passed in.

But wait—there’s even more. I’ve only found this in one place so far but it probably holds elsewhere: there is no Application.NewDocument event (because there is an Application.NewDocument method in the Word API—and C# doesn’t support having an event and a method with the same name). However, you can cast an Application object to an ApplicationClass object, and you can then call ApplicationClass.NewDocument. Problem solved … well, actually, it’s not. The ApplicationClass solution works on some systems, but not on others. (I have no idea why – and could never get a straight answer on this from Microsoft.) But there is a solution. You can also cast the Application object to an ApplicationEvents4_Event object, and you then call the ApplicationEvents4_Event.NewDocument event (ApplicationEvents3_Event.NewDocument in Word 2002). (While this appears to work on all the systems I’ve tested thus far, you might come across systems where it doesn’t work.) So don’t cast objects to ApplicationClass; instead, cast them to ApplicationEvents4_Event objects. And the IntelliSense doesn’t work for the ApplicationEvents4_Event class, so you’ll have to type in the entire line of code, but it will compile and run fine.

Application.WindowSelectionChange is an event I haven’t found a solution for yet. It can always be set, but sometimes it doesn’t fire. And I can’t find any reason for this. I can start Word and it doesn’t fire, but when I exit and immediately restart, it works. It might not work two or three times in a row, but then work ten times in a row. Even when it works, if you enter text or press undo/redo, it doesn’t fire even though it changes the selection. So, it’s not an all selection changes event so much as a some selection changes event.

Enable and Disable Menu Items

Now, say you want to enable or disable menu items, like Word does for Edit, Copy and Edit, and Paste (only enabled if text is selected). An event is fired before the RMB pop-up menu appears. But for the main menu, there is no event before a menu is dropped down. (I know it seems like there must be an event for this, but there isn’t.)

The WindowSelectionChange event is the only method I’ve found to use. However, it’s inefficient because it either fires a lot or doesn’t fire at all, and it doesn’t fire when you enter text (for which there is no event).

So, you’ll need to do two things: first, enable or disable menu items when the selection event fires; and second, in the event handler for menu items that can be enabled or disabled, check when you first enter the event handler, and if the menu items should be disabled, call your menu update method and return. This way, after the user tries to execute that menu item, the menu is correct.

When you create menu objects, you’ll need to keep a couple of things in mind. First, you must store the returned menu objects in a location that will exist for the life of the program. The menus are COM objects, and if no persistent C# object is holding them, they are designated for garbage collection and your events will stop working the next time the garbage collector runs.

Second, the call CommandBarPopup.Controls.Add(MsoControlType.msoControlButton, Type.Missing, Type.Missing, Type.Missing, false) must have false for the final parameter. If this is set to true, as soon as any other add-in sets Application.CustomizationContext to another value, all your menus go away.

Apparently, temporary (the 5th parameter) isn’t the life of the application, but the life of the present CustomizationContext set as the CustomizationContext. If another add-in changes the CustomizationContext, your menu disappears. Given a user can normally have several add-ins, you can never set the 5th parameter to true. The downside is you’re adding your menus to the default template (normal if you don’t change it) permanently. I don’t think you can have menus exist for the life of the application, but not have them added to the template.

Give Users a Template

Another approach is to give users a template to use with your add-in. The template doesn’t have to do anything, but on startup, you look for that template and add your menus only if the template exists. You also add your menus to your template. In essence, you’re using the existence of the template as an Attribute. This is a clean way to have your add-in appear only when you want it to and have it not touch any other part of Word.

Each time your program starts, you need to determine if your menu has already been added (CommandBarControl.Tag is useful for this). If it isn’t there, add it. If it is there, either delete it and then add it, or set your events on the existing items. I delete and add it because over the course of writing the program, the menu items change at times. If you delete and add it, save the location of the menu and add it there. If a user customizes her menu by moving the location of your menu, you don’t want to force it back to the original position the next time Word runs.

When you set the menus, this changes the template, and Word normally asks the user when she exits if she wants to save the changed template. To avoid this, get the value of the default Template.Saved before making the changes, and set Template.Saved to that value after you’re done. If the template was not dirty when you first got the value, it will be set back to the clean value upon completion:

C#
// the template to use
private Template TemplateOn 
{ 
  get
  {
    // find the WordObject template - if not use AttachedTemplate
    // because that's better than nothing. 
    Templates tpltColl = ThisApplication.Templates; 

    foreach (Template tpltOn in tpltColl)
      if (tpltOn.Name == "MY_TEMPLATE.DOT")
        return tpltOn;
      return ThisApplication.NormalTemplate;
  }
}

...
// find the WordObject template - if not use AttachedTemplate because 
// that's better than nothing.
Template thisTemplate = TemplateOn;

// set the customization to our template and see if the template is dirty
bool clean = thisTemplate.Saved;
ThisApplication.CustomizationContext = thisTemplate;
...

thisTemplate.Saved = clean;

...

One warning: don’t look at the Template.Dirty value using the debugger. The act of looking at it sets it to dirty. This is a true Heisenbug. (The Heisenberg theory is that the act of observing a particle affects the particle.)

And even after you take all these measures, there is one more issue. Sometimes, when you close all documents so you have Word running but no document open, the menu events still fire fine, but calls to CommandBarControl.Enabled throw exceptions. Once you create a new document, the problem usually goes away—unless you have two instances of Word, close the first one, then bring up a document in the second one. Then the problem remains. The solution to this is covered in Part II.

Find Text Within Range

Now for the last bug (in this article, at least, and one I’ve only seen when searching for ranges within table cells). You need to find some text within a certain range. So you set the range and call Range.Find.Execute(). However, this call sometimes returns matching text found outside the range you selected. It can find text before or after the range and return it. If it finds it before, it doesn’t mean the text doesn’t exist in your range, just that Range.Find.Execute() hasn’t gotten here yet. (And yes, it is only supposed to return text inside the passed range – but it will return text outside the range.)

To fix this, you’ll need to set range.Find.Wrap = WdFindWrap.wdFindContinue and keep calling it until you get a find in your range, or it goes past the end of your range. However, there is another problem with this approach. The Find can first return text after your range even though there is text inside your range. In this case, you need to cycle through the entire document until it wraps back to your range to find the text. While this can burn a lot of clock cycles (think of a 200-page document where you have to walk all the way around), it only happens in cases where this bug occurs and, fortunately, those cases are rare.

C#
/// <summary>
/// Find the passed in text in the document.
/// </summary>
/// <param name="startOffset">Start the find at this offset.</param>
/// <param name="endOffset">End the find at this offset.</param>
/// <param name="forward">true if search forward.</param>
/// <param name="text">the text to find.</param>
/// <returns>The range the text is at, null if not found.</returns>

public Range Find (int startOffset, int endOffset, bool forward, string text) 
{
    // Word sometimes won't find it if it starts at startOffset.
    // So we start 1 earlier and if it's 
    // there also, we error out on the find and do it the hard way below.
    int rangeStart = Math.Max(0, startOffset - 1);

    // Word API has the end as exclusive, but we treat it as inclusive
    int rangeEnd = Math.Min(endOffset + 1, ThisDocument.Content.End);

    object start = rangeStart;
    object end = rangeEnd;
    Range range = ThisDocument.Range (ref start, ref end);
    range.Find.ClearFormatting();
    range.Find.Forward = forward;
    range.Find.Text = text;
    range.Find.Wrap = WdFindWrap.wdFindStop;

    object missingValue = Type.Missing;
    range.Find.Execute(ref missingValue, ref missingValue, 
                ref missingValue, ref missingValue, ref missingValue, 
                ref missingValue, ref missingValue, ref missingValue, 
                ref missingValue, ref missingValue, ref missingValue, 
                ref missingValue, ref missingValue, ref missingValue, 
                ref missingValue);

    // if in our range, we have it
    if (range.Find.Found && (startOffset <= range.Start) 
                             && (range.End <= rangeEnd))
        return range;

    // there is a bug where it sometimes doesn't find text.
    // So here we do it the old fashioned way.
    // find the start - there may be several tags
    // before the tag we are on in this para.
    // and we might be going forward - or backwards!
    start = startOffset;
    end = rangeEnd;
    range = ThisDocument.Range (ref start, ref end);
    string docText = range.Text;

    if (docText == null)
        return null;

    int ind = forward ? docText.IndexOf(text) : docText.LastIndexOf(text);
    if (ind == -1)
        return null;

    // ok, figure out the range for this - can't jump to start
    // + ind as that can go past the begining (no idea why)

    int _start = forward ? startOffset : rangeEnd - text.Length;
    while ((startOffset <= _start) && (_start < rangeEnd))
    {
        start = _start;
        end = rangeEnd;
        range = ThisDocument.Range (ref start, ref end);
        docText = range.Text;

        // see if we are now starting on it
        // - make sure it's that tag in our range

        if ((docText != null) && docText.StartsWith(text))
        {
            // get all of it (may have formatting in it)
            int endPos = range.Start + text.Length;

            while (endPos < endOffset) 
            {
                range.End = endPos;
                docText = range.Text;
                if (docText == text)
                    break;
                endPos ++;
            }
            return range;
        }
        _start += forward ? 1 : -1;
    }
    return null;
}

All in all, I’d say you should view the Word .NET façade as fragile. Aside from the Find bug, these problems are probably due to the façade and not Word itself. But keep in mind, you might have to experiment to find a way to talk to Word in a way that is solid.

Beyond the bugs I’ve discussed, you’ll want to keep these issues in mind for your add-in. If you need to tie data to the document, use Document.Variables. Two notes about this: first, Document.Variable only accepts strings (I uuencode my data); and second, it has a size limit between 64,000 and 65,000 bytes. Also, if you serialize your data, be aware that .NET sometimes has trouble finding the assembly of your add-in when deserializing the objects. This is a .NET issue, not a Word one.

If you’re going to call the Help methods from your add-in, they require a Control object for the parent window. Given Word isn’t a .NET program, you have no way to convert an hwnd (Word’s main window) to a Control. So for Help, you’ll need to pass a null parent window.

Occasionally (in my case, it’s about once a week), when you’re building the COM add-in shim, you’ll get the error:

stdafx.h(48): error C3506: there is no typelib registered 
            for LIBID '{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}'

When you get this, go to C:\Program Files\CommonFiles\Designer and run the regsvr32 msaddndr.dll command. I have no idea why this happens (sometimes it occurs between two builds when all I’ve done is edit a CS file), but it’s easy to fix.

The add-in you create is a COM add-in, not an automation add-in. To see it, add the tool menu item called curiously enough “COM Add-Ins” to the Tools menu.

Exit All Instances of Word

One issue that can sideswipe you in a myriad of ways is that if you use Outlook, it uses Word for composing messages. Word loads add-ins when the first instance starts, and uses that set of add-ins for all other instances. The only way to reload instances is to exit all instances of Word.

Word also locks the files it’s using, such as the DLL files for add-ins. Again, the executable files for your add-in are locked. If you run the debugger, it appears to lock the PDB files too. So if things become strange—builds fail, files are locked, new changes aren’t executing—make sure you’ve exited Outlook as well as all instances of Word. This isn’t a bug; it’s a correct operation, but it’s not obvious and can be frustrating to figure out.

Winword.exe can also run as an orphan process at times. No copy of Word will be on the screen; Outlook isn’t running; somehow an instance of Word never exited. Again, in this case, you can’t build until you kill that orphan process.

You’ll also come across the standard Visual Studio issue where occasionally you’ll need to exit and restart Visual Studio. This isn’t a Word issue (my work colleagues have the same problem when they’re writing C# Windows application code), and if all else fails, reboot.

Remember that Outlook starts Word without a document. Make sure your add-in handles this. Also keep in mind that because the API was originally from VB, all arrays are 1-based. This is a major pain to remember, but Microsoft does need to keep the API consistent across all .NET languages, and VB was used first for the API, so it wins.

Another issue to watch for is that Word doesn’t display exceptions thrown in your add-in. You can catch exceptions and act on them, but if you don’t catch the exception, it’s never displayed. You have to set your debugger to break on managed exceptions so you can see them, or put a try/catch in every event handler.

An IDTExtensibility2 add-in can run on Word 2002 (VSTO is limited to Word 2003). However, MSDN’s article on installing the Office 10 Primary Interop Assemblies, or PIAs, (see Additional Resources) is wrong or incomplete. You get the PIAs added to the GAC, but you can’t find them with add references – and you cannot select a file in the GAC. So copy the files out of the GAC, add those copied files, and it will then point at the PIAs in the GAC.

What about Word 2000? Supposedly, PIAs can be built for it. I’m still trying to figure this out, and if I do figure it out, look for it to be covered in Part II or Part III. But Microsoft does not provide Word 2000 PIAs.

Okay, you’ve got your add-in working; just a few more minutes and you can ship it to the world—or so it would seem. First comes the fact that if you Authenticode sign your C# DLL, it doesn’t matter to Word. You have to create a COM shim DLL that then calls your C# DLL. The MSDN article “Using the COM Add-in Shim Solution to Deploy Managed COM Add-ins in Office XP” covers this. Note: this does not work for VSTO applications. (To vent for a second: WHY, Why, why didn’t Microsoft set it up so Word accepts signed .NET DLLs as a signed Word add-in?) Follow the article very closely. In a number of cases, if your changes to the shim are just slightly wrong, the shim won’t work. And figuring out what is wrong is not easy. In fact, the only way to fix problems is to keep trying different approaches. There is a lot to cover about the shim add-in, and it is covered in Part II.

IMHO, this is the one place Microsoft really blew it. The shim solution adds a lot of work for every developer (as opposed to Microsoft doing it once up front), makes the whole solution more complex, and there is no way to debug problems with it.

Once you have the shim working, you’ll need to strong name all .NET DLLs, then Authenticode sign all DLLs. This is necessary to run on systems where security is set to High/do not trust add-ins.

There is one final issue you must address for your code to install correctly. Your initial setup program has registry settings in the HKCU for your add-in. These need to be deleted as you are not setting up your .NET code to be the add-in, but the shim instead. In your shim, the ConnectProxy.rgs file sets its add-in registry entries in the HKCU. If you always want to install your add-in for the current user only, this is fine. But if you want to give your users the choice of installing for everyone or installing for just me (and for 98% of you, you should), place the registry settings in the setup program under User/Machine Hive and delete them from ConnectProxk.rgs. (There will be a longer explanation of this in Part II.)

The biggest problem the Word add-in C# API suffers from is neglect, and its issues reside almost entirely in the .NET/C# wrapper and its associated features (such as documentation, signing security, and the shim layer). This means, once you figure out how to work around these issues, you’re left with a solid, powerful engine.

Additional Resources:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Chief Technology Officer Windward Studios
United States United States
CTO/founder - Windward Studios

Comments and Discussions

 
QuestionAre you sure VSTO isn't for commersial add-ins? Pin
Evgeny.Knyazev17-Aug-09 21:48
Evgeny.Knyazev17-Aug-09 21:48 
AnswerRe: Are you sure VSTO isn't for commersial add-ins? Pin
David Thielen18-Aug-09 3:35
David Thielen18-Aug-09 3:35 
QuestionPIAs office 2000 problem Pin
mgruby20-Nov-08 3:36
mgruby20-Nov-08 3:36 
GeneralCOM Shim wizard Pin
Eric P Schneider12-Nov-08 9:55
Eric P Schneider12-Nov-08 9:55 
Generalnice article. Pin
manjeetbhadwal16-Oct-08 0:45
manjeetbhadwal16-Oct-08 0:45 
GeneralThe new event Pin
Morten Louw Nielsen4-Apr-07 1:17
Morten Louw Nielsen4-Apr-07 1:17 
Questiondo you know it is possible to replace Word dialog? Pin
xingyun, he11-Mar-07 17:08
xingyun, he11-Mar-07 17:08 
QuestionHow to connect to your Add-in from some application? Pin
Preky18-Jun-06 21:41
Preky18-Jun-06 21:41 
QuestionIsolate Outlook 2003 from Word 2003 in COM Addin Pin
mervin1321-Oct-05 3:48
mervin1321-Oct-05 3:48 
AnswerRe: Isolate Outlook 2003 from Word 2003 in COM Addin Pin
Geralzibob1-Nov-05 13:59
Geralzibob1-Nov-05 13:59 
QuestionWord add-in deployment problems Pin
tartancli14-Sep-05 2:52
tartancli14-Sep-05 2:52 
AnswerRe: Word add-in deployment problems Pin
Japan Shah26-Aug-07 20:18
Japan Shah26-Aug-07 20:18 
AnswerRe: Word add-in deployment problems Pin
Biju Alapatt2-Sep-07 3:44
Biju Alapatt2-Sep-07 3:44 
GeneralModifying built in commands in c# word add-in Pin
tartancli23-Aug-05 5:49
tartancli23-Aug-05 5:49 
GeneralOutlook Pin
Anonymous17-Feb-05 0:50
Anonymous17-Feb-05 0:50 
GeneralCOM Add-in using odc_comshim demo Pin
Member 171877216-Feb-05 2:06
Member 171877216-Feb-05 2:06 
GeneralRe: COM Add-in using odc_comshim demo Pin
Dead Skin Mask7-Apr-05 2:35
Dead Skin Mask7-Apr-05 2:35 
GeneralLooking forward to Part II Pin
Hannes Pavelka30-Nov-04 6:14
Hannes Pavelka30-Nov-04 6:14 
GeneralDavid, When is part II coming? Pin
zevel22-Feb-05 2:56
zevel22-Feb-05 2:56 
GeneralGood Start, but Needs More Code Pin
Daniel_Hochee28-Nov-04 21:05
Daniel_Hochee28-Nov-04 21:05 
I'm writing a C# COM Add-In for Outlook and am having a hard time finding examples describing how to perform specific functions. I picked up a few interesting pointers from your contribution, but without a downloadable solution to look at or a lot more code excerpts, it's not too useful.

As you noted, there are lots of undocumented properties and methods in the Office object models that don't work when using the basic PIA classes due to C#'s strong typing. I would like to see specific code examples of how you worked around these (at the moment I'm looking for info on adding custom menus to the main menu bars, but any specifics are helpful). Microsoft provides decent step-by-step articles on the basics of creating the Add-Ins, but they fall short on the details.

Hope that helps as you forge ahead with Part 2. Incidentally, although you haven't received a lot of positive feedback yet, please don't let that hold you back. Creating Office COM Add-Ins can be a frustrating endeavor, and the more samples and documentation there is, the better for all of us.
GeneralRe: Good Start, but Needs More Code Pin
David Thielen30-Nov-04 9:33
David Thielen30-Nov-04 9:33 
GeneralRe: Good Start, but Needs More Code Pin
dsfasdfasdfasdfasdf7-Mar-07 12:52
dsfasdfasdfasdfasdf7-Mar-07 12:52 
GeneralUse the Macro Recorder Pin
magrasso3-Feb-05 6:17
magrasso3-Feb-05 6:17 
GeneralRe: Good Start, but Needs More Code Pin
frenchyyy4-Feb-05 5:08
frenchyyy4-Feb-05 5:08 
GeneralRe: Good Start, but Needs More Code Pin
wpm18-Feb-05 15:28
wpm18-Feb-05 15:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.