Bug in MS Word's VBA Document Collection, not sure why this workaround crashes - vba

MS Word 2010 has a bug in its ability to correctly maintain (of all things) the documents collection (link to earliest report found - social.msdn.microsoft.com).
As far as I can tell this bug only impacts Word 2010. Although the documents collection is not maintained, it turns out that the Application.Windows collection is. Hence, for Word 2010 the following code based on the original reporters investigation (see below) and this question on answers.microsoft.com seem to provide a good alternative to the buggy documents collection:
' PURPOSE:
' Return a document collection, work-around for Word 2010 bug
Public Function docCollection() As VBA.Collection
Dim indexOfAvailableAppWindows As Long
Dim resultDoc As VBA.Collection
Dim foundDoc As Word.Document
Set resultDoc = New Collection
For indexOfAvailableAppWindows = 1 To Application.Windows.Count
If Application.Windows(indexOfAvailableAppWindows).Document.Type = wdTypeDocument Then
Set foundDoc = Application.Windows(indexOfAvailableAppWindows).Document
resultDoc.Add foundDoc, foundDoc.FullName ' Can NOT use 'name' - fails to be unique
End If
Next indexOfAvailableAppWindows
Set docCollection = resultDoc
Set resultDoc = Nothing
End Function
However, and here's my question, the above code some times fails with error 457 This key is already associated with an element of this collection on line resultDoc.Add foundDoc, foundDoc.FullName. What circumstances could possibly lead to such a failure?
So far the code has only failed on 1 PC running Word 2016. I don't have access to the PC. I did discover that the original version used Document.Name as the key (which was not always unique, so this was changed to Document.Full name)
Assumptions:
Document.FullName will always be unique
Things I've ruled out:
use of Split Window
opening downloaded documents (protected window documents are not counted)
Code that can be used to demonstrate the issue in Word 2010 (adapted from the original report).
' Function Credit Bas258 (https://social.msdn.microsoft.com/profile/bas258)
Function test01() As Boolean
'Adapted to VBA from original: 03-11-2012 1.0 Visual Studio 2008 VB code
Dim oDoc As Word.Document
Dim oDoc0 As Word.Document
Dim oDoc1 As Word.Document
Dim oDoc2 As Word.Document
Dim oDoc3 As Word.Document
Dim oDoc4 As Word.Document
Dim n As Integer
Set WDapp = Application
With WDapp
Debug.Print (Format(Now(), "dd-MM-yyyy") & " MS Office " & .Application.Version)
Set oDoc0 = .Documents.Add: Debug.Print ("add " & oDoc0.Name)
Set oDoc1 = .Documents.Add: Debug.Print ("add " & oDoc1.Name)
Set oDoc2 = .Documents.Add: Debug.Print ("add " & oDoc2.Name)
Set oDoc3 = .Documents.Add: Debug.Print ("add " & oDoc3.Name)
Set oDoc4 = .Documents.Add: Debug.Print ("add " & oDoc4.Name)
For n = 1 To .Documents.Count
Debug.Print ("count " & n & " " & .Documents(n).Name)
Next n
Debug.Print ("close " & oDoc4.Name)
oDoc4.Close
Set oDoc4 = Nothing
Debug.Print ("close " & oDoc3.Name)
oDoc3.Close
Set oDoc3 = Nothing
For n = 1 To .Documents.Count
Debug.Print ("count " & n & " " & .Documents(n).Name)
Next n
n = 0
For Each oDoc In .Documents
n = n + 1
Debug.Print ("doc " & n & " " & oDoc.Name)
Next oDoc
n = 0
For Each oWin In .Windows
n = n + 1
Debug.Print ("win " & n & " " & oWin.Document.Name)
Next oWin
Debug.Print ("close " & oDoc2.Name)
oDoc2.Close
Set oDoc2 = Nothing
Debug.Print ("close " & oDoc1.Name)
oDoc1.Close
Set oDoc1 = Nothing
Debug.Print ("close " & oDoc0.Name)
oDoc0.Close
Set oDoc0 = Nothing
End With
Set WDapp = Nothing
End Function

