wordpress hit counter
Welcome to OpenXML Developer Sign in | Join | Help

Custom XML Markup sample

This article gives the code and the demonstration steps of the usage of a powerful feature of Open XML called ‘Custom XML’. It explains how custom xml data can be integrated and used to create dynamic Word documents. It uses the Open XML Packaging Convention in order to do the same.

 

This example was built by Michael Höhne, who attended the Open XML Workshop at Germany on 4th May 2007, with the guidance from our trainers.

 

In this example we display the details from an external XML called the “invoice.xml” which in turn is the custom xml of the document in the form of an Invoice which includes some static/repetitive data like the delivery address, common textural information etc and dynamic content line the product line with its number, price, quantity and line total.

 

The “invoice.xml” would contain the following data –

<?xml version="1.0" encoding="utf-8"?>

<invoice number="4711" date="2007-05-04T12:35:00Z">

  <customer id="{B37C4413-7F67-42de-BFD1-91E1199952C7}">

    <formattedAddress>

      <![CDATA[

stunnware

Michael Höhne

Harthausener Str. 17

 

85630 Grasbrunn, Germany

      ]]>

    </formattedAddress>

  </customer>

  <lines net="336.92" tax="64.01" total="400.93">

    <line part="Keyboard" price="59.00" qty="3.00" total="177.00" />

<line part="Mouse" price="19.99" qty="8.00" total="159.92" />

    <line part="Monitor" price="399.00" qty="1.00" total="399.00" />

    <line part="Printer" price="59.00" qty="3.00" total="177.00" />

    <line part="Desk" price="19.99" qty="8.00" total="159.92" />

    <line part="Mouse Pad" price="399.00" qty="1.00" total="399.00" />

    <line part="CPU" price="59.00" qty="3.00" total="177.00" />

    <line part="CPU1" price="59.00" qty="3.00" total="177.00" />

  </lines>

</invoice>

 

The Invoice template (InvoiceTemplate.docx) would look something similar like this –

 

Here we can see that the dynamic content has been marked, these are the content controls which are placed in the Invoice template. The content controls are present in the ‘Developer’ ribbon of the Microsoft Office Word 2007 application. If the Developer ribbon is not visible, it can be enabled by clicking on Office button on left top most corner of the application and clicking on the word options and selecting the show developer tag in the ribbon option.

 

After the content controls are placed in the template document, a bookmark is placed at the beginning of the Product Line content controls encompassing all of them. This is needed because the content from the “invoice.xml” is replaced at this bookmark for the respective content controls.

 

Now that we have our data in the xml ready and the invoice template document ready, we can write a small .NET code to use this data and generate dynamic Word documents. This class would follow these steps:

 

  1. Declare three string variables which stores the path of the xml data file, template document file and the to-be generated invoice document file.

const string Source = @"D:\OpenXML\Invoice\InvoiceTemplate.docx";

const string Target = @"D:\OpenXML\Invoice\Invoice.docx";

const string InvoiceXml = @"D:\OpenXML\Invoice\Invoice.xml";

 

  1. Make a copy of the template which creates the new invoice document.

File.Copy(Source, Target, true);

  1. Open this newly create invoice document and get the main document part which is the “document.xml”.

Package invoice = Package.Open(Target);

PackagePartCollection parts = invoice.GetParts();

PackagePart wordDocumentMainPart = null;

 

foreach (PackagePart part in parts) {

 if (part.ContentType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml") {

      wordDocumentMainPart = part;

      break;

 }

}

  1. Load the content of the main document part into an XMLDocument object.

XmlDocument mainDocument = new XmlDocument();

using (Stream s = wordDocumentMainPart.GetStream(FileMode.Open, FileAccess.Read)) {

            mainDocument.Load(s);

         }

  1. Load the content of the “invoice.xml” into an XMLDocument object and get the details of all the line items.

XmlDocument invData = new XmlDocument();

invData.Load(InvoiceXml);

XmlNodeList invoiceLines = invData.SelectNodes("invoice/lines/line");

  1. Iterate through each node of the invoice line items and place the x:path address of the these line items of the custom xml into the w:dataBinding element of the main document and assign the value to the respect w:sdt elements. After this remove the bookmarks from the document.

