wordpress hit counter
WordProcessingML Snippets: Part 4, table snippets - OpenXML Developer - Blog - OpenXML Developer
Goodbye and Hello

OpenXmlDeveloper.org is Shutting Down

There is a time for all good things to come to an end, and the time has come to shut down OpenXmlDeveloper.org.

Screen-casts and blog posts: Content on OpenXmlDeveloper.org will be moving to EricWhite.com.

Forums: We are moving the forums to EricWhite.com and StackOverflow.com. Please do not post in the forums on OpenXmlDeveloper.org. Instead, please post in the forums at EricWhite.com or at StackOverflow.com.

Please see this blog post for more information about my plans moving forward.  Cheers, Eric

WordProcessingML Snippets: Part 4, table snippets

WordProcessingML Snippets: Part 4, table snippets

  • Comments 8

This article is part of the "Building documents with code snippets" series.
After talking about how basic tables are represented in WordProcessingML, it is time to handle the corresponding snippets. There are various snippets which I’ll handle in corresponding pairs. Because of the xml representation of merged cells I’ve mentioned in the previous article, the code gets a bit more complex. Especially breaking down tables by removing rows or columns is more difficult, because you need to take care of these merged cells. The code sections described in this article make use of various utility snippets. I chose to create these utility snippets to have a bit of code re-use, which is easily achievable because of the structure of Open XML.   

Handling rows

There are two snippets which you can use to either add or remove rows from a table. Adding a new row is the easiest, because the new row will not contain any merged cells by default. The code is provided by the AddRow snippet. This snippet needs to create a new w:tr node to represent the entire row and next add the correct number of w:tc nodes to represent each table cell. The number of columns defined in the table can be retrieved from the w:tblGrid node, which is available using a bit of x-path. The rest is really basic code which handles the creation of the correct xml structure. The AddCell snippet is used to add new cells to the row. It will later also be used by other table related snippets.

        static void AddRow(XmlNode tableNode, XmlNamespaceManager namespaces)
            string wordNamespace = namespaces.LookupNamespace("w");
            int cellCount = tableNode.SelectNodes("w:tblGrid/w:gridCol", namespaces).Count;
            XmlNode rowNode = tableNode.OwnerDocument.CreateElement(
                "w", "tr", wordNamespace);
            for (int i = 0; i < cellCount; i++)
                string defautlCellWidthXpath = String.Format("w:tblGrid/w:gridCol[{0}]", i + 1);
                int defaultCellWidth = Int32.Parse(tableNode.SelectSingleNode(
                    defautlCellWidthXpath,  namespaces).Attributes["w:w"].Value);
                AddCell(rowNode, namespaces, defaultCellWidth, null);

Removal of rows is a bit harder.  When adding rows using the above code it is known for a fact that there will be no merged cells, since we programmed it that way. For removal of rows on the other hand, this information is not that certain. A row might contain multiple sets of horizontally and vertically merged cells, which we will need to work with when coding.
Horizontally merged cells pose no real problem. The entire merge structure is represented inside the w:tr node, so removing the entire row also removes the horizontally merged cells automatically. Vertically merged cells span multiple rows, and that is where will need to put in some work. If any cell inside the row which you are trying to remove is the top cell of a vertically merged set of cells, that set of cells needs to be updated. This is because the top cell uses a different value for the w:vmerge node compared to the other cells in a vertical merge. Since the top cell is will be removed, the next cell in the vertical merge needs to become the top cell, which is defined by the ‘restart’ value for the w:val node.

First let's get to the node representing the entire row.

    static void RemoveRow(XmlNode tableNode, XmlNamespaceManager namespaces, int rowIndex)
        string wordNamespace = namespaces.LookupNamespace("w");
        string xpath = String.Format("w:tr[{0}]", rowIndex);
        XmlNode rowNode = tableNode.SelectSingleNode(xpath, namespaces);

Next  run through each cell in the row. Because these cells can be merged horizontally as well,  it is a bit harder to find the cell node corresponding to a specific column. You can’t just use the ‘columnIndex’ variable (remember the missing w:tc nodes when merging cells horizontally? ). A utility snippet called RetrieveCellByColumnIndex is used to retrieve the right xml node by taking horizontally merged cells into the equation. 

        int columnCount = tableNode.SelectNodes("w:tblGrid/w:gridCol", namespaces).Count;
        for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
            XmlNode cellNode = RetrieveCellByColumnIndex(
                rowNode, namespaces, columnIndex);

Now that we have a reference to the node representing the cell, we can find out if it is merged vertically, and if so, if it is the top cell in the merge. When that happens, code needs to be written to update the merged column.

            XmlNode vmergeValAttribute = cellNode.SelectSingleNode(
                "w:tcPr/w:vmerge/@w:val", namespaces);
            if (vmergeValAttribute != null &&
                vmergeValAttribute.Value == "restart")

If the current cell is the top cell of a vertical merge, the next row might need to be updated. There are various exceptional situations you need to address. First of all there might not even be a next row, and the next row might not even take part in the vertical merge, or it may start a new separate vertical merge.

                xpath = String.Format("w:tr[{0}]", rowIndex + 1);
                XmlNode nextRowNode = tableNode.SelectSingleNode(xpath, namespaces);
                if (nextRowNode != null)
                    XmlNode nextCellNode = RetrieveCellByColumnIndex(
                        nextRowNode, namespaces, columnIndex);
                    XmlNode vmergeNode = nextCellNode.SelectSingleNode(
                        "w:tcPr/w:vmerge", namespaces);
                    if (vmergeNode != null)

