In my last blog post "Introduction to data and schema-mapped documents" I introduced the way <w:customXml> tags can be used to overlay business semantics on a document and what benefits this model provides compared to using data-bound content-controls (mainly the contextual hierarchy of tags and a way to identify tables). In this second blog post on the subject I will start building the foundations for a data-embedding tool which can act quite generically for embedding XML-based business data into a schema-mapped document. As you might expect the data should follow the mapped schema. I will split the details on how to embed the data in two. This blog post will cover how to embed a simple value and my next one will cover mapped tables, which can reuse some of the code for the simple value updates (since a table is made up of rows with simple values).
To map data to a document you can go two main routes. Either the document mapping is leading, or the data. You either start parsing the document to find <w:customXml> tags and then find the data for each tag, or you start parsing the data, and try to find a <w:customXml> tag for that data. For instance, if you have some repeating data which is mapped to a non-repeating construct like a normal paragraph, what do you do? When the document is leading you would not notice it in your code since you see a non-repeating element, and the data will contain that row (and probably more than one, but who cares?). When the data is leading, you will see the multiple rows of data in the XML and also see that this data is not mapped to a repeating element in the document, in which case this would cause an error (where do you leave the rest of the data rows?)
In my sample, I made the business data leading, so the code will start parsing the data file, lookup the right <w:customXml> tag, or when the data is a repeating element, create the new <w:customXml> tags to accommodate the data. Taking this route does pose one slight difficulty. When the data is put inside the document, you need to ensure that you follow the paragraph-run-text hierarchy of a WordprocessingML document. E.g. you need to find out where the <w:customXml> tag which will store the data is placed inside the document. If it is inside a paragraph, you create a run, if it is within the body of the document, you create a paragraph and a run, and so on.
How to use the code
So, let's begin with the end in mind. Because the model is not entirely generic yet, there is one main method to drive the embedding of data called UpdateDocumentData, and from within it you will call updates to simple values, and later on updates for tables. An update for a single value is done by calling the UpdateDocumentValue method like you can observe in the following sample:
UpdateDocumentValue(
"/iv:invoice/iv:customer/iv:name",
BuildCustomXMLSelectionQuery(
XMLNS_IV,
new string[]
{
"invoice",
"customer",
"name"
}),
data.DocumentElement,
document.DocumentElement);
There are four things you need to pass into the UpdateDocumentValue method, two X-Path expressions and two XmlNode objects. The first X-Path expression is the expression that selects the data inside the XML data file. The second parameter, which is the second X-Path expression, selects the <w:customXml> tag inside the body of the document. The third parameter passes in the XML data file to query against, and the last parameter does the same for the body of the WordprocessingML document.
Selecting the right <w:customXml> tags
Now the most interesting bit in this piece of code is the call to the BuildCustomXMLSelectionQuery method. In my previous blog post on this subject I showed how <w:customXml> tags can form a hierarchy:
<w:customXml w:uri="urn:mynamespace" w:element="myElement">
<w:p>
<w:customXml w:uri="urn:mynamespace" w:element="mySubElement">
<w:r>
<w:t>Data for myElement</w:t>
</w:r>
</w:customXml>
</w:p>
</w:customXml>
So in order to select the part of the WordprocessingML document we need to walk this hierarchy. As you can see the <w:customXml> tag has two parts, the @w:uri attribute defines the XML namespace of your data file, and the @w:element provides the name of the XML data element. To walk the hierarchy you need to have the combination of namespaces / element names to select each <w:customXml> tag in a hierarchy. My sample only supports the usage of a single XML namespace for the data file, so the first parameter is that namespace (using the XMLNS_IV constant in the project), and of course you will need to support the list of element names to select the right <w:customXML> tag. A call to BuildCustomXMLSelectionQuery for the previous sample would look like:
string xpath = BuildCustomXMLSelectionQuery(
"urn:mynamespace",
new string[]
{
"myElement",
"mySubElement"
});
The resulting X-Path expression will look like the following (note that I formatted the line a little to make it fit):
descendant::w:customXml[
@w:uri='urn:mynamespace' and @w:element='myElement']
/descendant::w:customXml[
@w:uri='urn:mynamespace' and @w:element='mySubElement']
The BuildCustomXMLSelectionQuery method is rather simple. You just need to format the string
"descendant::w:customXml[@w:uri='{0}' and @w:element='{1}']"
for each node name that is passed in to the method. Since you need to add the '/' character in between the formatted strings you need to have a helper function, which we will put into a Func<T, TReturn> delegate using a lambda (note that you can also use an explicit separate helper method, but that makes the code more verbose since there are more methods)
static string BuildCustomXMLSelectionQuery(
string xmlNamespace,
string[] nodes)
{
Func<string, string> buildMethod =
name => String.Format(
"descendant::w:customXml[@w:uri='{0}' and @w:element='{1}']",
xmlNamespace, name);
// more code to go here
}
In the place where the is displayed, more code is used J. The following block was missing in that spot. It's the code that does the heavy-lifting for building the X-Path.
StringBuilder xpath = new StringBuilder();
string nodeName = nodes[0];
xpath.Append(buildMethod(nodeName));
for (int i = 1; i < nodes.Length; i++)
{
xpath.Append("/");
xpath.Append(buildMethod(nodes[i]));
}
return xpath.ToString();
Updating the value in the document
Now we know what to do, and we know how to do it. Without further ado, let's go ahead and do it. All the following blocks of code go inside the UpdateDocumentValue method, you know, the one with the four parameters, two xpaths + two node objects.
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
{
}
Inside the UpdateDocumentValue method we first need to select the data value, by executing the XPath that is passed in to the method.
XmlNode dataNode = dataPart.SelectSingleNode(
dataSelector, mgr);
Easy enough, same for the getting the <w:customXml> tag.
XmlNode customXmlNode = documentPart.SelectSingleNode(
customXmlSelector, mgr);
The next step is building a part of the paragraph-run-text hierarchy used by WordprocessingML. The UpdateDocumentValue method cheats a little, by not looking down into the WML hierarchy, but only upwards toward the root. That way we know for sure that we need to create the <w:r> and <w:t> tags ourselves.
XmlNode runNode =
documentPart.OwnerDocument.CreateElement("w", "r", XMLNS_W);
XmlNode textNode =
documentPart.OwnerDocument.CreateElement("w", "t", XMLNS_W);
textNode.InnerText = dataNode.InnerText;
runNode.AppendChild(textNode);
To ensure that our content will fit snugly into the WordprocessingML markup, we clear all the content of the <w:customXml> tag. Note that this does also throw away any special formatting that you did. So for a more real-life sample you would need to more complex things, but I think you'll find that this will work well enough for many of your situations.
ClearChildNodes(customXmlNode);
We do need to know if the <w:customXml> is inside a paragraph or inside the document body. If it is inside the document body tag (<w:body>) we need to add an extra paragraph ourselves.
XmlNode wmlParentContainer = FindContainerNode(customXmlNode);
if (wmlParentContainer.LocalName == "body")
{
XmlNode paragraphNode =
documentPart.OwnerDocument.CreateElement(
"w", "p", XMLNS_W);
paragraphNode.AppendChild(runNode);
customXmlNode.AppendChild(paragraphNode);
}
else if(wmlParentContainer.LocalName == "p")
{
customXmlNode.AppendChild(runNode);
}
That is it, and that is that. You can now update simple values into a schema-mapped document by calling a single simple method. In my next blog post I will show how to do the same with tables, and I will post the code for you (note that this was also one of my devdays samples, so you can pick it out of that download too if you want to).
Hope it helps!