D
David Thielen
Hi;
This article is also at
http://dave.thielen.com/Word Add-In Part I.htm and will be
updated there. But I am posting it here so google searches will find
it.
*****'
Writing this article took a lot of time. And figuring all of this out
took even more time. If you use this article, I ask one thing in
return. Please email the developers you know and ask them to please
take a look at www.WindwardReports.com if they need a reporting
solution.
Write a Word Add-In
Learn to write an add-in in C# that does everything a regular C#
program does.
by David Thielen
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
http://msdn.microsoft.com/vstudio/office/officetools.aspx.
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 theming for buttons
the way you do for a Windows application. But a solution exits.
Knowledge Base article 830033 comes to the rescue. Once again, when
you follow the instructions, turning on theming 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 explination 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 and 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, 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 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:
// 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.
/// <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)
{
object start = startOffset;
object end = endOffset;
Range range = ThisDocument.Range (ref start, ref end);
range.Find.ClearFormatting();
range.Find.Forward = forward;
range.Find.Text = text;
range.Find.Wrap = WdFindWrap.wdFindContinue;
object missingValue = Type.Missing;
int lastStart = forward ? 0 : int.MaxValue;
// the find can actually return stuff outside the range. And
this can come before it finds stuff inside the range.
// so we have to walk until we find our text, or we have gone
past the end (based on forward/reverse).
// when it gets to the end, it will loop through the document
again!
while (true)
{
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 nothing - we are done
if (! range.Find.Found)
return null;
// if in our range, we have it
if ((startOffset <= range.Start) && (range.End <=
endOffset))
return range;
// if forward & past the end or reverse and past the
beginning, OR we've looped - then we're done
if (forward && ((endOffset < range.End) || (range.Start <=
lastStart)))
return null;
if ((! forward) && ((range.Start < startOffset) ||
(range.Start >= lastStart)))
return null;
lastStart = range.Start;
}
}
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 explination 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 is a solid, powerful engine.
Additional Resources:
Microsoft Knowledge Base article 302901: “How To Build an Office COM
Add-in by Using Visual C#” .NET:
http://support.microsoft.com/default.aspx?scid=kb;en-us;302901
Microsoft Knowledge Base article 830033: “How to apply Windows SP
themes to Office COM add-ins”:
http://support.microsoft.com/default.aspx?scid=kb;en-us;830033
MSDN Library: “Automating Word Using the Word Object Model”:
http://msdn.microsoft.com/library/d...l/wroriautomatingwordusingwordobjectmodel.asp
MSDN Library: “Using the COM Add-in Shim Solution to Deploy Managed
COM Add-ins in Office XP” by Misha Shneerson and Siew-Moi Khor:
http://msdn.microsoft.com/library/en-us/dnoxpta/html/odc_comshim.asp
MSDN Library: “Working with the Office XP Primary Interop Assemblies”
by Paul Cornell:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnoxpta/html/odc_oxppias.asp
Writing this article took a lot of time. And figuring all of this out
took even more time. If you use this article, I ask one thing in
return. Please email the developers you know and ask them to please
take a look at www.WindwardReports.com if they need a reporting
solution.
Article home - http://dave.thielen.com/magazines.htm
Copyright © 2004 by David Thielen – All Rights Reserved.
This article may be freely copied as long as it is copied in it’s
entirety.
This article is also at
http://dave.thielen.com/Word Add-In Part I.htm and will be
updated there. But I am posting it here so google searches will find
it.
*****'
Writing this article took a lot of time. And figuring all of this out
took even more time. If you use this article, I ask one thing in
return. Please email the developers you know and ask them to please
take a look at www.WindwardReports.com if they need a reporting
solution.
Write a Word Add-In
Learn to write an add-in in C# that does everything a regular C#
program does.
by David Thielen
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
http://msdn.microsoft.com/vstudio/office/officetools.aspx.
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 theming for buttons
the way you do for a Windows application. But a solution exits.
Knowledge Base article 830033 comes to the rescue. Once again, when
you follow the instructions, turning on theming 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 explination 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 and 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, 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 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:
// 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.
/// <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)
{
object start = startOffset;
object end = endOffset;
Range range = ThisDocument.Range (ref start, ref end);
range.Find.ClearFormatting();
range.Find.Forward = forward;
range.Find.Text = text;
range.Find.Wrap = WdFindWrap.wdFindContinue;
object missingValue = Type.Missing;
int lastStart = forward ? 0 : int.MaxValue;
// the find can actually return stuff outside the range. And
this can come before it finds stuff inside the range.
// so we have to walk until we find our text, or we have gone
past the end (based on forward/reverse).
// when it gets to the end, it will loop through the document
again!
while (true)
{
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 nothing - we are done
if (! range.Find.Found)
return null;
// if in our range, we have it
if ((startOffset <= range.Start) && (range.End <=
endOffset))
return range;
// if forward & past the end or reverse and past the
beginning, OR we've looped - then we're done
if (forward && ((endOffset < range.End) || (range.Start <=
lastStart)))
return null;
if ((! forward) && ((range.Start < startOffset) ||
(range.Start >= lastStart)))
return null;
lastStart = range.Start;
}
}
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 explination 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 is a solid, powerful engine.
Additional Resources:
Microsoft Knowledge Base article 302901: “How To Build an Office COM
Add-in by Using Visual C#” .NET:
http://support.microsoft.com/default.aspx?scid=kb;en-us;302901
Microsoft Knowledge Base article 830033: “How to apply Windows SP
themes to Office COM add-ins”:
http://support.microsoft.com/default.aspx?scid=kb;en-us;830033
MSDN Library: “Automating Word Using the Word Object Model”:
http://msdn.microsoft.com/library/d...l/wroriautomatingwordusingwordobjectmodel.asp
MSDN Library: “Using the COM Add-in Shim Solution to Deploy Managed
COM Add-ins in Office XP” by Misha Shneerson and Siew-Moi Khor:
http://msdn.microsoft.com/library/en-us/dnoxpta/html/odc_comshim.asp
MSDN Library: “Working with the Office XP Primary Interop Assemblies”
by Paul Cornell:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnoxpta/html/odc_oxppias.asp
Writing this article took a lot of time. And figuring all of this out
took even more time. If you use this article, I ask one thing in
return. Please email the developers you know and ask them to please
take a look at www.WindwardReports.com if they need a reporting
solution.
Article home - http://dave.thielen.com/magazines.htm
Copyright © 2004 by David Thielen – All Rights Reserved.
This article may be freely copied as long as it is copied in it’s
entirety.