Previously we've looked at using Word Interop for creating dynamic documents, ranging from the simple, to the more in-depth, methods of working with Word from C#. Check out Part 1, Part 2, and Part 3 to learn about working with bookmarks, ranges, images, and other tidbits. Barring any more specific requests for information this will be the final installment, so I can get this stuff out of my brain and forget it ever happened.
Tables are a great way to add both visual structure and navigability to your documents. Creating a table structure in your templates can make you less reliant on bookmarks when getting around the document in code.
Let's start by adding another method to our WordDocument utility class for creating a table on the fly:
protected Table CreateTable(Range tableRange, Range insertRange, int rows, int columns,
float defaultFontSize)
{
Table newTable = tableRange.Tables.Add(insertRange, rows, columns,
ref oMissing, ref oMissing);
newTable.Range.Font.Size = defaultFontSize;
return newTable;
}
So here we feed two ranges to this method, set the number or rows and columns, and set font size. You will find that most of the time you use the same Range object for tableRange and insertRange. This puts a table inline with your current spot in the document, which seems to be (for me) the most standard use. However, should you need to insert a table into a specific spot in a larger range (like maybe even the range of the document itself) the option is open for you.
Now let's discuss table structure in Word. It's strange. Tables are numbered in a 1-based array (*sigh*) from top to bottom on the Word Document. Tables that occupy the same vertical position appear to take the index number in the order they were created. So if you have two tables side by side, and the one on the right was created first, that table will probably take the lower index number. I say probably because I'm not entirely sure, and have nothing but anecdotal evidence from trial-and-error to back me up. Tables that are nested within tables do not show up in the Tables[] collection of the document. They show up in the Tables[] collection for the container table.
This is a good time to point out that all of the primary Word objects (tables, ranges, etc) are accessible at a practically infinite level throughout the object hierarchy. A range can have a table that has a table that has 50 ranges that each have ranges that have tables that....point is...try to keep your structure simple before you decide to jump from a building.
Ok so to put this in perspective, let's say you have a document structured with a table that will represent a 2 column newsprint type view:
For the sake of the example we will assume that this is the only table in the document. The top row is simply 2 cells merged to make a "header" row, and then you have the two column structure.
Let's fill in the top row here with some header text.
Table contentTable = wordDocument.Tables[1];
Range insertRange = contentTable.Cell(1, 1).Range;
insertRange.Text = "Word Tables Made Easy";
insertRange.Font.Bold = 1;
insertRange.Font.Size = 14;
//now fill in some text on the first column
insertRange = contentTable.Cell(2, 1).Range;
insertRange.Text = "Lorem Ipsum Blah Blah blahblah";
The Cell object is what you really want to interact with. It represents a (Row, Column) cell in the table, and the default Range object starts at the insert point that exists if you were to open the document and place your cursor in the cell and start typing. After the above code, your table will look like:
Easy enough, right? Ok now let's say you want to add another table to the right column that will contain some inset images or something. This is easy enough to do.
Range insertRange = wordDocument.Tables[1].Cell(2,2).Range;
CreateTable(insertRange, insertRange, 1, 1, 10);
Table insertedTable = wordDocument.Tables[1].Cell(2,2).Tables[1];
//the table we put in is now accessible from the Tables collection of the second row, second column cell.
If you want to dynamically add a row, it's also a trivial matter. Just get your table and add a row. It will by default take the properties of the row before it:
WordDocument.Tables[1].Cell(2,2).Tables[1].Rows.Add(ref oMissing);
This adds a row to our new table. We could put this code in a loop to add a collection of images, for example.
Finally, let's talk about removing structure from tables. This is easy on the face, but I will give a warning: Always delete objects from the bottom of the document first, moving up the structure. This is because you don't want to have to keep track of the changing indexes if you delete tables in the middle. You'll need to do some advanced planning when you want to do this, but it will save you a lot of heartache later.
Finding the bottom object (table, row, etc) is an easy matter, and pretty intuitive:
//Find the last table on the document Table lastTable = wordDocument.Tables[wordDocument.Tables.Count]; //Find the last row in a table Row lastRow = Table.Rows[Table.Rows.Count]; //Finally, call Delete() on the object; lastTable.Delete(); lastRow.Delete();
And there you have it. This method will allow you to fully table out a template, but then removed unused objects dynamically. As an example, I have a report that has a box near the bottom for special comments entered. This box is a table that is the second to last table on the document. As I am filling out the data, I test that the special comments are not present, and therefore I just want to dump the table.
if(specialComments == null || specialComments.Trim().Length == 0)
{
wordDocuments.Tables[wordDocument.Tables.Count -1].Delete();
}
And there you have it. Working with tables is one of the more simple and straightforward ways to navigate and structure a document for dynamic content via Interop.
A few random tips/tricks before we go.
Here's a few other things I have come across to make doing Word Interop less painful.
- Create a simple winforms test project that runs your code and will allow you to easily try a bunch of things when creating your documents. This will save you a lot of trouble and make "debugging" your document that much easier.
- Don't try too hard to refactor your Word Interop code. The code is going to be ugly as it is, but sometimes writing things out longhand will keep you better aware of your position on the document and keep you from making mistakes. This is the only time you will hear me arguing in favor of verbosity and duplicate code where it makes sense. I got a whole bunch of code down for a report template, then went and gutted it and moved things out into more reusable structures, and actually found it more difficult to go back and make additions or changes. Maybe I'm just bad.
- Distribute your templates as resources in your assembly. The last thing you need is for someone to go and fuck with your Word templates because they see them on disk and think it would be a good idea to change some formatting or something to fit their needs. This will cause your app to explode. The quick and dirty way to do this is to add a resource to the project, add the word document, leave the default properties, and then load it like so:
filepath = "c:\\sometemppath\\sometempname.doc";
System.IO.File.WriteAllBytes(filePath, Properties.Resources.ReportTemplate);
reportPath = filePath;
//If you are using the WordDocument class just
LoadDocument();
I really hope this series is helpful for someone. A lot of this information can be real difficult to find, or when you can find it, the descriptions as to the how and why are not so useful. Also, you come across a lot of pitfalls doing Word Interop that will be very frustrating, and without a guide, you are stuck with a lot of trial and error. So hopefully my pain turns out to be your gain.
Tags:
Word Interop Code