The Open XML file formats enable you to modify custom document properties in a Word 2007 or Word 2010 document. The Open XML SDK 2.0 adds strongly typed classes to simplify access to the Open XML file formats. The SDK is designed to simplify the task of modifying custom document properties, and the code sample that is included with this Visual How To shows how to use the SDK to do this.
The sample code included with this Visual How To creates and modifies custom document properties in a Word 2007 or Word 2010 document. To use the sample code, install the Open XML SDK 2.0 from the link listed in the Explore It section. The sample code is included as part of a set of code examples for the Open XML SDK 2.0. The Explore It section also includes a link to the full set of code examples, although you can use the sample code without downloading and installing the code examples.
The sample application modifies custom properties in a document that you supply, calling the WDSetCustomProperty method in the sample to do the work. The method enables you to set a custom property, and returns the current value of the property, if it exists. The calls to the method resemble the following code example.
Const fileName As String = "C:\temp\test.docx"
Console.WriteLine("Manager = " &
WDSetCustomProperty(fileName, "Manager", "Peter",
PropertyTypes.Text))
Console.WriteLine("Manager = " &
WDSetCustomProperty(fileName, "Manager", "Mary",
PropertyTypes.Text))
Console.WriteLine("ReviewDate = " &
WDSetCustomProperty(fileName, "ReviewDate",
#12/21/2010#, PropertyTypes.DateTime))
const string fileName = "C:\\temp\\test.docx";
Console.WriteLine("Manager = " +
WDSetCustomProperty(fileName, "Manager", "Peter",
PropertyTypes.Text));
Console.WriteLine("Manager = " +
WDSetCustomProperty(fileName, "Manager", "Mary",
PropertyTypes.Text));
Console.WriteLine("ReviewDate = " +
WDSetCustomProperty(fileName, "ReviewDate",
DateTime.Parse("12/21/2010"), PropertyTypes.DateTime));
The sample code also includes an enumeration that defines the various possible types of custom properties; The WDSetCustomProperty procedure requires you to supply one of these values when you pass it a property and value.
Public Enum PropertyTypes
YesNo
Text
DateTime
NumberInteger
NumberDouble
End Enum
public enum PropertyTypes : int
{
YesNo,
Text,
DateTime,
NumberInteger,
NumberDouble
}
It is important to understand how custom properties are stored in a Word document. The Open XML SDK 2.0 includes, in its tool directory, a useful application named OpenXmlSdkTool.exe, shown in Figure 1. This tool enables you to open a document and view its parts and the hierarchy of parts. Figure 1 shows the test document after you run the code in this sample, and in the right-hand panes, the tool displays both the XML for the part and reflected C# code that you can use to generate the contents of the part.
Figure 1 shows the Open XML SDK 2.0 Productivity Tool that enables you to view the Open XML content of a document.
Figure 1. Open XML SDK 2.0 Productivity Tool
If you examine the XML content in Figure 1, you will find information, similar to the following, about the code:
Each property in the XML content consists of an XML element, including the name and the value of the property.
For each property, the XML content includes an fmtid attribute, always set to the same string value: {D5CDD505-2E9C-101B-9397-08002B2CF9AE}.
Each property in the XML content includes a pid attribute, which must include an integer starting at 2 for the first property and incrementing for each successive property.
Each property tracks its type (in the figure, the vt:lpwstr and vt:filetime element names define the types for each property).
The sample code provided with this Visual How To includes the code that is required to create or modify a custom document property in a Word 2007 or Word 2010 document.
Setting Up References
To use the code from the Open XML SDK 2.0, you must add several references to your project. The sample project includes these references, but in your own code, you would must explicitly reference the following assemblies:
WindowsBase─This reference may be set for you, depending on the kind of project that you create.
DocumentFormat.OpenXml─Installed by the Open XML SDK 2.0.
You should also add the following using/Imports statements to the top of your code file.
Imports System.IO
Imports DocumentFormat.OpenXml.CustomProperties
Imports DocumentFormat.OpenXml.Packaging
Imports DocumentFormat.OpenXml.VariantTypes
using System.IO;
using DocumentFormat.OpenXml.CustomProperties;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.VariantTypes;
Examining the Procedure
The WDSetCustomProperty procedure accepts four parameters:
The name of the document to modify (string).
The name of the property to add or modify (string)
The value of the property (object)
The type of property (one of the values in the PropertyTypes enumeration)
Public Sub XLInsertHeaderFooter(
ByVal fileName As String, ByVal sheetName As String, _
ByVal textToInsert As String, ByVal type As HeaderType)
public static string WDSetCustomProperty(string fileName,
string propertyName, object propertyValue,
PropertyTypes propertyType)
The procedure returns the existing value of the property, if it exists. To call the procedure, pass all the parameter values, as shown in the following code example.
Const fileName As String = "C:\temp\test.docx"
Console.WriteLine("Manager = " &
WDSetCustomProperty(fileName, "Manager", "Peter",
PropertyTypes.Text))
const string fileName = "C:\\temp\\test.docx";
Console.WriteLine("Manager = " +
WDSetCustomProperty(fileName, "Manager", "Peter",
PropertyTypes.Text));
Handling Procedure Parameters
The WDSetCustomProperty procedure starts by setting up some internal variables. Next, it examines the information about the property, and creates a new CustomDocumentProperty based on the parameters that you have specified. The code also maintains a variable named propSet to indicate whether it successfully created the new property object. This code verifies the type of the property value, and then converts the input to the correct type, setting the appropriate property of the CustomDocumentProperty object.
Note
The CustomDocumentProperty type works much like a VBA Variant type. It maintains separate placeholders as properties for the various types of data it might contain.
Dim returnValue As String = Nothing
Dim newProp As New CustomDocumentProperty
Dim propSet As Boolean = False
' Calculate the correct type:
Select Case propertyType
Case PropertyTypes.DateTime
' Verify that you were passed a real date,
' and if so, format correctly.
' The date/time value passed in should
' represent a UTC date/time.
If TypeOf (propertyValue) Is DateTime Then
newProp.VTFileTime = _
New VTFileTime(String.Format(
"{0:s}Z", nvert.ToDateTime(propertyValue)))
propSet = True
End If
Case PropertyTypes.NumberInteger
If TypeOf (propertyValue) Is Integer Then
newProp.VTInt32 = New VTInt32(propertyValue.ToString())
propSet = True
End If
Case PropertyTypes.NumberDouble
If TypeOf propertyValue Is Double Then
newProp.VTFloat = New VTFloat(propertyValue.ToString())
propSet = True
End If
Case PropertyTypes.Text
newProp.VTLPWSTR = New VTLPWSTR(propertyValue.ToString())
propSet = True
Case PropertyTypes.YesNo
If TypeOf propertyValue Is Boolean Then
' Must be lowercase.
newProp.VTBool = _
New VTBool(
Convert.ToBoolean(propertyValue).ToString().ToLower())
propSet = True
End If
End Select
If Not propSet Then
' If the code could not convert the
' property to a valid value, throw an exception:
Throw New InvalidDataException("propertyValue")
End If
string returnValue = null;
var newProp = new CustomDocumentProperty();
bool propSet = false;
// Calculate the correct type:
switch (propertyType)
{
case PropertyTypes.DateTime:
// Verify that you were passed a real date,
// and if so, format correctly.
// The date/time value passed in should
// represent a UTC date/time.
if ((propertyValue) is DateTime)
{
newProp.VTFileTime = new VTFileTime(string.Format(
"{0:s}Z", Convert.ToDateTime(propertyValue)));
propSet = true;
}
break;
case PropertyTypes.NumberInteger:
if ((propertyValue) is int)
{
newProp.VTInt32 = new VTInt32(propertyValue.ToString());
propSet = true;
}
break;
case PropertyTypes.NumberDouble:
if (propertyValue is double)
{
newProp.VTFloat = new VTFloat(propertyValue.ToString());
propSet = true;
}
break;
case PropertyTypes.Text:
newProp.VTLPWSTR = new VTLPWSTR(propertyValue.ToString());
propSet = true;
break;
case PropertyTypes.YesNo:
if (propertyValue is bool)
{
// Must be lowercase.
newProp.VTBool = new VTBool(
Convert.ToBoolean(propertyValue).ToString().ToLower());
propSet = true;
}
break;
}
if (!propSet)
{
// If the code could not convert the
// property to a valid value, throw an exception:
throw new InvalidDataException("propertyValue");
}
At this point, if the code has not thrown an exception, you can assume that the property is valid, and the code sets the FormatId and Name properties of the new custom property.
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
newProp.Name = propertyName
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
newProp.Name = propertyName;
Working with the Document
Given the CustomDocumentProperty object, the code next interacts with the document that you supplied in the parameters to the WDSetCustomProperty procedure. The code starts by opening the Word document in read-write mode, using the Open method of the WordProcessingDocument class. The code attempts to retrieve a reference to the custom file properties part, using the CustomFilePropertiesPart property of the document.
Using document = WordprocessingDocument.Open(fileName, True)
Dim customProps = document.CustomFilePropertiesPart
' Code removed here…
End Using
using (var document = WordprocessingDocument.Open(fileName, true))
{
var customProps = document.CustomFilePropertiesPart;
// Code removed here…
}
If the code cannot find a custom properties part, it creates a new part, and adds a new set of properties to the part.
If customProps Is Nothing Then
' No custom properties? Add the part, and the
' collection of properties now.
customProps = document.AddCustomFilePropertiesPart
customProps.Properties = New Properties
End If
if (customProps == null)
{
// No custom properties? Add the part, and the
// collection of properties now.
customProps = document.AddCustomFilePropertiesPart();
customProps.Properties =
new DocumentFormat.OpenXml.CustomProperties.Properties();
}
Next, the code retrieves a reference to the custom properties part's Properties property (that is, a reference to the properties themselves). If the code had to create a new custom properties part, you know that this reference is not null, but for existing custom properties parts, it is possible, although highly unlikely, that the Properties property will be null. If so, the code cannot continue.
Dim props = customProps.Properties
If props IsNot Nothing Then
' Code removed here…
End If
var props = customProps.Properties;
if (props != null)
{
// Code removed here…
}
The next step is difficult to justify. If the property already exists, the code retrieves its current value, and then deletes it. Why delete the property? If the new type for the property matches the existing type for the property, the code could set the value of the property to the new value. On the other hand, if the new type does not match, the code must create a new element, deleting the old one (it is the name of the element that defines its type─for more information, see Figure 1). It can be simpler to always delete and recreate the element. The code uses a simple LINQ query to find the first match for the property name.
Dim prop = props.
Where(Function(p) CType(p, CustomDocumentProperty).
Name.Value = propertyName).FirstOrDefault()
' Does the property exist? If so, get the return value,
' and then delete the property.
If prop IsNot Nothing Then
returnValue = prop.InnerText
prop.Remove()
End If
var prop = props.
Where(p => ((CustomDocumentProperty)p).
Name.Value == propertyName).FirstOrDefault();
// Does the property exist? If so, get the return value,
// and then delete the property.
if (prop != null)
{
returnValue = prop.InnerText;
prop.Remove();
}
Now, you will know for sure that the custom property part exists, a property that has the same name as the new property does not exist, and that there may be other existing custom properties. The code performs the following steps:
Appends the new property as a child of the properties collection.
Loops through all the existing properties, and sets the pid attribute to increasing values, starting at 2.
Saves the part.
props.AppendChild(newProp)
Dim pid As Integer = 2
For Each item As CustomDocumentProperty In props
item.PropertyId = pid
pid += 1
Next
props.Save()
props.AppendChild(newProp);
int pid = 2;
foreach (CustomDocumentProperty item in props)
{
item.PropertyId = pid++;
}
props.Save();
Finally, the code returns the stored original property value.
Return returnValue
return returnValue;
Provide a test document, and run the sample code. Load the modified document in Word 2007 or Word 2010, and view the custom document properties (you can alternatively load the document into the Open XML SDK Productivity Tool and view the part─verify that the results match those shown in Figure 1.
Sample Procedure
The sample procedure includes the following code.
Public Function WDSetCustomProperty( _
ByVal fileName As String, ByVal propertyName As String, _
ByVal propertyValue As Object, ByVal propertyType As PropertyTypes)
As String
Dim returnValue As String = Nothing
Dim newProp As New CustomDocumentProperty
Dim propSet As Boolean = False
' Calculate the correct type:
Select Case propertyType
Case PropertyTypes.DateTime
' Verify that you were passed a real date,
' and if so, format correctly.
' The date/time value passed in should
' represent a UTC date/time.
If TypeOf (propertyValue) Is DateTime Then
newProp.VTFileTime = _
New VTFileTime(String.Format(
"{0:s}Z", Convert.ToDateTime(propertyValue)))
propSet = True
End If
Case PropertyTypes.NumberInteger
If TypeOf (propertyValue) Is Integer Then
newProp.VTInt32 = New VTInt32(propertyValue.ToString())
propSet = True
End If
Case PropertyTypes.NumberDouble
If TypeOf propertyValue Is Double Then
newProp.VTFloat = New VTFloat(propertyValue.ToString())
propSet = True
End If
Case PropertyTypes.Text
newProp.VTLPWSTR = New VTLPWSTR(propertyValue.ToString())
propSet = True
Case PropertyTypes.YesNo
If TypeOf propertyValue Is Boolean Then
' Must be lowercase.
newProp.VTBool = _
New VTBool(Convert.ToBoolean(
propertyValue).ToString().ToLower())
propSet = True
End If
End Select
If Not propSet Then
' If the code could not convert the
' property to a valid value, throw an exception:
Throw New InvalidDataException("propertyValue")
End If
' Now that you have handled the parameters,
' work on the document.
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
newProp.Name = propertyName
Using document = WordprocessingDocument.Open(fileName, True)
Dim customProps = document.CustomFilePropertiesPart
If customProps Is Nothing Then
' No custom properties? Add the part, and the
' collection of properties now.
customProps = document.AddCustomFilePropertiesPart
customProps.Properties = New Properties
End If
Dim props = customProps.Properties
If props IsNot Nothing Then
Dim prop = props.
Where(Function(p) CType(p, CustomDocumentProperty).
Name.Value = propertyName).FirstOrDefault()
' Does the property exist? If so, get the return value,
' and then delete the property.
If prop IsNot Nothing Then
returnValue = prop.InnerText
prop.Remove()
End If
' Append the new property, and
' fix up all the property ID values.
' The PropertyId value must start at 2.
props.AppendChild(newProp)
Dim pid As Integer = 2
For Each item As CustomDocumentProperty In props
item.PropertyId = pid
pid += 1
Next
props.Save()
End If
End Using
Return returnValue
End Function
public static string WDSetCustomProperty(
string fileName, string propertyName,
object propertyValue, PropertyTypes propertyType)
{
string returnValue = null;
var newProp = new CustomDocumentProperty();
bool propSet = false;
// Calculate the correct type:
switch (propertyType)
{
case PropertyTypes.DateTime:
// Verify that you were passed a real date,
// and if so, format in the correct way.
// The date/time value passed in should
// represent a UTC date/time.
if ((propertyValue) is DateTime)
{
newProp.VTFileTime = new VTFileTime(string.Format(
"{0:s}Z", Convert.ToDateTime(propertyValue)));
propSet = true;
}
break;
case PropertyTypes.NumberInteger:
if ((propertyValue) is int)
{
newProp.VTInt32 = new VTInt32(propertyValue.ToString());
propSet = true;
}
break;
case PropertyTypes.NumberDouble:
if (propertyValue is double)
{
newProp.VTFloat = new VTFloat(propertyValue.ToString());
propSet = true;
}
break;
case PropertyTypes.Text:
newProp.VTLPWSTR = new VTLPWSTR(propertyValue.ToString());
propSet = true;
break;
case PropertyTypes.YesNo:
if (propertyValue is bool)
{
// Must be lowercase.
newProp.VTBool = new VTBool(
Convert.ToBoolean(propertyValue).ToString().ToLower());
propSet = true;
}
break;
}
if (!propSet)
{
// If the code could not convert the
// property to a valid value, throw an exception:
throw new InvalidDataException("propertyValue");
}
// Now that you have handled the parameters,
// work on the document.
newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
newProp.Name = propertyName;
using (var document = WordprocessingDocument.Open(fileName, true))
{
var customProps = document.CustomFilePropertiesPart;
if (customProps == null)
{
// No custom properties? Add the part, and the
// collection of properties now.
customProps = document.AddCustomFilePropertiesPart();
customProps.Properties = new DocumentFormat.OpenXml.
CustomProperties.Properties();
}
var props = customProps.Properties;
if (props != null)
{
var prop = props.
Where(p => ((CustomDocumentProperty)p).
Name.Value == propertyName).FirstOrDefault();
// Does the property exist? If so, get the return value,
// and then delete the property.
if (prop != null)
{
returnValue = prop.InnerText;
prop.Remove();
}
// Append the new property, and
// fix all the property ID values.
// The PropertyId value must start at 2.
props.AppendChild(newProp);
int pid = 2;
foreach (CustomDocumentProperty item in props)
{
item.PropertyId = pid++;
}
props.Save();
}
}
return returnValue;
}
}
The code example in this Visual How To includes many of the issues that you will encounter when you work with the Open XML SDK 2.0. Each example is slightly different. However, the basic concepts are the same. Unless you understand the structure of the part you are trying to work with, even the Open XML SDK 2.0 will not make it possible to interact with the part. Take the time to investigate the objects that you are working with before you start to write code─you will save time. |