Search code examples
ms-wordopenxmlopenxml-sdk

create a new document from word template with multiple pages using documentformat.openxml


I have a Microsoft Word template with some content controls. It contains a table of contents and some extra information.

On the page, I have set some content controls where I would like to inject a new text from a RESTful service. If the RESTful service returns an array of data (objects), I need to replicate the information on each page based on the Word template.

Any idea on how can I do that using the Open XML SDK (DocumentFormat.OpenXml)?

Edit:

I found this post here, which is great, but I don't know how can I apply the array of data to multiple pages from the same template.

So, how can I create multiple pages from the same template in a new document? The data comes in as an array.


Solution

  • The sample code below (which is unit-tested and works) does what you are trying to achieve. It is based on the following interpretation of the question and assumptions:

    • "Control place holders" means "Rich Text content controls", which are called block-level structured document tags (SDTs) in Open XML lingo and are thus represented by the SdtBlock class in the Open XML SDK.
    • The content controls have tags, meaning the relevant w:sdt elements have grandchild elements like <w:tag="tagValue" />. Those tags are used to link the data received from the REST service to the content controls.
    • The data is provided as a Dictionary<string, string>, mapping tag values to the content control text (data).

    The general approach is to perform a pure functional transformation of the main document part of your WordprocessingDocument. The void WriteContentControls(WordprocessingDocument) method wraps the outermost pure functional transformation object TransformDocument(OpenXmlElement). The latter uses an inner pure functional transformation object TransformSdtBlock(OpenXmlElement, string).

    public class ContentControlWriter
    {
        private readonly IDictionary<string, string> _contentMap;
    
        /// <summary>
        /// Initializes a new ContentControlWriter instance.
        /// </summary>
        /// <param name="contentMap">The mapping of content control tags to content control texts.
        /// </param>
        public ContentControlWriter(IDictionary<string, string> contentMap)
        {
            _contentMap = contentMap;
        }
    
        /// <summary>
        /// Transforms the given WordprocessingDocument by setting the content
        /// of relevant block-level content controls.
        /// </summary>
        /// <param name="wordDocument">The WordprocessingDocument to be transformed.</param>
        public void WriteContentControls(WordprocessingDocument wordDocument)
        {
            MainDocumentPart part = wordDocument.MainDocumentPart;
            part.Document = (Document) TransformDocument(part.Document);
        }
    
        private object TransformDocument(OpenXmlElement element)
        {
            if (element is SdtBlock sdt)
            {
                string tagValue = GetTagValue(sdt);
                if (_contentMap.TryGetValue(tagValue, out string text))
                {
                    return TransformSdtBlock(sdt, text);
                }
            }
    
            return Transform(element, TransformDocument);
        }
    
        private static object TransformSdtBlock(OpenXmlElement element, string text)
        {
            return element is SdtContentBlock
                ? new SdtContentBlock(new Paragraph(new Run(new Text(text))))
                : Transform(element, e => TransformSdtBlock(e, text));
        }
    
        private static string GetTagValue(SdtElement sdt) => sdt
            .Descendants<Tag>()
            .Select(tag => tag.Val.Value)
            .FirstOrDefault();
    
        private static T Transform<T>(T element, Func<OpenXmlElement, object> transformation)
            where T : OpenXmlElement
        {
            var transformedElement = (T) element.CloneNode(false);
            transformedElement.Append(element.Elements().Select(e => (OpenXmlElement) transformation(e)));
            return transformedElement;
        }
    }
    

    This should provide enough input for implementing your specific solution even if the details vary (e.g., regarding how you map your array of data to the specific content controls). Further, if you don't use block-level structured document tags (SdtBlock, Rich Text content controls) but but rather inline-level structured document tags (SdtRun, plain text content controls), the principle is the same. Instead of the Paragraph instances (w:p elements) contained in SdtContentBlock instances, you would have Run instances (w:r elements) contained in SdtContentRun instances.

    Update 2019-11-23: My CodeSnippets GitHub repository contains the code for the ContentControlWriter and AltChunkAssemblyTests classes. The latter shows how the ContentControlWriter class can be used.