April 6th, 2010

Hey, Scripting Guy! How Can I Add Custom Properties to a Microsoft Word Document?

Bookmark and Share

  Hey, Scripting Guy! Question

Hey, Scripting Guy! A few months ago, you wrote a really cool Hey, Scripting Guy! post, How Can I Retrieve the Custom Properties of a Microsoft Word Document. The post illustrated how to use Windows PowerShell to read the custom properties from Microsoft Word documents. You, however, did not show how to modify the value of custom properties. I am wondering if you would care to tackle that task now?

— JB

 

Hey, Scripting Guy! AnswerHello JB,

Microsoft Scripting Guy Ed Wilson here. I am sipping a cup of English Breakfast tea with a little lemon grass and a stick of cinnamon in it. I was able to score some Tim Tams from my friend Brent in Sydney recently while he was in the United States for the Microsoft TechReady conference in Seattle, Washington, so things are progressing nicely this morning. I have Cream cranked up so loud on my Zune HD that I can feel the energetic Jack Bruce bass licks deep inside my bones, and therefore I am sipping, munching, and grooving as I review the e-mail sent to the scripter@microsoft.com address. I have no idea where the Scripting Wife is right now. She evacuated the upstairs about the time that Sunshine of Your Love came on, and she was long gone by the time Badge was playing. She took her laptop with her and muttered something about me being “impossible at times”—as if she weren’t alive in the 1970s or something.

Anyway, as I was reviewing the e-mail inbox, I ran across Jit’s message. I knew it was Jit because I recognized his e-mail address. I first met Jit several years ago in Brisbane, Australia, when I went there to teach a VBScript class that was based upon one of my VBScript books. He was the Microsoft guy that I was to train to deliver my class and I got to work with him for two weeks—one week in Brisbane and one in Sydney. We have since become great friends. Thinking of Jit reminded me of some of the pictures I took during that trip, such as the following one that was taken in downtown Brisbane.

Photo Ed took in Brisbane, Australia

 

JB, I decided to write the Set-WordCustomProperties.ps1 script for you to allow you to set custom properties on a Microsoft Word document. You should refer to the Hey, Scripting Guy! post you mentioned in your e-mail message for information about working with custom properties in Microsoft Word, as well as for additional information about working with the InvokeMember method. The complete Set-WordCustomProperties.ps1 script is seen here.

Set-WordCustomProperties.ps1

$path = “C:fsoTest.docx”
$application = New-Object -ComObject word.application
$application.Visible = $false
$document = $application.documents.open($path)
$binding = “System.Reflection.BindingFlags” -as [type]

$customProperties = $document.CustomDocumentProperties
$typeCustomProperties = $customProperties.GetType()

$CustomProperty = “Client”
$Value = “My_WayCool_Client”
[array]$arrayArgs = $CustomProperty,$false, 4, $Value

