Quantcast
Channel: Open XML Format SDK forum
Viewing all articles
Browse latest Browse all 1288

Word document set document property

$
0
0

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
profile for Karen Payne on Stack Exchange, a network of free, community-driven Q&A sites


Viewing all articles
Browse latest Browse all 1288

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>