for (int i = 1; i < invoiceLines.Count; i++) {

      XmlNode nextLine = currentRow.CloneNode(true);

      currentRow.ParentNode.InsertAfter(nextLine, currentRow);

 

      XmlNodeList dataBindings = nextLine.SelectNodes(".//w:dataBinding", nsMgr);

 

foreach (XmlNode dataBinding in dataBindings) {

            XmlAttribute attr = dataBinding.Attributes["w:xpath"];

            attr.Value = attr.Value.Replace("line[" + i + "]", "line[" + (i + 1) + "]");

      }

XmlNodeList contentTagIds = nextLine.SelectNodes(".//w:sdt/w:sdtPr/w:id", nsMgr);

 

      foreach (XmlNode contentTagId in contentTagIds) {

            XmlAttribute valAttr = contentTagId.Attributes["w:val"];

            valAttr.Value = nextContentControlId.ToString();

            nextContentControlId++;

      }

 

while (true) {

            XmlNode bookmark = nextLine.SelectSingleNode("w:bookmarkStart", nsMgr);

            if (bookmark != null) {

                  nextLine.RemoveChild(bookmark);

            }

else {

                  break;

            }

      }

 

              

      currentRow = nextLine;

}

  1. To include the custom xml into the document, get the custom xml part and load the “invoice.xml” into the custom xml part.

Uri customXmlUri = new Uri("/customXml/item1.xml", UriKind.Relative);

PackagePart customXmlPart = invoice.GetPart(customXmlUri);

using (Stream s = customXmlPart.GetStream(FileMode.Create, FileAccess.Write)) {

invoiceData.Save(s);

}

  1. Save the main document, flush it and close it.

using (Stream s = wordDocumentMainPart.GetStream(FileMode.Create, FileAccess.Write)) {

mainDocument.Save(s);

}

invoice.Flush();

invoice.Close();

  1. The invoice document which is created and edited for the inclusion of the custom xml data can be viewed to have all the line item data.

 

This is a simple example demo which illustrates the usage of custom xml with the static content not being disturbed and the dynamic content being changed based on the required data changes. This feature can be extended to fetch data from a backend database and generate dynamic word processing documents.

 

The example demo is attached with the article as a zip file.

Published Saturday, June 30, 2007 6:52 AM by SanjayKumarM Edit
Attachment(s): Invoice.zip

Comments

 

Mike_at_Software said:

Thanks for the post.

I've converted the sample to VB.net, and successfully generated the invoice.docx. There's only one propblem which I don't understand. The lines below (equivalent of line 108 onwards in form1.cs) always result in a fault, complaining about object reference not set when executing RemoveChild. Commenting out that line allows the sample to run, and generate the invoice - but what could be causing the fault?

>
While True
                   Dim bookmark As XmlNode = nextLine.SelectSingleNode("w:bookmarkStart", nsMgr)
                   If Not IsDBNull(bookmark) Then
                       'This always faults here, complaining about object not existing: Why?
                       ''''nextLine.RemoveChild(bookmark)
                   Else
                       Exit While
                   End If
               End While

July 27, 2007 2:06 PM [Remove this Comment]
 

TwelveBaud said:

If Not IsDBNull(bookmark) Then

should read:

If Not IsNothing(bookmark) Then
July 30, 2007 12:14 PM [Remove this Comment]
 

Julien Patte said:

Nice example, this is exactly what I needed!

Btw, it seems like there is a hidden infinite loop there:

           XmlNode currentRow = bookmarkInvoiceLine.ParentNode;

           while ((currentRow != null) && (currentRow.LocalName != "tr")) {
              currentRow = bookmarkInvoiceLine.ParentNode;
           }

Obviously, it should be

           XmlNode currentRow = bookmarkInvoiceLine.ParentNode;

           while ((currentRow != null) && (currentRow.LocalName != "tr")) {
              currentRow = currentRow.ParentNode;
           }
August 1, 2007 9:39 AM [Remove this Comment]
 

Mike_at_Software said:

Thanks TwelveBaud
August 1, 2007 1:43 PM [Remove this Comment]
 

Angna said:

Hi,
I want to add Functionality like Final Showing markup with VB.NET
can any 1 suggest?
August 17, 2007 7:16 AM [Remove this Comment]
Anonymous comments are disabled