none
Problem with Office 2010 customProperties RRS feed

  • Question

  • Hi

    In my Open XML project I have a function with which customproperties to a document is added/changed. If the property is missing, it is added to the document with its value.

    I found the function (below) in one of the HowTo's forums here on MSDN. (http://msdn.microsoft.com/en-us/library/bb308936(office.12).aspx). This was for Office 2007, but I assumed there was no change, because no information for Office 2010 was found.

    Public Enum PropertyTypes
            YesNo
            Text
            DateTime
            NumberInteger
            NumberDouble
    End Enum
    
    Public Function WDSetCustomProperty(ByVal docName As String, ByVal propertyName As String, ByVal propertyValue As Object, ByVal propertyType As PropertyTypes) As Boolean
    
            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
    

    Now when I use this function to set a value of Space(1) or " ", the final document propertyvalue consists of a LineFeed (Chr(10) ) and 4 spaces.

    The call to the function looks like this:
    WDSetCustomProperty("C:\Temp\Testdoc.docx", TestPropName, Space(1), PropertyTypes.Text)

    Whats can be wrong with the function?


    Best Regards Peter Karlström Midrange AB, Sweden

    Tuesday, January 29, 2013 4:13 PM

Answers

  • Hi Cindy

    I do understand and I also appreciate your support very much.

    However, Microsoft force me to rewrite my entire application because of non-supportive automation. This is also time critical since my customer are upgrading servers as we speak.

    We can argue about lack of planing and so on, but my facts are that I have very little time to rewrite the whole application, and Microsoft support (from the ones that gets paid) are sometimes hard to find in the "information djungle" in MSDN.

    After som more digging I found a complete, accurate and up to date funtion which handles my needs.
    For other users in the same situation, heres the VB-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


    Best Regards Peter Karlström Midrange AB, Sweden

    Wednesday, January 30, 2013 1:41 PM

All replies

  • Hi Peter

    The code you found is old, before the full Open XML SDK (2.0) was released. Here's a more "modern" version. It doesn't cover all the data types (time being a factor, here), but all the ones that can be assigned in the Office UI, plus a few more. I'm not sure, but I think you may be assigning the wrong property type in your code...

    This code sample writes information to a text box control on a Windows Form.

    private void btnListCustomDocProps_Click(object sender, EventArgs e)
    {
        string sourcePath = @"C:\Test\TestDocProps.docx";
    
        using (WordprocessingDocument sourcePkg = WordprocessingDocument.Open(sourcePath, true))
        {
            CustomFilePropertiesPart customPropsPart = sourcePkg.GetPartsOfType<CustomFilePropertiesPart>().FirstOrDefault();
            IEnumerable<DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty> customProps = customPropsPart.Properties.Descendants<DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty>();
            foreach (DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty customProp in customProps)
            {
                string propVal = null;
                string propType = string.Empty;
                if (customProp.VTBool != null)
                {
                    //document property contains a boolean value
                    propVal = customProp.VTBool.InnerText;
                    propType = "boolean";
                }
                if (customProp.VTLPWSTR != null)
                {
                    //document property contains a string value
                    propVal = customProp.VTLPWSTR.InnerText;
                    propType = "string";
                    customProp.VTLPWSTR.Text = " "; //change the value
                 }
                if (customProp.VTDate != null)
                {
                    //document property contains a date value
                    propVal = customProp.VTDate.InnerText;
                    propType = "date";
                }
                if (customProp.VTInteger != null)
                {
                    //document property contains an integer value
                    propVal = customProp.VTInteger.InnerText;
                    propType = "integer";
                }
                if (customProp.VTDecimal != null)
                {
                    //document property contains a decimal value
                    propVal = customProp.VTDecimal.InnerText;
                    propType = "decimal";
                }
                if (customProp.VTInt32 != null)
                {
                    //document property contains a Int32 value
                    propVal = customProp.VTInt32.InnerText;
                    propType = "integer 32";
                }
    
                this.txtMessages.Text += "fmtId: " + customProp.FormatId +
                    ", Name: " + customProp.Name + ", propId: " + customProp.PropertyId +
                    ", data type: " + propType + ", string content: " + propVal+Environment.NewLine;
            }
            customPropsPart.Properties.Save();
    
        }
    }


    Cindy Meister, VSTO/Word MVP, my blog

    Tuesday, January 29, 2013 7:50 PM
    Moderator
  • Hi Cindy

    Thanks for the sample code.

    However your code doesn't work on a document which has no custom properties.
    I recieve an error on the line:

    Dim customProps As IEnumerable(Of DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty) = customPropsPart.Properties.Descendants(Of DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty)()

    Also, as far as I have tested, it only handles properties that already exists in the document.

    My funtion must create the customPropertyPart if it doesn't exist, and also add a new property if it's missing in the document. All properties in my project are of type string or integer.

    Thanks in advance


    Best Regards Peter Karlström Midrange AB, Sweden

    Tuesday, January 29, 2013 9:58 PM
  • Hi Peter

    This is pretty much the same as with Document Variables and anything else. I've given you a number of code samples that demonstrate how to check whether something already exists and how to create it, if it does not. You might try it, yourself, once, so that you become more independent of the assistance of others (who by the way aren't being paid for the hours they're investing, here)?


    Cindy Meister, VSTO/Word MVP, my blog

    Wednesday, January 30, 2013 7:41 AM
    Moderator
  • Hi Cindy

    I do understand and I also appreciate your support very much.

    However, Microsoft force me to rewrite my entire application because of non-supportive automation. This is also time critical since my customer are upgrading servers as we speak.

    We can argue about lack of planing and so on, but my facts are that I have very little time to rewrite the whole application, and Microsoft support (from the ones that gets paid) are sometimes hard to find in the "information djungle" in MSDN.

    After som more digging I found a complete, accurate and up to date funtion which handles my needs.
    For other users in the same situation, heres the VB-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


    Best Regards Peter Karlström Midrange AB, Sweden

    Wednesday, January 30, 2013 1:41 PM