This is NOT going to be the accepted answer. Although it does answer the broader question (what could cause this code to crash) it not address the specific crash that I am trying to isolate. Either way there appears to be another bug in MS Word which seemed to be worth capturing for the common good.
This time the bug is with the Windows Collection; and joy of joys, I've confirmed it for both Word 2010 and Word 2016 - both 64 bit apps.
Steps to reproduce the bug are as follows:
In windows explorer enable the Preview Pane
Select a word document FILE so that it is 'previewed'
Open the same document (without losing the 'preview view')
Run the code from the OP, it will crash on this line:
If Application.Windows(indexOfAvailableAppWindows).Document.Type = wdTypeDocument Then
It turns out that when a word file is being previewed the Application.Windows.Count property is incremented by the preview; however any attempt to get a property of that window results in Error 5097 - Word has encountered a problem.
So, an improvement to the original code would therefore be:
' PURPOSE:
' Returns a healthy document collection
' - work-around for Word 2010 bug
' - excludes hits from Windows Explorer Preview Pane
Public Function docCollection() As VBA.Collection
On Error GoTo docCollectionError
Dim indexOfAvailableAppWindows As Long
Dim resultDoc As VBA.Collection
Dim foundDoc As Word.Document
Set resultDoc = New Collection
' Use index instead of Each to avoid For Loop Not initialised error, preview pane
For indexOfAvailableAppWindows = 1 To Application.Windows.Count
If Application.Windows(indexOfAvailableAppWindows).Document.Type = wdTypeDocument Then
Set foundDoc = Application.Windows(indexOfAvailableAppWindows).Document
resultDoc.Add foundDoc, foundDoc.FullName ' Key must NOT be 'name' - fails to be unique see BUG: 1315
End If
lblSkipThisDoc:
Next indexOfAvailableAppWindows
Set docCollection = resultDoc
Set resultDoc = Nothing
Exit:
Exit Function
docCollectionError:
If Err.Number = 5097 Then ' An open document is also open in the Windows Explorer Preview Pane
Err.Clear
Resume lblSkipThisDoc ' - skip this window
End If
If Err.Number = 457 Then ' Key is already used, but HOW? Unknown cause of error
Err.Clear
Stop 'Resume lblSkipThisDoc ' Is it safe to skip this document, why is there a duplicate?
End If
End Function

There is a setting in MS Word that enables 1 document to be viewed in 2 windows. In Word 2010 it is under the View (Tab): Window > New Window
The new window is counted separately in Application.Windows.Count and returns the same document object, hence the key exists.
For indexOfAvailableAppWindows = 1 To Application.Windows.Count ' <<< New Windows is counted
If Application.Windows(indexOfAvailableAppWindows).Document.Type = wdTypeDocument Then
Set foundDoc = Application.Windows(indexOfAvailableAppWindows).Document
resultDoc.Add foundDoc, foundDoc.FullName ' <<< fails to add 2nd instance of document
End If
So... the solution would likely involve checking the caption of the document:
IMMEDIATE WINDOW:
?foundDoc.Windows(1).Caption
Document2:1

Related

Access Code partially stopped working (not populating data to word document)

