A requirement on a recent SharePoint workflow development project involved the programmatic creation of a formatted Word document. The Word document itself was simple: a title and a table. The table contains rows for each SPListItem in an SPListItemCollection object. I spent a little while looking at how to achieve this without the use of Word automation/Interop before deciding to examine how quickly I could achieve this using the OpenXML SDK and streaming the contents of the XML into an SPFile object. This blog post describes the chosen approach.
Caveats: This post focuses on concepts rather than posting hundreds of lines of code. The idea is that you can take away a good understanding of the tools available to quickly generate OpenXML compliant documents and write them back to SharePoint. This post assumes a good level of knowledge of the SharePoint API and refactoring complex, auto-generated code from productivity tools such that it's production ready. This post also does not focus on using a .dotx template which in hindsight, would have been more appropriate. We'll leave that as a task for the reader :)
Okay.. so lets get started!
You will need to download the OpenXML SDK 2.0 (OpenXMLSDKv2.msi) and OpenXML Productivity Tool (OpenXMLSDKTool.msi) from the following location:
Step 1. Setup your project
Once you have the SDK and productivity tools installed, create a new Console Application and add a reference to DocumentFormat.OpenXml.dll.
Step 2. Generate your base document
The approach I took to quickly generate a document was to create the skeleton layout of the document as shown below. In this instance, I wanted to generate a table with rows comprised of items from an SPListItemCollection.
Step 3. Generate your code stub
The OpenXML productivity tool now allows us to generate a C# stub of code, albeit quite verbose, that represents that document. Open the productivity tool from it's location on the file system (the actual file is OpenXmlSdkTool.exe) and click "Open File" and locate your previously created Word document. Now you need to click on "Reflect Code". The productivity tool will produce a code stub for you. Copy the contents of this file, obviously amending namespaces as necessary etc into your Visual Studio project.
Step 4. Refactor
What you are now able to do is create multiple instances of that document. This of course is no use, so we need to do some refactoring. The extent and nature of the refactoring process depends upon what you want to do. As such, I'm not going to post large amounts of application specific code, rather I'll focus on concepts.
In my case I needed to modify the constructor to take an SPListItemCollection which I then wanted it to use to generate a document and write that back to a document library. As such I modified the signature and behaviour of the CreatePackage() method such that it looked as follows:
public void CreatePackage(string fileName, SPListItemCollection Items,
this.fileName = fileName;
this.items = Items;
SPFolder folder = web.GetFolder(destinationOutputListName);
using (MemoryStream memoryStream = new MemoryStream())
using (WordprocessingDocument package = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document))
SPFile file = folder.Files.Add(fileName, memoryStream, true);
The next piece of refactoring I did was inside the auto-generated method called GenerateMainDocumentPart1Content(). I created two methods, one called GenerateTable() and another called GenerateTableRows(). By default, the productivity tool will create the Table structure for you, what I needed to do was add multiple instances of TableRow objects to the Table object for my list items.
If you create a basic Word document as I did, and search through the code you'll see exactly how the Table object is being created. Based on the initial structure I had, the auto-generated code stub would create one row for me (the headers) which is great. I simply needed to refactor this code out into it's own method and call it multiple times, with each row being populated from each SPListItem. The end result of this is of course a document that looks something like this:
from a base SPList which may look something like this:
Step 5. Package it up
Exactly what you do here depends also depends upon your requirements. In my case I wrapped up these assemblies to be called from an external workflow, this could easily be a web part or some other SharePoint piece of functionality. Either way, you could quite easily call it in the following way:
Until next time... happy SharePointing.
MyWordDoc generatedWordDoc = new MyWordDoc();
SPListItemCollection items = web.Lists["Links"].Items;