Nearly a year ago, I wrote a post on how to detect and fix Word add-in problems with a macro and batch file, in a Windows XP and Office 2007 environment.
This was sufficiently effective, but it was also overly complicated, requiring four separate components:
- an autoexec Word 2007 macro that runs each time Word is opened
- a batch file that runs the registry merge file and writes an entry to a log file
- the registry merge file that contains the correct LoadBehavior settings for the add-ins
- a text file that acts as a log
This month, I decided to rewrite the macro to handle the registry changes and write to the log file. It was also a good opportunity to dig a bit deeper into VBA, and I also wanted to confirm that it would work in a more modern environment of Windows 7 and Office 2010 (that code is near the bottom of the post). The new system has only two components:
- an autoexec Word 2007 macro that runs each time Word is opened
- a text file that acts as a log
Background
First, a bit of background.
Many of the problems with Word 2007 are due to Word’s handling of add-ins. When something unexpected happens in Word, and Word attributes the problem to an add-in, Word will react by flagging it and prompting the user for a decision the next time Word opens. Depending on the severity of the problem and the user’s response, the add-in can be either ‘hard-disabled’ or ‘soft-disabled’.
Microsoft explains the differences between Hard Disabled vs Soft Disabled in a MSDN article at: http://msdn.microsoft.com/en-us/library/ms268871(VS.80).aspx.
I’ve explained a bit about the process by which Word disables add-ins at the end of this post, and I’ve written a shorter post about the basics behind the registry keys responsible for disabling add-ins.
Handling disabled add-ins programmatically
A Word macro can access the condition of an add-in via an Application.COMAddIns object, and it can read and write to the registry. This allows us to tell when an add-in has been disabled and re-enabled it.
My macro has some admittedly hackish parts that need to be cleaned up, there is the matter of unsetting variables to be addressed, and it could certainly be made more elegant, but it works. Note that a file named addinslog.txt must exist in the %TEMP% directory in order for the macro to write the log file. This is what the Word 2007 macro looks like, using the COM add-in installed with Adobe Acrobat 8 Standard as the required add-in…
Option Explicit ' Set up a function to search for a key and return true or false Public Function KeyExists(key) Dim objShell On Error Resume Next Set objShell = CreateObject("WScript.Shell") objShell.RegRead (key) Set objShell = Nothing If Err = 0 Then KeyExists = True End Function Sub AutoExec() ' ' FixMissingAddins Macro ' Display a message box with any critical but not 'Connected' COM add-ins, then fix them programatically ' ' Oliver Baty ' June, 2010 - April, 2011 ' ' Information on the Application.COMAddIns array ' http://msdn.microsoft.com/en-us/library/aa831759(v=office.10).aspx ' ' Running macros automatically ' http://support.microsoft.com/kb/286310 ' ' Using Windows Scripting Shell (WshShell) to read from and write to the local registry ' http://technet.microsoft.com/en-us/library/ee156602.aspx ' Declare the WshShell variable (this is used to edit the registry) Dim WshShell ' Declare the fso and logFile variables (these are used to write to a txt file) Dim fso Dim logFile ' Create an instance of the WScript Shell object Set WshShell = CreateObject("WScript.Shell") ' Declare some other variables Dim MyAddin As COMAddIn Dim stringOfAddins As String Dim listOfDisconnectedAddins As String Dim requiredAddIn As Variant Dim msg As String ' Notes on deleting registry keys and values in VB ' http://www.vbforums.com/showthread.php?t=425483 ' http://www.tek-tips.com/viewthread.cfm?qid=674375 ' http://www.robvanderwoude.com/vbstech_registry_wshshell.php ' Create a string containing the names of all 'Connected' COM add-ins named "stringOfAddins" For Each MyAddin In Application.COMAddIns If MyAddin.Connect = True Then stringOfAddins = stringOfAddins & MyAddin.ProgID & " - " End If Next ' Create an array to hold the names of the critical (required) add-ins named "requiredAddIns" ' Example: change to "Dim requiredAddIns(0 To 4)" if the macro is checking 5 total add-ins) Dim requiredAddIns(0 To 0) As String ' Add each required AddIn to the array requiredAddIns(0) = "PDFMaker.OfficeAddin" ' requiredAddIns(1) = "" ' requiredAddIns(2) = "" ' requiredAddIns(3) = "" ' requiredAddIns(4) = "" ' Cycle through the array of required add-ins, and see if they exist in the connected add-ins list For Each requiredAddIn In requiredAddIns If InStr(stringOfAddins, requiredAddIn) Then ' The required add-in is in the string of connected add-ins msg = msg Else ' The required add-in is not in the string of connected add-ins, so add the add-in name to a string named "listOfDisconnectedAddins" msg = msg & requiredAddIn & vbCrLf listOfDisconnectedAddins = requiredAddIn & " " & listOfDisconnectedAddins listOfDisconnectedAddins = Trim(listOfDisconnectedAddins) End If Next ' If the msg variable is not blank (it contains at least one add-in's name) handle it, otherwise, do nothing If msg = "" Then ' There are no critical, unconnected add-ins (yay!) ' The script can now exit Else ' There are critical add-ins that are not connected, so handle this MsgBox "The following critical Word Add-In(s) are disabled: " & vbCrLf & vbCrLf & msg & vbCrLf & vbCrLf & "To correct this problem, please save any documents you are working on, then close Word and reopen Word." ' I find it extremely hackish to check for each possible key and delete it if found... need to research how to delete the tree ' One potential obstacle to this method is that I've seen a DocumentRecovery subkey under Resiliency (only once, while editing this macro), that I haven't researched yet ' Note: Since the WSH Shell has no Enumeration functionality, you cannot ' use the WSH Shell object to delete an entire "tree" unless you ' know the exact name of every subkey. ' If you don't, use the WMI StdRegProv instead. ' http://www.robvanderwoude.com/vbstech_registry_wshshell.php ' More info on WMI StdRegProv at: ' http://msdn.microsoft.com/en-us/library/aa393664(v=vs.85).aspx ' This is hackish, but it effectively deletes a registry key, if it exists If KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Word\Resiliency\DisabledItems\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\12.0\Word\Resiliency\DisabledItems\" ElseIf KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Word\Resiliency\StartupItems\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\12.0\Word\Resiliency\StartupItems\" ElseIf KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Word\Resiliency\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\12.0\Word\Resiliency\" End If ' To be completely thorough, we can also set the desired LoadBehavior for certain add-ins ' This can be done selectively, and only if the LoadBehavior was incorrect, but the quick and dirty way would be to just force the values WshShell.RegWrite "HKLM\SOFTWARE\Microsoft\Office\Word\Addins\PDFMaker.OfficeAddin\LoadBehavior", 3, "REG_DWORD" ' Release the WshShell object Set WshShell = Nothing ' Declare a few variables for the log file Dim user, machine, datetime, output Set WshShell = CreateObject("WScript.Shell") user = WshShell.ExpandEnvironmentStrings("%USERNAME%") machine = WshShell.ExpandEnvironmentStrings("%COMPUTERNAME%") temp = WshShell.ExpandEnvironmentStrings("%TEMP%") ' Convert the slashes in Now to hyphens to prevent a fatal error datetime = Replace(Now, "/", "-") ' Create the string that will be written to the log file output = datetime + ", " + user + ", " + machine + ", " + listOfDisconnectedAddins ' Write the event to a log file logfile = temp + "\addinslog.txt" ' http://msdn.microsoft.com/en-us/library/2z9ffy99(v=vs.85).aspx ' http://www.devguru.com/technologies/vbscript/quickref/filesystemobject_opentextfile.html Set fso = CreateObject("Scripting.FileSystemObject") Set logFile = fso.OpenTextFile(logfile, 8, True) logFile.WriteLine (output) logFile.Close Set logFile = Nothing Set fso = Nothing ' Should we clear the variables? ' Release the WshShell object Set WshShell = Nothing End If ' Ardamis.com - We're in your macros, fixing your COM add-ins. End Sub
While working on this, I found that there were some gaps in my understanding of the sequence of events that occur when Word 2007 disables a COM add-in. Please comment if you find that any of this is inaccurate or incomplete.
What happens when Word launches
A critical key to the whole business of Word add-ins is HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Word\Resiliency
When Word launches, it looks for data under the Resiliency key and a subkey: HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Word\Resiliency\StartupItems
If the StartupItems subkey contains a REG_BINARY value that corresponds to an add-in, Word throws the familiar warning:
Microsoft Office Word
Word experienced a serious problem with the ‘[addin name]’ add-in. If you have seen this message multiple times, you should disable this add-in and check to see if an update is available. Do you want to disable this add-in?
[Yes] [No]
Choosing No at the prompt removes the Resiliency key and allows Word to continue to launch, leaving the LoadBehavior for that add-in unchanged.
Choosing No also writes an Error event to the Application Event Viewer log:
Event Type: Error Event Source: Microsoft Office 12 Event Category: None Event ID: 2000 Date: 5/23/2011 Time: 3:15:29 PM User: N/A Computer: [WORKSTATION_NAME] Description: Accepted Safe Mode action : Microsoft Office Word. For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.
Choosing Yes at the prompt removes the StartupItems subkey and creates a new DisabledItems subkey. This DisabledItems subkey will contain a different REG_BINARY value, the data of which contains information about the disabled add-in.
Choosing Yes also writes an Error event to the Application Event Viewer log:
Event Type: Error Event Source: Microsoft Office 12 Event Category: None Event ID: 2001 Date: 5/23/2011 Time: 3:12:36 PM User: N/A Computer: [WORKSTATION_NAME] Description: Rejected Safe Mode action : Microsoft Office Word. For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.
At this point, the add-in is ‘hard-disabled’, but not ‘soft-disabled’.
Word then continues to launch, but without loading the add-in.
To see which add-ins have been hard-disabled, click on the Office Button | Word Options | Add-Ins, and scroll down to “Disabled Application Add-ins”.
To see which add-ins have been soft-disabled, click on the Office Button | Word Options | Add-Ins. Select “COM Add-Ins” in the Manage menu and click Go.
Word is somewhat tricky in this regard, as the add-in will not have a checkmark, but the LoadBehavior registry value will be unchanged. At any other time, the presence of a checkmark is an indication of the LoadBehavior, but when an add-in has been hard-disabled, the box will always be unchecked.
What users can do at this point
Going through Word Options and enabling the hard-disabled COM add-in will remove the Resiliency key. This may not make the add-in immediately available in Word, however.
To immediately load the add-in and gain its functionality, you can check the box. Otherwise, close and reopen Word, which will cause Word to launch with the add-in’s specified LoadBehavior.
In case you were curious about the keyboard shortcuts used to enable the first disabled add-in in the list of disabled add-ins (maybe you wanted to do something with SendKeys, for example), they are:
Alt+F, I, A, A, Tab, Tab, Tab, D, Enter, G, Space, Alt+E, C, Alt+F4.
In summary, deleting the Resiliency key after the “serious problem” prompt, then closing and reopening Word, returns Word to a normal operating state.
What I intend to accomplish with the macro is to re-enable the hard-disabled add-in, return any LoadBehavior values back to the desired settings, then prompt the user to save their work and close and reopen Word.
This should return Word to a working state.
Word 2010 on 64-bit Windows 7
As a bonus, here’s the same macro, with some minor adjustments to run in Word 2010 on Windows 7 64-bit, with Adobe Acrobat 9 Pro’s COM add-in acting as one of the required add-ins. The OneNote add-in is not enabled in Word by default, and the macro below does not attempt to enable it, but does consider it a required add-in. This is done to demonstrate the pop-up window. Note that a file named addinslog.txt must exist in the %TEMP% directory in order for the macro to write the log file.
Option Explicit ' Set up a function to search for a key and return true or false Public Function KeyExists(key) Dim objShell On Error Resume Next Set objShell = CreateObject("WScript.Shell") objShell.RegRead (key) Set objShell = Nothing If Err = 0 Then KeyExists = True End Function Sub AutoExec() ' ' FixMissingAddins Macro ' Display a message box with any critical but not 'Connected' COM add-ins, then fix them programatically ' ' Oliver Baty ' June, 2010 - April, 2011 ' ' Information on the Application.COMAddIns array ' http://msdn.microsoft.com/en-us/library/aa831759(v=office.10).aspx ' ' Running macros automatically ' http://support.microsoft.com/kb/286310 ' ' Using Windows Scripting Shell (WshShell) to read from and write to the local registry ' http://technet.microsoft.com/en-us/library/ee156602.aspx ' Declare the WshShell variable (this is used to edit the registry) Dim WshShell ' Declare the fso and logFile variables (these are used to write to a txt file) Dim fso Dim logfile ' Create an instance of the WScript Shell object Set WshShell = CreateObject("WScript.Shell") ' Declare some other variables Dim MyAddin As COMAddIn Dim stringOfAddins As String Dim listOfDisconnectedAddins As String Dim requiredAddIn As Variant Dim msg As String ' Notes on deleting registry keys and values in VB ' http://www.vbforums.com/showthread.php?t=425483 ' http://www.tek-tips.com/viewthread.cfm?qid=674375 ' http://www.robvanderwoude.com/vbstech_registry_wshshell.php ' Create a string containing the names of all 'Connected' COM add-ins named "stringOfAddins" For Each MyAddin In Application.COMAddIns If MyAddin.Connect = True Then stringOfAddins = stringOfAddins & MyAddin.ProgID & " - " End If Next ' Create an array to hold the names of the critical (required) add-ins named "requiredAddIns" ' Example: change to "Dim requiredAddIns(0 To 4)" if the macro is checking 5 total add-ins) Dim requiredAddIns(0 To 1) As String ' Add each required AddIn to the array requiredAddIns(0) = "PDFMaker.OfficeAddin" requiredAddIns(1) = "OneNote.WordAddinTakeNotesService" ' requiredAddIns(2) = "" ' requiredAddIns(3) = "" ' requiredAddIns(4) = "" ' Cycle through the array of required add-ins, and see if they exist in the connected add-ins list For Each requiredAddIn In requiredAddIns If InStr(stringOfAddins, requiredAddIn) Then ' The required add-in is in the string of connected add-ins msg = msg Else ' The required add-in is not in the string of connected add-ins, so add the add-in name to a string named "listOfDisconnectedAddins" msg = msg & requiredAddIn & vbCrLf listOfDisconnectedAddins = requiredAddIn & " " & listOfDisconnectedAddins listOfDisconnectedAddins = Trim(listOfDisconnectedAddins) End If Next ' If the msg variable is not blank (it contains at least one add-in's name) handle it, otherwise, do nothing If msg = "" Then ' There are no critical, unconnected add-ins (yay!) ' The script can now exit Else ' There are critical add-ins that are not connected, so handle this MsgBox "The following critical Word Add-In(s) are disabled: " & vbCrLf & vbCrLf & msg & vbCrLf & vbCrLf & "To correct this problem, please save any documents you are working on, then close Word and reopen Word." ' I find it extremely hackish to check for each possible key and delete it if found... need to research how to delete the tree ' One potential obstacle to this method is that I've seen a DocumentRecovery subkey under Resiliency (only once, while editing this macro), that I haven't researched yet ' Note: Since the WSH Shell has no Enumeration functionality, you cannot ' use the WSH Shell object to delete an entire "tree" unless you ' know the exact name of every subkey. ' If you don't, use the WMI StdRegProv instead. ' http://www.robvanderwoude.com/vbstech_registry_wshshell.php ' More info on WMI StdRegProv at: ' http://msdn.microsoft.com/en-us/library/aa393664(v=vs.85).aspx ' This is hackish, but it effectively deletes a registry key, if it exists If KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\14.0\Word\Resiliency\DisabledItems\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\14.0\Word\Resiliency\DisabledItems\" ElseIf KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\14.0\Word\Resiliency\StartupItems\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\14.0\Word\Resiliency\StartupItems\" ElseIf KeyExists("HKEY_CURRENT_USER\Software\Microsoft\Office\14.0\Word\Resiliency\") Then WshShell.RegDelete "HKCU\Software\Microsoft\Office\14.0\Word\Resiliency\" End If ' To be completely thorough, we can also set the desired LoadBehavior for certain add-ins ' This can be done selectively, and only if the LoadBehavior was incorrect, but the quick and dirty way would be to just force the values WshShell.RegWrite "HKCU\Software\Microsoft\Office\Word\Addins\PDFMaker.OfficeAddin\LoadBehavior", 3, "REG_DWORD" ' Release the WshShell object Set WshShell = Nothing ' Declare a few variables for the log file Dim user, machine, temp, datetime, output Set WshShell = CreateObject("WScript.Shell") user = WshShell.ExpandEnvironmentStrings("%USERNAME%") machine = WshShell.ExpandEnvironmentStrings("%COMPUTERNAME%") temp = WshShell.ExpandEnvironmentStrings("%TEMP%") ' Convert the slashes in Now to hyphens to prevent a fatal error datetime = Replace(Now, "/", "-") ' Create the string that will be written to the log file output = datetime + ", " + user + ", " + machine + ", " + listOfDisconnectedAddins ' Write the event to a log file logfile = temp + "\addinslog.txt" ' http://msdn.microsoft.com/en-us/library/2z9ffy99(v=vs.85).aspx ' http://www.devguru.com/technologies/vbscript/quickref/filesystemobject_opentextfile.html Set fso = CreateObject("Scripting.FileSystemObject") Set logfile = fso.OpenTextFile(logfile, 8, True) logfile.WriteLine (output) logfile.Close Set logfile = Nothing Set fso = Nothing ' Should we clear the variables? ' Release the WshShell object Set WshShell = Nothing End If ' Ardamis.com - We're in your macros, fixing your COM add-ins. End Sub