I have an Access database with linked tables. I have created a code to do the following:
1- Create a folder in a specific location with a specific name (name populated from data in access).
2- Open a word document saved in a specific path
3- I then use formfields in the document to populate the word document with data from the table
4- Lastly, I save the word document to the previously created folder with a new name using data from the table
I have been using this code successfully for well over a year with no issues.
Suddenly, for no apparent reason and without any change to the code it stopped populating the word document with data. note, its still doing steps 1,2, & 4 but not step 3.
I cannot figure out what the issue is and any help would be much appreciated.
Below is a sample of the code used:
Sub Onboarding_Documents_Saudi_Click()
'STEP ONE: create the appropriate Folder
Dim fs, cf, strFolder
On Error Resume Next
strFolder = "C:\Users\1161\OneDrive - Anfas Medical Care\Master - Anfas Medical Care\New Employees\" & Me.Name_In_English & " " & Me.Emp_Id
Set fs = CreateObject("Scripting.FileSystemObject")
If fs.FolderExists(strFolder) = True Then
MsgBox "'" & strFolder & "' already exists!"
Else
Set cf = fs.CreateFolder(strFolder)
If fs.FolderExists(strFolder) = True Then
MsgBox "'" & strFolder & "' successfully created!"
Else
MsgBox "'" & strFolder & "' was not successfully created!"
End If
End If
'STEP TWO:Make Contract .
Dim appWord As Word.Application
Dim doc As Word.Document
Dim Base As String
Base = Format(Me.base_salary, "Standard")
Dim Housing As String
Housing = Format(Me.housing_allowence, "Standard")
Dim Trans As String
Trans = Format(Me.transportation_allowence, "Standard")
On Error Resume Next
Err.Clear
Set appWord = GetObject(, "Word.Application")
If Err.Number <> 0 Then
Set appWord = New Word.Application
End If
Set doc = appWord.Documents.Open("C:\Users\1161\OneDrive - Anfas Medical Care\Master - Anfas Medical Care\Forms\Onboarding Documents\Access\Saudi\ContractSaudiAccess.docx", , True)
With doc
.FormFields("frnameinarabic").Result = Me.Name_In_Arabic
.FormFields("frnameinenglish").Result = Me.Name_In_English
.FormFields("frid").Result = Me.Document_ID_number
.FormFields("frmobile").Result = Me.mobile_number
.FormFields("frjtenglish").Result = Me.Job_title_English
.FormFields("frjtarabic").Result = Me.Job_Title_Arabic
.FormFields("frbasesalary").Result = Base
.FormFields("frhousing").Result = Housing
.FormFields("frtrans").Result = Trans
.FormFields("fremail").Result = Me.Personal_Email
.FormFields("empid").Result = Me.Emp_Id
.FormFields("joindate").Result = Me.Join_Date
.FormFields("joindatehijri").Result = Me.[Join Date Hijri]
.FormFields("contractperiod").Result = Me.[Contract Length]
.FormFields("contractperiodar").Result = Me.[Contract Length Ar]
.FormFields("frdepartment").Result = Me.Department
.FormFields("frdepartmentarabic").Result = Me.Department_Ar
.FormFields("joindate1").Result = Format(Me.Join_Date, "dddd dd/mmm/yyyy", vbUseSystemDayOfWeek)
.Activate
.Visible = True
.Activate
End With
doc.Fields.Update
doc.SaveAs2 "C:\Users\1161\OneDrive - Anfas Medical Care\Master - Anfas Medical Care\New Employees\" & Me.Name_In_English & " " & Me.Emp_Id & "\" & FileName & "Contract " & Me.Name_In_English & " " & Me.Emp_Id & ".docx"
Set doc = Nothing
Set appWord = Nothing```
Could this be a change due to Office Updates? I have something similar which works with Word v1910 (Build 12130.20272) but not in v2301 (Build 16026.20146). That may explain the second machine suddenly not working also?
It appears opening the "template" document which you are then adding information into opens read only as requested but now no longer allows changes to be made, which is where I think your code is skipping too? Our running code displays the Word document after filling in the form fields and there is no option to change the file mode to the top right of the screen to allow editing.
Screen shot of viewing / editing options from Word toolbar
I don't have an answer as to how to fix it as yet, I'm afraid, apart from making the template document open for write access and changing rights on the network to make the documents read only. We've not tested that as yet though. Hopefully it helps by giving you something else to check as the code has run successfully for a time and suddenly stopped.
I'm currently trying to find an option for opening a file as read only but allowing changes to the open document but am struggling to find anything like this in the Microsoft documentation. If I do find a solution I'll come back and post it.
It may work for you changing the following line from True to False at the end if the file is somewhere not shared.
Set doc = appWord.Documents.Open("C:\Users\1161\OneDrive - Anfas Medical Care\Master - Anfas Medical Care\Forms\Onboarding Documents\Access\Saudi\ContractSaudiAccess.docx", , True)

Automatisation of macros

I have a Word document and I want to do following with it:
Select some part of it when I open a Word doc (let´s say from page 40 to 45).
Reverse text in selected area.
Get text reversed again as it was before opening, when I close document.
I have this code, that reverses the text:
Sub ReverseSelectedWords()
Dim i As Integer
Dim oWords As Words
Dim oWord As Range
Set oWords = Selection.Range.Words
For i = 1 To oWords.Count Step 1
Set oWord = oWords(i)
Do While oWord.Characters.Last.Text = " "
Call oWord.MoveEnd(WdUnits.wdCharacter, -1)
Loop
Debug.Print "'" & oWord.Text & "'"
oWord.Text = StrReverse(oWord.Text)
Next i
End Sub
For what you've described as being your goal, it would make far more sense to apply a password for opening to the document and provide only the intended reader(s) with that password. No code required.

Word.GetAddress in Excel / The "Check Names" dialog displays in background

I am using the Word.GetAddress function in an Excel document to retrieve the first & last names of someone if he is in the GAL.
From what I have understand, the only way to have the built-in "Check Names" dialog is to use the Word.GetAddress function.
When the name entered matches more than entry, the "Check Names" displays but in the background. I have to Alt+Tab to get it.
I have tried to use the "Activate" function or the "WindowsState" property to bring it upfront but I am stuck ...
Function getFirstAndLastNames(pName As String) As String
Dim oWord As Word.Application
Dim strAddress As String
On Error GoTo getFirstAndLastNames_Error
'If the search doesn't work, returns the argument
getFirstAndLastNames = pName
'Create the Word object to use GetAddress
Set oWord = CreateObject("Word.Application")
'Search
strAddress = oWord.GetAddress(Name:=pName, CheckNamesDialog:=True, AddressProperties:="<PR_GIVEN_NAME> <PR_SURNAME>")
'If there is a result, the function returns it
If strAddress <> "" Then getFirstAndLastNames = strAddress
'Quit Word
oWord.Quit
Set oWord = Nothing
On Error GoTo 0
Exit Function
getFirstAndLastNames_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure getFirstAndLastNames of Module Test"
If Not (oWord Is Nothing) Then oWord.Quit
End Function
I have seen this post where there was a similar issue resolved but it doesn't say how ...
Thanks in advance for your help.
The edit to the other post does not state that they found a resolution for the box not coming to the front; only that it could be made visible using alt-tab to bring it to the front.
You have a deadlock in that your Excel code is stopped, waiting for Word and you need an action to have the Word (or rather Outlook) window brought to the front so the user can find it.
You could minimize and restore the Excel window but it is kludge and if there are other windows on screen then it'll be unreliable as the dialog you need will be hidden behind those too.
What you need to do is a bit ugly but will work. Which is to have a helper script or application which you can fire off asynchronously using Application.Run which will start the app and continue to execute in VBA. That script/app will wait for a little while (to give VBA time to run the GetAddress line) and then bring that dialog to the front using the windows API.
Most scripting or programming languages will be good enough and which one you choose depends on what you are most comfortable with. StackOverflow has an example for Powershell that you can adjust to your needs.
Finally, I found an article on the support of Microsoft.com that explain how to use CheckSpelling outside of Word. I adapted it to my use.
The code position the Word window off the screen but the dialogs appears in the foreground.
Function getFirstAndLastNames(pName As String) As String
Dim oWord As Word.Application
Dim strAddress As String
Dim lOrigTop As Long
Dim lOrigState As Byte
'Display the "Check names" dialog (available only with Word.Application ...)
On Error GoTo getFirstAndLastNames_Error
'If the search doesn't work, returns the argument
getFirstAndLastNames = pName
'Create the Word object to use GetAddress
Set oWord = CreateObject("Word.Application")
'Position Word off screen to avoid having document visible
'http://support.microsoft.com/kb/243844/en-us
lOrigTop = oWord.Top
lOrigState = oWord.WindowState
oWord.Top = -3000
oWord.Visible = True
oWord.WindowState = wdWindowStateMinimize
oWord.Activate
'Search
strAddress = oWord.GetAddress(Name:=pName, CheckNamesDialog:=True, AddressProperties:="<PR_GIVEN_NAME> <PR_SURNAME>")
'If there is a result, the function returns it
If strAddress <> "" Then getFirstAndLastNames = strAddress
'Reset the position and state of Word and quit the application
oWord.Visible = False
oWord.Top = lOrigTop
oWord.WindowState = lOrigState
oWord.Quit
Set oWord = Nothing
On Error GoTo 0
Exit Function
getFirstAndLastNames_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure getFirstAndLastNames of Module Test"
'If an error raised, Reset the position and state of Word and quit the application
If Not (oWord Is Nothing) Then
oWord.Top = lOrigTop
oWord.WindowState = lOrigState
oWord.Quit
End If
End Function

Using VBA to get extended file attributes

Trying to use Excel VBA to capture all the file attributes from files on disk, including extended attributes. Was able to get it to loop through the files and capture the basic attributes (that come from the file system):
File Path
File Name
File Size
Date Created
Date Last Accessed
Date Last Modified
File Type
Would also like to capture the extended properties that come from the file itself:
Author
Keywords
Comments
Last Author
Category
Subject
And other properties which are visible when right clicking on the file.
The goal is to create a detailed list of all the files on a file server.
You say loop .. so if you want to do this for a dir instead of the current document;
Dim sFile As Variant
Dim oShell: Set oShell = CreateObject("Shell.Application")
Dim oDir: Set oDir = oShell.Namespace("c:\foo")
For Each sFile In oDir.Items
Debug.Print oDir.GetDetailsOf(sFile, XXX)
Next
Where XXX is an attribute column index, 9 for Author for example.
To list available indexes for your reference you can replace the for loop with;
for i = 0 To 40
debug.? i, oDir.GetDetailsOf(oDir.Items, i)
Next
Quickly for a single file/attribute:
Const PROP_COMPUTER As Long = 56
With CreateObject("Shell.Application").Namespace("C:\HOSTDIRECTORY")
MsgBox .GetDetailsOf(.Items.Item("FILE.NAME"), PROP_COMPUTER)
End With
You can get this with .BuiltInDocmementProperties.
For example:
Public Sub PrintDocumentProperties()
Dim oApp As New Excel.Application
Dim oWB As Workbook
Set oWB = ActiveWorkbook
Dim title As String
title = oWB.BuiltinDocumentProperties("Title")
Dim lastauthor As String
lastauthor = oWB.BuiltinDocumentProperties("Last Author")
Debug.Print title
Debug.Print lastauthor
End Sub
See this page for all the fields you can access with this: http://msdn.microsoft.com/en-us/library/bb220896.aspx
If you're trying to do this outside of the client (i.e. with Excel closed and running code from, say, a .NET program), you need to use DSOFile.dll.
'vb.net
'Extended file stributes
'visual basic .net sample
Dim sFile As Object
Dim oShell = CreateObject("Shell.Application")
Dim oDir = oShell.Namespace("c:\temp")
For i = 0 To 34
TextBox1.Text = TextBox1.Text & oDir.GetDetailsOf(oDir, i) & vbCrLf
For Each sFile In oDir.Items
TextBox1.Text = TextBox1.Text & oDir.GetDetailsOf(sFile, i) & vbCrLf
Next
TextBox1.Text = TextBox1.Text & vbCrLf
Next
I was finally able to get this to work for my needs.
The old voted up code does not run on windows 10 system (at least not mine). The referenced MS library link below provides current examples on how to make this work. My example uses them with late bindings.
https://learn.microsoft.com/en-us/windows/win32/shell/folder-getdetailsof.
The attribute codes were different on my computer and like someone mentioned above most return blank values even if they are not. I used a for loop to cycle through all of them and found out that Title and Subject can still be accessed which is more then enough for my purposes.
Private Sub MySubNamek()
Dim objShell As Object 'Shell
Dim objFolder As Object 'Folder
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.NameSpace("E:\MyFolder")
If (Not objFolder Is Nothing) Then
Dim objFolderItem As Object 'FolderItem
Set objFolderItem = objFolder.ParseName("Myfilename.txt")
For i = 0 To 288
szItem = objFolder.GetDetailsOf(objFolderItem, i)
Debug.Print i & " - " & szItem
Next
Set objFolderItem = Nothing
End If
Set objFolder = Nothing
Set objShell = Nothing
End Sub
Lucky discovery
if objFolderItem is Nothing when you call
objFolder.GetDetailsOf(objFolderItem, i)
the string returned is the name of the property, rather than its (undefined) value
e.g. when i=3 it returns "Date modified"
Doing it for all 288 values of I makes it clear why most cause it to return blank for most filetypes
e.g i=175 is "Horizontal resolution"

Software Requirements Reviews with MS Word - How do I get metrics in an automated way?

Let's say I have a requirements document in MS Word, and someone else reviews it an provides a list of issues found using the "Track Changes" feature.
Is there a way to extract "how many major/minor issues were found during the review?" using an automated script - for metrics purposes?
I see CodeCollaborator has some MS Word integration, but it doesn't seem to know how to look inside Word to extract the tracked changes data. It just launches the document.
I did once write a Word macro that extracts the comments to a separate document, you are welcome to try adapting this to your purposes, if you have trouble then reply here and I can give you a hand with making changes.
Public Sub PROCESS_COMMENTS()
Dim strReplaceText As String
Dim myPar As Paragraph
Dim strCurrentColumn As String
Dim i As Integer
Dim Com As Comment
Application.ScreenUpdating = False
' set the input and output docs.
Set inDoc = ActiveDocument
' check we have comments to process in the original document
If inDoc.Comments.Count < 1 Then
MsgBox "No comments in the document"
Exit Sub
End If
' comments exist so create new document
Set outDoc = Documents.Add
Set outRange = outDoc.Content
outDoc.Range.InsertAfter "List of Comments:"
outDoc.Paragraphs(outDoc.Paragraphs.Count).Style = outDoc.Styles("Heading 1")
outDoc.Range.InsertParagraphAfter
' cycle through comments, inserting them in the new document
' display the new document and refresh
outDoc.Activate
Application.ScreenRefresh
For Each Com In inDoc.Comments
outRange.InsertAfter "[" & Com.Author & " - " & Com.Initial & Com.Index & "] "
outDoc.Paragraphs(outDoc.Paragraphs.Count).Range.Font.Bold = True
outDoc.Range.InsertParagraphAfter
outRange.InsertAfter Com.Range.Text
outDoc.Paragraphs(outDoc.Paragraphs.Count).Range.Font.Bold = False
Set myRange = Com.Scope
outDoc.Range.InsertParagraphAfter
outDoc.Range.InsertParagraphAfter
Next
Application.ScreenUpdating = True
Set outDoc = ActiveDocument
End Sub