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:
private Template TemplateOn
{
get
{
Templates tpltColl = ThisApplication.Templates;
foreach (Template tpltOn in tpltColl)
if (tpltOn.Name == "MY_TEMPLATE.DOT")
return tpltOn;
return ThisApplication.NormalTemplate;
}
}
...
Template thisTemplate = TemplateOn;
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.
public Range Find (int startOffset, int endOffset, bool forward, string text)
{
int rangeStart = Math.Max(0, startOffset - 1);
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 (range.Find.Found && (startOffset <= range.Start)
&& (range.End <= rangeEnd))
return range;
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;
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;
if ((docText != null) && docText.StartsWith(text))
{
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: