Using custom XML parts in Word add-ins
Microsoft Word contains enough tools to make any writer productive. But writers are peculiar people and they need and require more features. Writers that are also developers have the power to enable their peculiarities and thrust them upon others.
A good document complies with certain qualities or standards. You can check them off and enforce your standards if you know how to employ the right technology. For what I want to accomplish today, we need to employ custom XML parts.
- What are custom XML parts?
- What can you do with XML parts?
- When should you use XML parts?
- How do I start developing with an XML part?
- Creating a Word add-in that uses custom XML parts
- Writing code that works with custom XML parts
Custom XML parts are chunks of XML that reside within a Word document. They are not part of the document, per se, because they are not visible to the user. Starting with Office 2007, the Office file formats are XML-based and are comprised of XML parts.
So, just think of custom XML parts like you do custom document properties. They aren’t built-in and exist for your purposes.
Unlike document properties, you can store lots of data in a single part. This is due to the fact that you store a chunk of XML in a custom XML part. So, fill that sucker up with any and all data that fits your needs. Personally, I think they are perfect for storing process data related to the document. For example, a document to do list.
Hmmm… sounds like the basis for a solid code sample.
The short answer is whenever you want if you are using Word 2007 or later. The longer answer is you should use them when your requirements are more complex than storing data in Word document properties.
Well, besides the obvious tools of Visual Studio, Office, and (of course) Add-in Express, you need the following:
1. An idea
And the skills. No doubt you can’t make it happen if you don’t know how to do it. And lucky of you, providence has smiled upon us all. I have a solid code sample to show just how to make effective use of custom XML parts in Microsoft Word.
In this sample Word addin, I’ll show you how to build a custom Word task pane that contains a document-specific to-do list. This to-do list’s state will reside in its corresponding Word document as a custom XML part. When the user saves the document, we will insert the to-do list’s state. When the user opens the document, we will retrieve the custom XML part and display it in the custom task pane.
How do we retrieve it you ask? I can’t tell you now. You must read on to learn the secret.
Before we start, you need to create a new Add-in Express based COM Add-in project in Visual Studio. I’ll use Visual Studio 2012 with Add-in Express for Office and .NET.
Creating the Word COM add-in project
Go ahead and create the project (you can find the detailed step-by-step instructions in this article Building Office COM add-ins) and open the AddinModule in design view. After you do that, the fun can begin. By the way, I named my sample project UsingXMLParts. You can do the same but it isn’t a requirement.
Add the necessary Add-in Express components
We need to add three components to the project… we’ll take them one-at-a-time.
Advanced Word task pane
In the Visual Studio solution explorer window, right-click the project name and click Add. In the Add New Item dialog…
- Go to the Add-in Express Items, select Word, and select ADX Word Task Pane.
- Name the pane DocToDoPane and click OK.
The DocToDoPane will look like the image below. I’ve added the controls and their properties. Go ahead and add them.
Note. I apologize for the use of the Telerik RADTreeview control… but not really. For code samples, I really try to use only native WinForm controls. But, in this case, the Telerik control simplifies the scenario and allowed me to focus solely on custom XML parts. If I were to use the WinForm treeview, we would need to write a lot of “not-germane-to-the-topic” code. I didn’t want to write that code so, Telerik it is.
Word Task Panes Manager
After completing the task pane’s design, we need to open the AddInModule design view and add an ADXWordTaskPanesManager. Just right click the AddInModule and select it from the context menu.
After adding the task panes manager, select it and open its Items collection. In the ADXWordTaskPanesCollection Editor dialog, add a new collection item and configure its properties to match this image:
Word Events component
This component allows us to write code that responds to Microsoft Word events. In the AddinModule design view, ADXWordEvents icon in the AddinModule toolbar. You don’t need to do anything with this component. The code we write next will take care of it..
The code resides in two locations:
- DocToDoPane: the code for saving and retrieving XML parts resides here.
- AddinModule: the code for responding to Word events resides here.
There’s more to it than this but this fulfills Pareto’s law.
Writing code for the DocToDoPane
To me, the logical place for creating, saving, and retrieving custom XML parts is in the task pane. I think this because the XML parts display in the task pane via the treeview control. You might think differently.
A user will work with a word document and edit the task pane’s to-do list. This To-Do list resides in a tree view control. We need to save the state of this treeview in a custom XML Part. This what the SaveToDoList function does.
Friend Function SaveToDoList() As Boolean 'Wrapper method for a call from AddinModule methods. 'Calls the AddCustomXmlPartToDocument function Try Dim wApp As Word.Application = TryCast(Me.WordAppObj, Word.Application) AddCustomXmlPartToDocument(wApp.ActiveDocument, RadTreeView1.TreeViewXml) Return True Catch ex As Exception Return False End Try End Function
The To-Do list has a property called WordAppObj. This is the Word Application object. We use this object to gain access to the ActiveDocument and pass it to the AddCustomXMLPartToDocument method. We also pass the RadTreeView‘s XML value.
The name says it all. This method adds a custom XML part to a Word document.
Private Sub AddCustomXmlPartToDocument(ByVal document As Word.Document, xml As String) '1-Creates an XML Part and saves into the doc's CustomXMLParts collection '2-Creates a custom document property to store the CustomXMLPart's ID. Dim xmlString As String = xml Dim treeviewXMLPart As Office.CustomXMLPart = document.CustomXMLParts.Add(xmlString) 'Add document property Try document.CustomDocumentProperties.Add( _ "treeViewToDO", False, _ Office.MsoDocProperties.msoPropertyTypeString, treeviewXMLPart.Id) Catch ex As Exception document.CustomDocumentProperties("treeViewToDO") = treeviewXMLPart.Id End Try Marshal.ReleaseComObject(treeviewXMLPart) End Sub
The method takes the passed Word document and XML string and adds a new CustomXMLPart to the document’s CustomXMLParts collection.
But this isn’t all! The method also creates a custom document property. This property stores the custom XML part’s ID. This is the secret sauce that allows us to retrieve the part later.
This method starts the custom XML part retrieval process. It mimics aspect of SaveToDoList because it grasps the Word Application object to retrieve the ActiveDocument. It then calls the RetrieveCustomXMLPart method to retrieve the custom XML part.
Private Const tempXMLFilePath As String = "C:\_projects\TVTOC.xml" Friend Function LoadToDoList(partID As String) As Boolean 'Populates the TreeView control by retrieving the CustomXML from the document and… 'saving to the file system (because it fails if I don't do this and try to 'load straight from memory… no idea why) and… 'calling the treeview's LoadXML method. Try Dim wApp As Word.Application = TryCast(Me.WordAppObj, Word.Application) Dim result As String = RetrieveCustomXMLPart(wApp.ActiveDocument, partID) Dim writer As New StreamWriter(tempXMLFilePath, False) writer.Write(result) writer.Close() If result <> "" Then RadTreeView1.LoadXML(tempXMLFilePath) End If Return True Catch ex As Exception Return False End Try End Function
With the XML in-hand, the method writes its content to a file (tempXMLFilePath) and the loads it into the RadTreeView. This seems silly to me but… when I try to load the XML directly from memory, the RadTreeView control would fail. So, I went with a solution that worked. And… it works well.
LoadFreshList list method
This method exists to clear the tree view control so that no nodes have checks in their check boxes.
Friend Function LoadFreshList() 'Friendly helper method to clear the treeview for a new document. RadTreeView1.ClearSelection() End Function
This situation is relevant anytime a user creates a new Word document.
This method retrieves the custom XML part that matched the passed partID string.
Private Function RetrieveCustomXMLPart(doc As Word.Document, partID As String) As String 'Retrieves the CustomXMLPart using the passed partID. Dim treeviewXMLPart As Office.CustomXMLPart = Nothing Try treeviewXMLPart = doc.CustomXMLParts.SelectByID(partID) Return treeviewXMLPart.XML.ToString Catch ex As Exception Return "" End Try Marshal.ReleaseComObject(treeviewXMLPart) End Function
This method really makes the use of the document property to store the ID look really smart. I love it when a plan comes together.
Writing code for the Addin Module
The code in the AddinModule exists to respond to relevant document events. In addition, it is the place for helper methods that call the “friendly” methods residing in the DocToDoPane. I didn’t mention it before but maybe you noticed. Some of the methods in the DocToDoPane use the Friend modifier. This is done so that we can call them form the events code residing in the AddInModule.
Not all documents support custom XML parts. As I mentioned before, only the XML-based Office documents support them. This means existing “.docx” document or new document created with Word 2007, Word 2010 or 2013.
Private Function IsXMLSupported(doc As Word.Document) As Boolean 'Quick and Dirty rule for determining if we can inject a 'CustomXML Part into the document. If Right(doc.FullName, 4) = "docx" Then Return True ElseIf Right(doc.FullName, 3) = "doc" Then Return False ElseIf doc.Application.Version >= 12 Then Return True Else Return False End If End Function
This method runs through a series of tests to determine if the document supports XML. We need this test to save some cycles in our code… as you will see.
This is the first method to call one the DocToDoPane’s “friendly” methods. It starts by creating a reference to the current instance of the Word task pane.
Private Sub RefreshToDoPane(doc As Word.Document, display As Boolean) Dim toDoPane As DocToDoPane toDoPane = TryCast( _ AdxWordTaskPanesCollectionItem1.CurrentTaskPaneInstance, DocToDoPane) If display Then Dim xmlPartID As String = HasToDoList(doc) toDoPane.LoadToDoList(xmlPartID) End If toDoPane.Visible = display End Sub
With the task pane in-hand, the method checks for the existence of a To-Do list (by calling HasToDoList). If a To-Do list exists, the methods calls the task pane’s LoadToDoList method and passes the custom XML Part’s ID to it.
So just how do we determine whether or not a document has a To-Do list? Well, we check the custom document properties for the existence of the treeViewToDo property. If it exists, we assume the document as a To-Do list.
Friend Function HasToDoList(doc As Word.Document) As String 'Check for the treeViewToDO custom property. 'If it exists, we should have a customXML part to retrieve 'and display in the DocToDoPane Try 'if this works, we have a Todo list Dim prop As Office.DocumentProperty = _ doc.CustomDocumentProperties.Item("treeViewToDO") If Not prop Is Nothing Then Dim propValue As String = prop.Value Marshal.ReleaseComObject(prop) Return propValue End If Catch ex As Exception Return "" End Try End Function
It’s simple but elegant.
This is another method that calls a task pane “friendly” method… in this case, the SaveToDoList method.
Private Sub SaveToDoPane(doc As Word.Document) 'Just a wrapper method to call the docToDoPane's Save method If IsXMLSupported(doc) Then Dim toDoPane As DocToDoPane toDoPane = TryCast( _ AdxWordTaskPanesCollectionItem1.CurrentTaskPaneInstance, DocToDoPane) toDoPane.SaveToDoList() End If End Sub
Before it makes this call, it needs to grab the instance of the custom task pane that belongs to the passed Word document.
The DocumentBeforeSave event
This event occurs before a document saves. It’s the ideal location for the code to trigger the saving of the To-Do list’s state.
Private Sub adxWordEvents_DocumentBeforeSave( _ sender As Object, e As ADXHostBeforeSaveEventArgs) _ Handles adxWordEvents.DocumentBeforeSave Try SaveToDoPane(TryCast(e.HostObject, Word.Document)) Catch ex As Exception End Try End Sub
The event code references the document being saved and calls the SaveToDoPane method.
The DocumentOpen event
When the user opens a Word document, we need to check if the document supports XML.
Private Sub adxWordEvents_DocumentOpen(sender As Object, hostObj As Object) _ Handles adxWordEvents.DocumentOpen Dim doc As Word.Document = TryCast(hostObj, Word.Document) If IsXMLSupported(doc) Then RefreshToDoPane(doc, True) Else RefreshToDoPane(doc, False) End If End Sub
If the XML is in-play, we need to refresh the custom task pane and display it. If not, we need to hide the task pane. The RefreshToDoPane method handles both scenarios.
The DocumentBeforeClose event
When a document closes, we need to call the RefreshToDoPane method and tell it to hide the task pane.
Private Sub adxWordEvents_DocumentBeforeClose(sender As Object, _ e As ADXHostBeforeActionEventArgs) _ Handles adxWordEvents.DocumentBeforeClose Dim doc As Word.Document = TryCast(e.HostObject, Word.Document) RefreshToDoPane(doc, False) End Sub
This is especially relevant when Word remains open but does not have any documents open. In this situation, it does not make sense to display the task pane. The code above prevents that kind of silliness.
The NewDocument event
When the user creates a new document, we need to check for XML support and respond accordingly.
Private Sub adxWordEvents_NewDocument(sender As Object, hostObj As Object) _ Handles adxWordEvents.NewDocument Dim doc As Word.Document = TryCast(hostObj, Word.Document) If IsXMLSupported(doc) Then Dim toDoPane As DocToDoPane toDoPane = TryCast( _ AdxWordTaskPanesCollectionItem1.CurrentTaskPaneInstance, DocToDoPane) toDoPane.LoadFreshList() End If End Sub
The even calls the IsXMLSupported function. If the document supports XML, then we grab the current task pane instance and create a fresh to-do list.
Awesome. This is how our custom task pane with a To-Do list looks like in Word 2013:
Custom XML Parts enable you to embed data into Word documents. As you see in this sample Word addin, custom XML parts provide considerable power to your solutions. They enable you to embed data that is relevant to the document. The document stores it and the add-in can read it and respond to it. This enables some powerful sharing capabilities.
This sample Word add-in was developed using Add-in Express for Office and .net:
Word add-in development in Visual Studio for beginners:
- Part 1: Word add-in development – Application and base objects
- Part 2: Customizing Word UI – What is and isn’t customizable
- Part 3: Customizing Word main menu, context menus and Backstage view
- Part 4: Creating custom Word ribbons and toolbars
- Part 5: Building custom task panes for Word 2013 – 2003
- Part 6: Working with Word document content objects
- Part 7: Working with Word document designs, styles and printing
- Part 8: Working with multiple Microsoft Word documents
- Part 10: Working with Word document properties, bookmarks, content controls and quick parts
- Part 11: Populating Word documents with data from external sources
- Part 12: Working with Microsoft Word templates