Welcome to OpenXML Developer Sign in | Join | Help

WordProcessingML Snippets: Part 6, Custom XML and Content Controls

This article is part of the “Building documents with code snippets” series.

This will be the final article on WordProcessingML for now. I will go into some of the details of working with custom XML. If you are new to working with custom xml inside your document, let me explain it a little bit. The idea  behind custom xml parts is data/view separation. You can basically mark up your document like normal, but store some data elements such as a ‘customer name’ or ‘invoice item’ outside of the main document body inside your own XML part within the OPC package. It is easy to update this separate XML part with new data, thereby ‘hydrating’ the document with custom values. One of the cool things about this is that the document doesn’t only display the custom XML data inline in the document, but when you update the fields through the Word UI the custom XML file is updated as well with these new values.

Storing and using custom xml files inside your document is a two-fold action.  First you need to store the XML file (or "part") within the package, and next you need to create the required WordProcessingML nodes within the body of the document.

Inside the WordProcessingML, XPath is used to query the custom XML files for data. Because there might be various custom XML files which you need to differentiate between, you can give each XML file a GUID identifier, and of course XML namespaces are available as well. These two items are specified inside another XML file called the custom XML properties file. One properties file is allowed for each custom XML file in the package.

Storing Custom XML files

First up is storing custom XML files inside the package. The following method uses the HowToStoreFile snippet to store an external XML file inside the package using a specific content type and relationship type. The code places the external XML file inside the /word/customXML folder. 

        public static PackagePart StoreCustomXmlPart(
                PackagePart documentPart, string fileName, 
                Guid storeItemID, params string[] schemaReferences)
        {
            string customXmlRelationshipType = 
                        "
http://schemas.openxmlformats...";
            string customXmlPropertiesRelationshipType =
                         "
http://schemas.openxmlformats...";

            PackagePart customXmlPart = StoreFile(fileName,
                "/word/customXML",
                "application/xml",
                documentPart.Package);

Next up this method provides some code for creating the custom XML properties file. A separate method creates the full Open XML structure and returns it as an XmlDocument. This document is later streamed into the package. The code takes the location of the custom xml file and generates a filename for the properties file. A small note is that this code doesn’t use the HowToStoreFile snippet to generate unique filenames, but assumes the generated filename to be unique through convention. 

            XmlDocument customXmlProperties = CreateCustomXmlPartPropertiesDocument(
                storeItemID, schemaReferences);
            string customXmlFileName = customXmlPart.Uri.ToString();

            string customXmlPropertiesFileName =
                Path.Combine(Path.GetDirectoryName(customXmlFileName),
                    Path.GetFileNameWithoutExtension(customXmlFileName) + "Props.xml");
            customXmlPropertiesFileName = customXmlPropertiesFileName.Replace(
                "\\", "/");
            PackagePart customXmlPropertiesPart = documentPart.Package.CreatePart(
                new Uri(customXmlPropertiesFileName, UriKind.Relative),
                "application/xml");
            using (Stream stream = customXmlPropertiesPart.GetStream())
            {
                customXmlProperties.Save(stream);
            }

All that needs to be done now is relate the various PackagePart objects using the Packaging API.
            documentPart.CreateRelationship(customXmlPart.Uri,
                        TargetMode.Internal, customXmlRelationshipType);
            customXmlPart.CreateRelationship(
                customXmlPropertiesPart.Uri, TargetMode.Internal, customXmlPropertiesRelationshipType);
            return customXmlPart;
        }

That is it for storing a custom XML file inside the document. You can now query the document for the data inside the XML by creating a set of WordProcessingML nodes. Which we will do with a second snippet.

Creating Content Controls

The second snippet takes care of creating a text content control using WordProcessingML elements. The new content control will be data bound to the custom XML file. You can specify the GUID identifier which is tied to the XML file and use namespace prefixes inside the XPath query.