Try
 {
  $typeCustomProperties.InvokeMember(`
    “add”, $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
    out-null
 }
Catch [system.exception]
 {
  $propertyObject = $typeCustomProperties.InvokeMember(`
    “Item”, $binding::GetProperty,$null,$customProperties,$CustomProperty)
  $typeCustomProperties.InvokeMember(`
    “Delete”, $binding::InvokeMethod,$null,$propertyObject,$null)
  $typeCustomProperties.InvokeMember(`
    “add”, $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
    Out-Null
 }
 
$document.Saved = $false
$document.save()
$application.quit()
$application = $null
[gc]::collect()
[gc]::WaitForPendingFinalizers()

JB, the first thing to do is to assign the path to the document. In reality, this could be a Get-ChildItem cmdlet that filters all of the document files, in the same manner that was used in yesterday’s Excel article. The reason I left that part out of the script is that the script was already rather complicated, and I did not want to clutter it up anymore than it already was:

$path = “C:fsoTest.docx”

When working with Microsoft Word, it is generally a requirement to create an instance of the Word.Application COM object. When the Word.Application object is stored in the $application variable, the visible property is set to false to keep the Word document from displaying on the screen. This is shown here:

$application = New-Object -ComObject word.application
$application.Visible = $false

It is time to open the Microsoft Word document, and to store the returned Document object in the $document variable. This is seen here:

$document = $application.documents.open($path)

Because of the way that the COM interop works, the BindingFlags class from the System.Reflection namespace needs to be created as a type. This is stored in the $binding variable as shown here. The BindingFlags class will be used several times in the Set-WordCustomProperties.ps1 script:

$binding = “System.Reflection.BindingFlags” -as [type]

Obtain the CustomDocumentProperties collection by calling the CustomDocumentProperties property. The object is stored in the $customProperties variable:

$customProperties = $document.CustomDocumentProperties

It is time to retrieve the CustomDocumentProperties type by using the GetType method. The CustomDocumentProperties type is stored in the $typeCustomProperties variable. This type will be used later on in the script:

$typeCustomProperties = $customProperties.GetType()

Next, an array with the custom property name and the value of the custom property is created. The $arrayArgs variable stores this information. When creating a custom document property, the add method requires four or five different values. The first is the name of the property, and the second specifies whether the property is linked to the contents of the document. If this is set to $true, the LinkSource value must also be specified. If instead it is $false, you must specify the value for the property. The third position is a MsoDocProperties constant value. Rather than creating the actual enumeration, I used the direct value of 4, which is a msoPropertyTypeString value. The following table lists the MsoDocProperties enumerations and the enumeration values.

Name

Value

Description

msoPropertyTypeBoolean

2

Boolean value

msoPropertyTypeDate

3

Date value

msoPropertyTypeFloat

5

Floating point value

msoPropertyTypeNumber

1

Integer value

msoPropertyTypeString

4

String value

The LinkSource, in the fifth position, would point to the source of the linked property if the LinkToContent position was equal to $true. This section of the script is shown here:

$CustomProperty = “Client”
$Value = “My_WayCool_Client”
[array]$arrayArgs = $CustomProperty,$false, 4, $Value

If the custom document property already exists and an attempt is made to write to the property, an error will be generated. To prevent the error from occurring, use the Try/Catch construction. In the Try block, the InvokeMember method from the CustomDocumentProperties type is used to add the property to the CustomDocumentProperties collection. If the custom property does not exist, no error is generated, and the property will be added to CustomDocumentProperties collection. Following the Add method, the InvokeMethod static property is used from the Binding Flags type. The CustomDocumentProperties object is passed, as well as the array containing the information required by the add method. This is rather complicated and is a requirement because of the way the COM interop model works. The returned system.object is piped to the Out-Null cmdlet to keep from cluttering up the Windows PowerShell console. The Try section of the script is shown here. The line continuation character (`) is used to allow the code block to display properly on our Web page. Normally I would type the InvokeMember command on a single line.

Try
 {
  $typeCustomProperties.InvokeMember(`
    “add”, $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
    out-null
 }

If the custom property already exists and an error is generated, control of the script moves to the Catch block. The first thing that must be done inside the Catch block is to retrieve the specific CustomDocumentProperty object that caused the error and store it in the $propertyObject variable. To do this, the InvokeMember method is once again called. This time, the Item method is used, and the GetProperty binding flag is specified. After the specific CustomDocumentProperty object has been retrieved, the InvokeMember method is called, and this time the Delete method is specified. The InvokeMethod binding is used along with the CustomDocumentProperty object that was stored in the $propertyObject variable. This section of the script is shown here:

Catch [system.exception]
 {
  $propertyObject = $typeCustomProperties.InvokeMember(`
    “Item”, $binding::GetProperty,$null,$customProperties,$CustomProperty)
  $typeCustomProperties.InvokeMember(`
    “Delete”, $binding::InvokeMethod,$null,$propertyObject,$null)

After the previous Custom Document Property has been deleted, the new value for the Custom Document Property can be added. This code is exactly the same that was used in the initial Try block:

  $typeCustomProperties.InvokeMember(`
    “add”, $binding::InvokeMethod,$null,$customProperties,$arrayArgs) |
    Out-Null
 }

When a Custom Document property is added via the Word automation model, the Document object saved property is not toggled. Therefore, calling the Save method from the Document object does not save the newly added Custom Document property. This behavior is by design. Therefore, to be able to save the newly created Custom Document property, it is necessary to first set the saved property of the Document object to $false, and then call the Save method from the Document object.

Yesterday, we toggled the Saved property of the Excel Workbook object to prevent the Save dialog box from appearing when we were retrieving linked worksheets.

This is shown here:

$document.Saved = $false
$document.save()

After all the custom properties have been added, it is time to call the quit method from the Word Application object. It would also be a good idea to call garbage collection to force removal of the COM objects from memory. This technique was also discussed in yesterday’s Hey, Scripting Guy! post. This section of the code is shown here:

Author

0 comments

Discussion are closed.