Now that we know for a fact that there is a next row, we can update it. This is only necessary when the w:val attribute is not present or has the ‘continue’ value. If so, the cell needs to be decorated with the 'restart' value.

                        vmergeValAttribute = vmergeNode.Attributes["w:val"];
                        if (vmergeValAttribute == null)
                            vmergeValAttribute = vmergeNode.OwnerDocument.CreateAttribute(
                                "w", "val", wordNamespace);
                            vmergeValAttribute.Value = "restart";
                        else if (vmergeValAttribute.Value == "continue")
                            vmergeValAttribute.Value = "restart";

This handles a single cell in the row which will be removed. The rest of the cells inside the row will also need to be updated. It is possible that there are more merged columns inside the row. Because the cell we just handled might also be merged horizontally, we can skip columns if that is the case.

            XmlNode gridSpanNode = cellNode.SelectSingleNode(
                "w:tcPr/w:gridSpan", namespaces);
            if (gridSpanNode != null)
                XmlAttribute gridSpanValAttribute = gridSpanNode.Attributes["w:val"];
                int gridSpan = Int32.Parse(gridSpanValAttribute.Value);
                columnIndex += gridSpan;

Just one finishing touch left, the removal of the w:tr row.


That is it for removing a row from a merged or unmerged table.

Handling columns

Just like with rows, there are two snippets available for working with columns. The names you can probably guess yourself, the complexity also. I will not show the AddColumn snippet as it is pretty basic. It just updates the w:tblGrid node to define a new column, and runs through each row to add a new cell at the end.

The RemoveColumn snippet however is more interesting. This time it is not the merged columns which need to be handled correctly, but obviously horizontally merging cells needs to be thought out because these can span across the column which will be removed. The code is a bit easier than RemoveRow, mainly because the difficulty of retrieving the right w:tc node for a column has been moved to the utility snippets.

First we remove a w:gridCol node from the w:tblGrid grid definition.

    static void RemoveColumn(XmlNode tableNode, XmlNamespaceManager namespaces,
        int columnIndex)
        string wordNamespace = namespaces.LookupNamespace("w");
        XmlNode gridDefinitionNode = tableNode.SelectSingleNode("w:tblGrid", namespaces);
        string xpath = String.Format("w:gridCol[{0}]", columnIndex);
        XmlNode gridColumnDefinitionNode = gridDefinitionNode.SelectSingleNode(xpath, namespaces);

Next we need to remove w:tc nodes from the table body. Because each w:tc node might be horizontally merged, we need to write a bit more code. First we need to find the w:tc node which is part of the column, taking merged cells into account. After the cell has been found either the merge count needs to be lowered, or the cell actually removed.

        foreach (XmlNode rowNode in tableNode.SelectNodes("w:tr", namespaces))
            XmlNode cellNode = RetrieveCellByColumnIndex(rowNode, namespaces, columnIndex);
            XmlNode gridSpanNode = cellNode.SelectSingleNode( "w:tcPr/w:gridSpan", namespaces);
            if (gridSpanNode == null)
                int spanCount = Int32.Parse(
                gridSpanNode.Attributes["w:val"].Value = spanCount.ToString();

Handling tables

I’ll finish with the AddTable snippet. It has a corresponding RemoveTable snippet which is really simple so I won’t go into the details right now. The AddTable snippet is fun, because it makes use of the other snippets described in this article.  Here is the code.

        static void AddTable(XmlNode parentNode, XmlNamespaceManager namespaces,
            int columnCount, int rowCount, int cellWidth)
            string wordNamespace = namespaces.LookupNamespace("w");
            XmlNode tableNode = parentNode.OwnerDocument.CreateElement(
                "w", "tbl", wordNamespace);
            XmlNode tablePropertiesNode = parentNode.OwnerDocument.CreateElement(
                "w", "tblPr", wordNamespace);
            XmlNode tableBordersNode = parentNode.OwnerDocument.CreateElement(
                "w", "tblBorders", wordNamespace);
            string[] sideNames = { "top", "left", "bottom", "right", "insideH", "insideV"};
            for (int i = 0; i < sideNames.Length; i++)
                XmlNode tableBorderNode = parentNode.OwnerDocument.CreateElement(
                    "w", sideNames[i], wordNamespace);
                XmlAttribute tableBorderValAttribute = parentNode.OwnerDocument.CreateAttribute(
                    "w", "val", wordNamespace);
                XmlAttribute tableBorderSzAttribute = parentNode.OwnerDocument.CreateAttribute(
                    "w", "sz", wordNamespace);
                tableBorderSzAttribute.Value = "1";
                tableBorderValAttribute.Value = "single";
            XmlNode gridDefinitionNode = tableNode.OwnerDocument.CreateElement(
                "w", "tblGrid", wordNamespace);
            for (int columnIndex = 0; columnIndex <= columnCount; columnIndex++)
                AddColumn(tableNode, namespaces, cellWidth);
            for (int rowIndex = 0; rowIndex < rowCount; rowIndex++)
                AddRow(tableNode, namespaces);

That is it for tables right now. The code is not 100% complete. When removing rows or columns the border settings will need updating as well. I will leave this as an exercise for you.

The next article will go into the details of the AddImage snippet, which you can use to store an image in the Package and display it in the document. It comes with an extra utility snippet which shows how to store a file in the package with a unique filename. I will probably finish that one in the next few days.

Page 1 of 1 (8 items)