One note about the XPath expressions used for data binding: at the moment, it is not yet possible to bind to XML in a truly recursive fashion.  Whether this will be supported in later versions of Open XML is not clear at this time. If you want to bind to recursive xml, you need to create each control with a separate full XPath query, e.g. /products/product[2]/name and /products/product[3]/name.

The code creates a simple text content control, which is identified by the <w:text  /> node. There is no support for creating other types of nodes, because the xml required can differ quite a bit. It should be easy to implement a more generic method, though.

        public static void AddContentControl(
            XmlNode parent, XmlNamespaceManager namespaces,
            string friendlyName, string xpath, string prefixMapping, Guid storeItemID)
        {
            string wordNamespace = namespaces.LookupNamespace("w");

            XmlNode documentTagNode = parent.OwnerDocument.CreateElement(
                "w", "sdt", wordNamespace);
            XmlNode documentTagPropertiesNode = parent.OwnerDocument.CreateElement(
                "w", "sdtPr", wordNamespace);
            XmlNode aliasNode = parent.OwnerDocument.CreateElement(
                "w", "alias", wordNamespace);
            XmlAttribute aliasValAttribute = parent.OwnerDocument.CreateAttribute(
                "w", "val", wordNamespace);
            XmlNode dataBindingNode = parent.OwnerDocument.CreateElement(
                "w", "dataBinding", wordNamespace);
            XmlAttribute dataBindingXPathAttribute = parent.OwnerDocument.CreateAttribute(
                "w", "xpath", wordNamespace);

Depending on what the user specifies in the method parameters, the prefix mappings node and the storeItemID node are created. The prefix mappings are required if the XPath expression contains namespace prefixes, e.g. /myNs:products/myNS:product[0]/myNS:name. The storeItemID can be used to identify the custom XML file.

            if (String.IsNullOrEmpty(prefixMapping) == false)
            {
                XmlAttribute dataBindingPrefixesAttribute = parent.OwnerDocument.CreateAttribute(
                    "w", "prefixMapping", wordNamespace);
                dataBindingPrefixesAttribute.Value = prefixMapping;
                dataBindingNode.Attributes.Append(dataBindingPrefixesAttribute);
            }
            if (storeItemID != Guid.Empty)
            {
                XmlAttribute dataBindingStoreItemIDAttribute = parent.OwnerDocument.CreateAttribute(
                    "w", "storeItemID", wordNamespace);
                dataBindingStoreItemIDAttribute.Value = storeItemID.ToString("B");
                dataBindingNode.Attributes.Append(dataBindingStoreItemIDAttribute);
            }

            XmlNode textControlNode = parent.OwnerDocument.CreateElement(
                "w", "text", wordNamespace);
            aliasValAttribute.Value = friendlyName;
            dataBindingXPathAttribute.Value = xpath;

And finally add the nodes together.

            aliasNode.Attributes.Append(aliasValAttribute);
            documentTagPropertiesNode.AppendChild(aliasNode);
            dataBindingNode.Attributes.Append(dataBindingXPathAttribute);
            documentTagPropertiesNode.AppendChild(dataBindingNode);
            documentTagPropertiesNode.AppendChild(textControlNode);
            documentTagNode.AppendChild(documentTagPropertiesNode);
            parent.AppendChild(documentTagNode);
        }

That is the last of the WordProcessingML snippets. I’ll be back later on with more info on Open XML, perhaps some PresentationML or SpreadsheetML is in order after all of this.

Published Saturday, August 05, 2006 1:22 PM by Wouterv

Comments

 

AgeBee said:

Hi,
can you tell me where I can get the complete coding from ?
What I miss is the methods ValidateFilename and CreateCustomXmlPartPropertiesDocument ?
It would be nice if you could help me with it.
Thanks
Harald
July 17, 2007 9:16 AM
 

rezash said:

I used this snippet ( Add content control ) but it didn't work and don't display any textbox content control in my word document !
August 19, 2008 7:35 AM
Anonymous comments are disabled

About Wouterv

Wouter is responsible for development and support of a .NET training curriculum with an emphasis on C#, ASP.NET and Office development.