A blog post or two ago I introduced the usage of <w:customXml> tags to mark up your documents with business semantics. A second blog post followed up on that displaying the code for mapping an XML data file to your schema-mapped document. In this third and last post on the subject I plan to cover repeating elements which are mapped to a table. This is slightly more complex than mapping simple values since we need to create new rows in the table to accommodate all the data that is provided in the XML data file.
The last blog post left off with a simple method to take a piece of XML data, identified using an XmlNode and an X-Path query, and map that to a <w:customXml> tag, also identified with an XmlNode container and an X-Path query:
static void UpdateDocumentValue(
string dataSelector, // xpath query into data node
string customXmlSelector, // xpath query to select customXml tag
XmlNode dataPart, // the relevant part of the data to query against
XmlNode documentPart) // the part of the document containing the data
{
...
}
In this second code walkthrough a similar method will be created for updating a table as well. One of the cool things is that we can re-use much of the code for the UpdateDocumentValue method, which updates single values. This is because conceptually a row of data consists of single values again, so with the proper factoring the existing code should work great for this situation as well.
The new method that you will call to update the document data is, very creatively, called UpdateDocumentTable (yes, long and hard thought went in to this naming schema). Take for instance the following blob of XML data, in which the <orderItem> is the repeating element that maps to your table:
<invoice xmlns="http://contoso.com/invoice">
<orderItems>
<orderItem />
<orderItem />
...
</orderItems>
</invoice>
To take this data and update your document, you would use a method call similar to:
UpdateDocumentTable(
"/iv:invoice/iv:orderItems",
BuildCustomXMLSelectionQuery(
XMLNS_IV,
new string[]
{
"invoice",
"orderItems"
}),
data,
document);
Note that this is largely similar to the UpdateDocumentValue method. The first parameter is the X-Path into the XML data file, relative to the node being passed in as the third parameter. The second parameter is an X-Path selecting the right <w:customXml> tag. Note that this X-Path doesn't select the repeating element itself, but the container element <orderItems>. The last parameter is the XmlNode representing the body of the WordprocessingML document.
The UpdateDocumentTable method
So what are the tasks that we need to perform in the UpdateDocumentTable method? First of all, take a look at a typical document with a mapped table. One important thing to notice is that the table which will contain the order items already has the first row for holding data available.
Assuming that there will also be at least one row of data, the logic that we need to implement is not that complex:
- Update data in the first row.
-
For 1 to data count
- Create new row
- Update data in new row
The first 'complexity' is getting the first data row of the table, so we can update that and use it as a template for the other rows. With a little knowledge of Open XML found in the spec it is not that hard. You first get the <w:customXml> tag which surrounds the entire table using the parameters supplied to the UpdateDocumentTable method. Next you select the <w:customXml> tag which surrounds the row. A row is defined using the <w:tr> tag, so a little X-Path and you're there. Since usually the first row of a table contains the columns headers, this will select the second row of the table. Note that this is obviously not generic enough to work for all documents that you can think of, but I think most of you will do it this way, so it's good enough for me J
XmlNode customXmlTableNode = document.SelectSingleNode(documentSelector, mgr);
XmlNode customXmlRowNode = customXmlTableNode.SelectSingleNode(
"w:tbl/w:customXml[w:tr]", mgr);
Now that we have that first row of the table, we can update it with data. First a query is performed into the XML data file, retrieving the container of the <orderItem> elements. Next the first <orderItem> is mapped to the row we have retrieved earlier.
XmlNode dataCollectionNode =
data.SelectSingleNode(dataXPath, mgr);
UpdateDocumentTableRow(
customXmlRowNode,
dataCollectionNode.ChildNodes[0]);
The last part of the UpdateDocumentTable method loops through the rest of the <orderItem> elements in the XML data file and creates a new row for it by cloning the previous row. Like I explained in the first blog post, schema-mapping is a hierarchical model. By just copying the previous row, the hierarchy is maintained correctly.
for (int i = 1; i < dataCollectionNode.ChildNodes.Count; i++)
{
// clone the Open XML table row
customXmlRowNode = AddTableRow( customXmlTableNode, customXmlRowNode);
// Update the new mapped row
UpdateDocumentTableRow(
customXmlRowNode,
dataCollectionNode.ChildNodes[i]);
}
So a few interesting things yet to be explained are how rows are copied (pretty simple), and how the update of the data in a row is performed.
Creating new table rows
First let's cover the addition of new rows to the table and how this affects table formatting.The copying itself is pretty simple. The <w:customXml> tag for the table is already available in the UpdateDocumentTable method, the <w:customXml> tag for the row is also known. First a deep clone is created from the row. This ensures that all the tags for the mapped cells are copied as well. Next this node is added at the end of the <w:tbl> node, which is the only child of the <w:customXml> tag for the table.
static XmlNode AddTableRow(
XmlNode customXmlTableNode,
XmlNode customXmlTableRowNode)
{
customXmlTableRowNode = customXmlTableRowNode.CloneNode(true);
customXmlTableNode["tbl", XMLNS_W].AppendChild(customXmlTableRowNode);
return customXmlTableRowNode;
}
An important note about doing it this way would be table formatting. By now most of us have found out that making the background shading for rows different for odd and even rows improves readability of the table. Doesn't cloning the row break this formatting?
|
Header Row |
|
Even row |
|
Odd row |
|
Even row |
It turns out that it doesn't! This type of formatting is dealt with in a generic way outside of the main table content, using table styles. This means that after you update the table with the XML data, it will look something like
Updating the values in the row
Now we get to the fun of reuse. When you take a look at updating the values in a row, it is remarkably similar to updating a single value.
For the update of a single value we needed to know the X-Path query to select a part of the document, and the part to select from was passed in to the method. For the updating of a single value explained in the previous post on this subject, the XmlNode passed in was the root node of the document body. For the row updates we can pass in the row instead. The X-Path query to select the right <w:customXml> node for the cell we need to update is easily found as well. It is just the name of each child element of the <orderItem> tag. Here is a sample of how the data for an <orderItem> looks. The <orderItem> is passed in to the UpdateDocumentTableRow method, together with the <w:customXml> tag representing the entire row in the document.
<orderItem>
<productID>1</productID>
<productName>Black Pencils</productName>
<quantity>3</quantity>
<unitPrice>2.95</unitPrice>
</orderItem>
The UpdateDocumentTableRow method begins with selecting each child tag of the <orderItem>. The name of the tag can then be used for two things. Selecting the right <w:customXml> tag in the document, and selecting the node in the XML data file. It`s then just a simple call to the method we created in the previous blog post.
foreach (XmlNode dataNode in dataContainerNode.ChildNodes)
{
// create an xpath query to select the w;customXml node
// for a single cell
// "descendant::w:customXml[w:uri='myns' and w:element='productId']"
string customXmlSelector =
BuildCustomXMLSelectionQuery(
dataNode.NamespaceURI,
new string[] { dataNode.LocalName });
// Create the xpath query to select the data item
// "myNs:productId"
string dataSelector = String.Format(
"{0}:{1}",
mgr.LookupPrefix(dataContainerNode.NamespaceURI),
dataNode.LocalName);
UpdateDocumentValue(
dataSelector,
customXmlSelector,
dataContainerNode,
customXmlRowNode);
}
One interesting note is that although the node in the XML data file with a piece of data for the cell is already available in the dataNode variable. However, the UpdateDocumentValue method expects to receive the container of that data element, and not that data element itself. So instead the right X-Path is calculated and the container is passed in for the update of the cell.
Now the last part that needs to be updated for it all to work is the UpdateDocumentValue method. Remember that last time I discussed how you must ensure that the right <p>-<r>-<t> structure is maintained? Same goes for the table cell updates. Since my approach throws away a part of the document to make it easier to maintain the hierarchy (I throw away the child elements of the <w:customXml> tag so you only need to look at your parents), you also need to recreate the <w:tc> table cell elements since they are thrown away. This is because the <w:customXml> tag for a table cell is the parent of the table cell itself, and all the children of the <w:customXml> tag are dropped (impacting formatting if you are not using table styles).
So to make it all work, the last part of the UpdateDocumentValue is updated. The gray part was already there, the last part is new.
if (wmlParentContainer.LocalName == "body")
{
XmlNode paragraphNode = documentPart.OwnerDocument.CreateElement(
"w", "p", XMLNS_W);
paragraphNode.AppendChild(runNode);
customXmlNode.AppendChild(paragraphNode);
}
// If there is already a paragraph, let's not add an extra one
else if(wmlParentContainer.LocalName == "p")
{
customXmlNode.AppendChild(runNode);
}
else if (wmlParentContainer.LocalName == "tr")
{
XmlNode cellNode =
documentPart.OwnerDocument.CreateElement(
"w", "tc", XMLNS_W);
XmlNode paragraphNode =
documentPart.OwnerDocument.CreateElement(
"w", "p", XMLNS_W);
cellNode.AppendChild(paragraphNode);
paragraphNode.AppendChild(runNode);
customXmlNode.AppendChild(cellNode);
}
That's it for the last part on updating a schema-mapped document with XML data. I hope it will help you build that new innovative solution that changes the world J
The code shown is in my DevDays 2008 download.