Hey all. I’ll try to make this brief and simple. 🙂
I have
- 40 or so boilerplate word documents with a series of fields (Name, address, etc) that need to be filled in. This is historically done manually, but it’s repetitive and cumbersome.
- A workbook where a user has filled a huge set of information about an individual.
I need
- A way to programatically (from Excel VBA) open up these boilerplate documents, edit in the value of fields from various named ranges in the workbook, and save the filled in templates to a local folder.
If I were using VBA to programatically edit particular values in a set of spreadsheets, I would edit all those spreadsheets to contain a set of named ranges which could be used during the auto-fill process, but I’m not aware of any ‘named field’ feature in a Word document.
How could I edit the documents, and create a VBA routine, so that I can open each document, look for a set of fields which might need to be filled in, and substitute a value?
For instance, something that works like:
for each document in set_of_templates
if document.FieldExists("Name") then document.Field("Name").value = strName
if document.FieldExists("Address") then document.Field("Name").value = strAddress
...
document.saveAs( thisWorkbook.Path & "\GeneratedDocs\ " & document.Name )
next document
Things I’ve considered:
- Mail merge – but this is insufficient because it requires opening each document manually and structuring the workbook as a data source, I kind of want the opposite. The templates are the data source and the workbook is iterating through them. Also, mail merge is for creating many identical documents using a table of different data. I have many documents all using the same data.
- Using placeholder text such as “#NAME#” and opening each document for a search and replace. This is the solution I would resort to if nothing more elegant is proposed.
It’s been a long time since I asked this question, and my solution has undergone more and more refinement. I’ve had to deal with all sorts of special cases, such as values that come directly from the workbook, sections that need to be specially generated based on lists, and the need to do replacements in headers and footers.
As it turns out, it did not suffice to use bookmarks, as it was possible for users to later edit documents to change, add, and remove placeholder values from the documents. The solution was in fact to use keywords such as this:
This is just a page from a sample document which uses some of the possible values that can get automatically inserted into a document. Over 50 documents exist with completely different structures and layouts, and using different parameters. The only common knowledge shared by the word documents and the excel spreadsheet is a knowledge of what these placeholder values are meant to represent. In excel, this is stored in a list of document generation keywords, which contain the keyword, followed by a reference to the range that actually contains this value:
These were the key two ingredients required. Now with some clever code, all I had to do was iterate over each document to be generated, and then iterate over the range of all known keywords, and do a search and replace for each keyword in each document.
First, I have the wrapper method, which takes care of maintaining an instance of microsoft word iterating over all documents selected for generation, numbering the documents, and doing the user interface stuff (like handling errors, displaying the folder to the user, etc.)
That routine calls
RunReplacementswhich takes care of opening the document, prepping the environment for a fast replacement, updating links once done, handling errors, etc:That routine then invokes
RunSimpleReplacements. andRunAdvancedReplacements. In the former, we iterate over the set of Document Generation Keywords and callWordDocReplaceif the document contains our keyword. Note that it’s much faster to try andFinda bunch of words to figure out that they don’t exist, then to call replace indiscriminately, so we always check if a keyword exists before attempting to replace it.This is the function used to detect whether a keyword exists in the document:
And this is where the rubber meets the road – the code that executes the replacement. This routine got more complicated as I encountered difficulties. Here are the lessons you will only learn from experience:
You can set the replacement text directly, or you can use the clipboard. I found out the hard way that if you are doing a VBA replace in word using a string longer than 255 characters, the text will get truncated if you try to place it in the
Find.Replacement.Text, but you can use"^c"as your replacement text, and it will get it directly from the clipboard. This was the workaround I got to use.Simply calling replace will miss keywords in some text areas like headers and footers. Because of this, you actually need to iterate over the
document.StoryRangesand run the search and replace on each one to ensure that you catch all instances of the word you want to replace.If you’re setting the
Replacement.Textdirectly, you need to convert Excel line breaks (vbNewLineandChr(10)) with a simplevbCrfor them to appear properly in word. Otherwise, anywhere your replacement text has line breaks coming from an excel cell will end up inserting strange symbols into word. If you use the clipboard method however, you do not need to do this, as the line breaks get converted automatically when put in the clipboard.That explains everything. Comments should be pretty clear too. Here’s the golden routine that executes the magic:
When the dust settles, we’re left with a beautiful version of the initial document with production values in place of those hash marked keywords. I’d love to show an example, but of course every filled in document contain all-proprietary information.
The only think left to mention I guess would be that
RunAdvancedReplacementssection. It does something extremely similar – it ends up calling the sameWordDocReplacefunction, but what’s special about the keywords used here is that they don’t link to a single cell in the original workbook, they get generated in the code-behind from lists in the workbook. So for instance, one of the advanced replacements would look like this:And then there will be a corresponding routine which puts together a string containing all the vessel information as configured by the user:
the resulting string can be used just like the contents of any excel cell, and passed to the replacement function, which will appropriately use the clipboard method if it exceeds 255 characters.
So this template:
Plus this spreadsheet data:
Becomes this document:
I sincerely hope that this helps someone out some day. It was definitely a huge undertaking and a complex wheel to have to re-invent. The application is huge, with over 50,000 lines of VBA code, so if I’ve referenced a crucial method in my code somewhere that someone needs, please leave a comment and I’ll add it in here.