Hello,
I'm looking to set a single property in a new document using Visual Studio 2017, C# and the current NuGet package for OpenXML.
There are two attempts, one C#, second C# and VB.NET. Bottom line is first attempt sets the property but does not persist after saving/opening via WinWord while the second does not work which I indicate where below.
Followed instructions in the following documentation. In Visual Studio 2017 local window the property Title gets set but not saved.
I did search through this forum yet the two attempts seemed the most logical out of several search pages.
Thanks for any thoughts.
First attempt
Code execution (_DocumentFolder is an existing folder below the executable folder)
public void CreateNewEmptyDocumentWithCustomProperties() { var fileName = Path.Combine(_DocumentFolder, "NewEmptyDocument.docx"); using (var document = WordprocessingDocument.Create(fileName, WordprocessingDocumentType.Document)) { MainDocumentPart mainPart = document.AddMainDocumentPart(); mainPart.Document = new Document(); mainPart.Document.AppendChild(new Body()); mainPart.Document.Save(); } SetCustomProperty(fileName, "Title", "My title", PropertyTypes.Text); }
Code taken from documentation (see link above)
public void CreateNewEmptyDocumentWithCustomProperties() { var fileName = Path.Combine(_DocumentFolder, "NewEmptyDocument.docx"); using (var document = WordprocessingDocument.Create(fileName, WordprocessingDocumentType.Document)) { MainDocumentPart mainPart = document.AddMainDocumentPart(); mainPart.Document = new Document(); mainPart.Document.AppendChild(new Body()); mainPart.Document.Save(); } //var x = new Class1(); //x.WDSetCustomProperty(fileName, "Title", "Writing code"); SetCustomProperty(fileName, "Title", "My title", PropertyTypes.Text); } public static string SetCustomProperty(string pFileName, string pPropertyName, object pPropertyValue, PropertyTypes pPropertyType) { string returnValue = null; var newProp = new CustomDocumentProperty(); bool propSet = false; switch (pPropertyType) { case PropertyTypes.DateTime: // The date/time value passed in should represent a UTC date/time. if ((pPropertyValue) is DateTime) { newProp.VTFileTime = new VTFileTime($"{Convert.ToDateTime(pPropertyValue):s}Z"); propSet = true; } break; case PropertyTypes.NumberInteger: if ((pPropertyValue) is int) { newProp.VTInt32 = new VTInt32(pPropertyValue.ToString()); propSet = true; } break; case PropertyTypes.NumberDouble: if (pPropertyValue is double) { newProp.VTFloat = new VTFloat(pPropertyValue.ToString()); propSet = true; } break; case PropertyTypes.Text: newProp.VTLPWSTR = new VTLPWSTR(pPropertyValue.ToString()); propSet = true; break; case PropertyTypes.YesNo: if (pPropertyValue is bool) { // Must be lowercase. newProp.VTBool = new VTBool(Convert.ToBoolean(pPropertyValue).ToString().ToLower()); propSet = true; } break; } if (!propSet) { // If the code was not able to convert the property to a valid value, throw an exception. throw new InvalidDataException("propertyValue"); } // Now that you have handled the parameters, start working on the document. newProp.FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; newProp.Name = pPropertyName; using (var document = WordprocessingDocument.Open(pFileName, 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) { // This will trigger an exception if the property's Name // property is null, but if that happens, the property is damaged, // and probably should raise an exception. var prop = props.FirstOrDefault(p => ((CustomDocumentProperty) p).Name.Value == pPropertyName); // 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 up all the property ID values. // The PropertyId value must start at 2. props.AppendChild(newProp); var pid = 2; foreach (CustomDocumentProperty item in props) { item.PropertyId = pid++; } props.Save(); //document.Save(); // not in original code sample } return returnValue; } }
Second attempt
Reading from the following post (in VB.NET) I decided to create a VB.NET class project, added it as a reference to the C# project. It's hacked enough to run for testing. On the line Dim node As XmlNode it returns Nothing which in turn bypasses setting the intended property. If this had worked I would rewrite in C# but it does not work.
Code execution
public void CreateNewEmptyDocumentWithCustomProperties() { var fileName = Path.Combine(_DocumentFolder, "NewEmptyDocument.docx"); using (var document = WordprocessingDocument.Create(fileName, WordprocessingDocumentType.Document)) { MainDocumentPart mainPart = document.AddMainDocumentPart(); mainPart.Document = new Document(); mainPart.Document.AppendChild(new Body()); mainPart.Document.Save(); } var x = new Class1(); x.WDSetCustomProperty(fileName, "Title", "Writing code"); }
VB.NET Code
Imports System.IO Imports System.Xml Public Class Class1 Private Enum PropertyTypes YesNo Text DateTime NumberInteger NumberDouble End Enum Public Function WDSetCustomProperty(ByVal docName As String, ByVal propertyName As String, ByVal propertyValue As Object) As Boolean Dim propertyType = PropertyTypes.Text ' Karen being lazy Dim documentRelationshipType As String = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Dim customPropertiesRelationshipType As String = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" Dim customPropertiesSchema As String = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" Dim customVTypesSchema As String = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" Dim retVal As Boolean = False Dim documentPart As System.IO.Packaging.PackagePart = Nothing Dim propertyTypeName As String = "vt:lpwstr" Dim propertyValueString As String = Nothing ' Calculate the correct type. Select Case propertyType Case PropertyTypes.DateTime propertyTypeName = "vt:filetime" ' Make sure 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.GetType() Is GetType(System.DateTime) Then propertyValueString = String.Format("{0:s}Z", Convert.ToDateTime(propertyValue)) End If Case PropertyTypes.NumberInteger propertyTypeName = "vt:i4" If propertyValue.GetType() Is GetType(System.Int32) Then propertyValueString = Convert.ToInt32(propertyValue).ToString() End If Case PropertyTypes.NumberDouble propertyTypeName = "vt:r8" If propertyValue.GetType() Is GetType(System.Double) Then propertyValueString = Convert.ToDouble(propertyValue).ToString() End If Case PropertyTypes.Text propertyTypeName = "vt:lpwstr" propertyValueString = Convert.ToString(propertyValue) Case PropertyTypes.YesNo propertyTypeName = "vt:bool" If propertyValue.GetType() Is GetType(System.Boolean) Then ' Must be lower case! propertyValueString = Convert.ToBoolean(propertyValue).ToString().ToLower() End If End Select If propertyValueString Is Nothing Then ' If the code wasn't able to convert the ' property to a valid value, ' throw an exception: Throw New InvalidDataException("Invalid parameter value.") End If ' Next code block goes here. Using wdPackage As System.IO.Packaging.Package = System.IO.Packaging.Package.Open(docName, FileMode.Open, FileAccess.ReadWrite) ' Get the main document part (document.xml). For Each relationship As System.IO.Packaging.PackageRelationship In wdPackage.GetRelationshipsByType(documentRelationshipType) Dim documentUri As Uri = System.IO.Packaging.PackUriHelper.ResolvePartUri(New Uri("/", UriKind.Relative), relationship.TargetUri) documentPart = wdPackage.GetPart(documentUri) ' There is only one document. Exit For Next ' Work with the custom properties part. Dim customPropsPart As System.IO.Packaging.PackagePart = Nothing ' Get the custom part (custom.xml). ' It may not exist. For Each relationship As System.IO.Packaging.PackageRelationship In wdPackage.GetRelationshipsByType(customPropertiesRelationshipType) Dim documentUri As Uri = System.IO.Packaging.PackUriHelper.ResolvePartUri(New Uri("/", UriKind.Relative), relationship.TargetUri) customPropsPart = wdPackage.GetPart(documentUri) ' There is only one custom properties part, ' if it exists at all. Exit For Next ' Manage namespaces to perform Xml ' XPath queries. Dim nt As New NameTable() Dim nsManager As New XmlNamespaceManager(nt) nsManager.AddNamespace("d", customPropertiesSchema) nsManager.AddNamespace("vt", customVTypesSchema) Dim customPropsUri As New Uri("/docProps/custom.xml", UriKind.Relative) Dim customPropsDoc As XmlDocument = Nothing Dim rootNode As XmlNode = Nothing ' Next code block goes here. If customPropsPart Is Nothing Then customPropsDoc = New XmlDocument(nt) ' Part doesn't exist. Create it now. Try customPropsPart = wdPackage.GetPart(customPropsUri) Catch ex As Exception customPropsPart = wdPackage.CreatePart(customPropsUri, "application/vnd.openxmlformats-officedocument.custom-properties+xml") End Try ' Set up the rudimentary custom part. rootNode = customPropsDoc.CreateElement("Properties", customPropertiesSchema) rootNode.Attributes.Append(customPropsDoc.CreateAttribute("xmlns:vt")) rootNode.Attributes("xmlns:vt").Value = customVTypesSchema customPropsDoc.AppendChild(rootNode) ' Create the document's relationship to _ ' the new custom properties part: wdPackage.CreateRelationship(customPropsUri, System.IO.Packaging.TargetMode.Internal, customPropertiesRelationshipType) Else ' Load the contents of the custom ' properties part into an XML document. customPropsDoc = New XmlDocument(nt) customPropsDoc.Load(customPropsPart.GetStream()) rootNode = customPropsDoc.DocumentElement End If ' Next block goes here. Dim searchString As String = String.Format("d:Properties/d:property[@name='{0}']", propertyName) Dim node As XmlNode = customPropsDoc.SelectSingleNode(searchString, nsManager) Dim valueNode As XmlNode = Nothing If node IsNot Nothing Then ' You found the node. Now check its type: If node.HasChildNodes Then valueNode = node.ChildNodes(0) If valueNode IsNot Nothing Then Dim typeName As String = valueNode.Name If propertyTypeName = typeName Then ' The types are the same. Simply ' replace the value of the node: valueNode.InnerText = propertyValueString ' If the property existed, and ' its type hasn't changed, you're done: retVal = True Else ' Types are different. Delete the node, and clear ' the node variable: node.ParentNode.RemoveChild(node) node = Nothing End If End If End If End If ' Next block goes here. If node Is Nothing Then Dim pidValue As String = "2" Dim propertiesNode As XmlNode = customPropsDoc.DocumentElement If propertiesNode.HasChildNodes Then Dim lastNode As XmlNode = propertiesNode.LastChild If lastNode IsNot Nothing Then Dim pidAttr As XmlAttribute = lastNode.Attributes("pid") If Not pidAttr Is Nothing Then pidValue = pidAttr.Value Dim value As Integer If Integer.TryParse(pidValue, value) Then pidValue = Convert.ToString(value + 1) End If End If End If End If ' Next block goes here. node = customPropsDoc.CreateElement("property", customPropertiesSchema) node.Attributes.Append(customPropsDoc.CreateAttribute("name")) node.Attributes("name").Value = propertyName node.Attributes.Append( customPropsDoc.CreateAttribute("fmtid")) node.Attributes("fmtid").Value = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" node.Attributes.Append( customPropsDoc.CreateAttribute("pid")) node.Attributes("pid").Value = pidValue valueNode = customPropsDoc.CreateElement(propertyTypeName, customVTypesSchema) valueNode.InnerText = propertyValueString node.AppendChild(valueNode) rootNode.AppendChild(node) retVal = True End If ' Save the properties XML back to its part. customPropsDoc.Save(customPropsPart.GetStream(FileMode.Create, FileAccess.Write)) wdPackage.Close() End Using Return retVal End Function End Class
Please remember to mark the replies as answers if they help and unmark them if they provide no help, this will help others who are looking for solutions to the same or similar problem. Contact via my Twitter (Karen Payne) or Facebook (Karen Payne) via my
MSDN profile but will not answer coding question on either.
VB Forums - moderator