Protect custom properties in word - vba

We have several templates in Word2016 which uses custom variables, these variables should be updated with my macro, so that data gets pushed to the Database when the user changes them.
Unfortunately we have some users that deletes the variables in the documents(in the text not in the file properties) which means that the database does not get updated. Is there a setting to make custom properties not able to be deleted from the text?
Properties for one example document are listed below
This is how it should look like in the document
Then this is what happens sometimes, which should not be allowed
By doing so I do not need to loop through document to find variables, because I can simply loop through custom properties:
Public Sub initializeVariablesFromDB(doc As Document, dokID As String)
Dim docProp As Object
Dim rowNumber As Integer
'Get valid properties
If CPearson.IsArrayEmpty(Settings.validPropertiesArray) Then
Settings.validPropertiesArray = Post.helpRequest("xxxxxx?Dok2=1")
End If
'We create the docid just in case....
Call CustomProperties.createCustomDocumentProperty(doc, "_DocumentID", dokID)
'We loop through all custom properties
For Each docProp In doc.CustomDocumentProperties
rowNumber = CPearson.findRownumber(docProp.name, Settings.validPropertiesArray)
If rowNumber <> -1 Then
'we clear all SIGN properties...
If InStr(UCase(docProp.name), "SIGN") > 0 Then
docProp.value = ""
End If
'We check if we should use value from DB
If Settings.validPropertiesArray(rowNumber, 1) = "0" Or InStr(UCase(docProp.name), "DOCUMENTCREATEDDATE") > 0 _
Or InStr(UCase(docProp.name), "DOCUMENTCREATOR") > 0 Or InStr(UCase(docProp.name), "DOCUMENTNAME") > 0 Then
'Update from DB
Call Post.updateDBFromCustomProperties(docId:=dokID, PropertyName:=docProp.name, whoRules:="dBrules", doc:=doc)
End If
End If
Next docProp
End Sub
This will fail for the _DocumentSubject in the text, the new value to the property will be collected but is not reflected in the text anymore because the user deleted the variable in the text, can I prevent this?

You can put the field into a Content Control and lock the content control. Users who know enough about MS Word can still delete the content control but they would need to take some fairly deliberate steps to do so (and I'm guessing those people are not the problem).
Normal fields, including document properties, can be added to content controls (probably best to use rich text).
You can put the field into a Content Control, seems like you can not do that :( – skatun 10 hours ago
You can totally do it, follow these steps (Word 2010):
Add a custom property to a new blank (even unsaved) document _SlowLearner
Add a RichText content control from the DeveloperTab
Click inside the content control
Insert > Quick Parts > Field
Filter fields Document Information > select DocProperty
Select property: _SlowLearner > OK
Note: the field is protected when the Content Control is fully locked. This has the curious side effect that the Content Control does NOT allow the field to update normally... to get around this you need to add some more code to your VBA, like this:
Sub UpdateProtectedField()
With ActiveDocument.ContentControls(1)
.LockContents = False
.Range.Fields(1).Update
.LockContents = True
End With
End Sub

One possibility is to use continuous sections breaks and protect sections, not an ideal solution because then images can not be formatted and so on..

Related

Saving Custom Document Properties in a Loop

I'm trying to save the values of data that have been input into my form. There are a total of about 50 different fields to save across 5 different agents, so I loaded the data into arrays.
I've tried saving the fields in a loop, but it doesn't seem to work in a loop, only if each field has a separate line, which is a lot of code and messy. The Ag1Name, Ag2Name and Ag3Name are the names of my textboxes that the user enters to populate the form.
Sub LoadAndSaveData()
NumberofAgents = 3
Dim AgentName(3) as String
AgentName(1) = Ag1Name.Value
AgentName(2) = Ag2Name.Value
AgentName(3) = Ag3Name.Value
For Count = 1 To NumberOfAgents
With ActiveDocument.CustomDocumentProperties
.Add Name:="AgentName" & Count, LinkToContent:=False, Value:=AgentName(Count), Type:=msoPropertyTypeString
End With
Next Count
End Sub
The data doesn't get saved to the Custom Document Properties when the code is set up in a loop like the above. Since there are so many values to save and all the data is already in arrays, I would much prefer to use a loop rather than write out a separate line of code for all ~50 of the values. It does seem to work when each field is saved in a separate line of code.
I think this would probably get what you want. You don't really need to count the document properties first, only increment with the ones you want to update. Hopefully the only document properties you want contain the name AgentName in it.
ReDim AgentName(0) As String
Dim P As Long
For Each c In ThisDocument.CustomDocumentProperties
If InStr(1, c.Name, "AgentName", vbTextCompare) > 0 Then
ReDim Preserve AgentName(P)
AgentName(P) = c.Value
P = P + 1
End If
Next c
As a guest I cannot post a comment here, but the code you gave works OK here.
However, there is a problem with creating legacy custom document properties programmatically, because doing that does not mark the document as "changed". When you close the document, Word does not necessarily save it and you lose the Properties and their values.
However, if you actually open up the Custom Document Property dialog, Word does then mark the document as "changed" and the Properties are saved.
So it is possible that the difference between your two scenarios is not the code, but that in one scenario you have actually opened the dialog box to check the values before closing the document and in the other you have not.
If that is the case, here, I was able to change this behaviour by adding the line
ActiveDocument.Saved = False
after setting the property values.
If you do not actually need the values to be Document Properties, it might be better either to use Document Variables, which are slightly easier to use since you can add them and modify them with exactly the same code, or perhaps by storing them in A Custom XML Part, which is harder work but can be useful if you need to extract the values somewhere where Word is not available.
You can make this even easier by looping the controls on the UserForm, testing whether the control name contains "Ag" and, if it does, create the Custom Document Property with the control's value - all in one step.
For example, the following code sample loops the controls in the UserForm. It tests whether the controls Name starts with "Ag". If it does, the CustomDocumentProperty is added with that control's value.
Sub LoadAndSaveData()
Dim ctl As MSForms.control
Dim controlName As String
For Each ctl In Me.Controls
controlName = ctl.Name
If Left(controlName, 2) = "Ag" Then
With ActiveDocument.CustomDocumentProperties
.Add Name:=controlName, LinkToContent:=False, value:=ctl.value, Type:=msoPropertyTypeString
End With
End If
Next
End Sub
I feel a little stupid... I just realized that the reason that the code wasn't working was that the variable NumberofAgents was not being calculated correctly elsewhere in my code. I've got it working now. Thanks for your thoughts!

How do you save data input into a macro so that the input form can be populated with the data next time it runs?

I need to save data that was input into a Microsoft Word form in VBA after the macro terminates. I have a form that has about 40 fields with names, addresses, phone numbers etc and I don't want the user to have to input everything again in case they realize that they made a mistake and need to change 1 item. Currently, the macro deletes all the data that was input into the form when the form closes.
I've looked around on forums and Google but haven't had any luck.
This code brings up my input form from a word document with a command button:
Private Sub CommandButton1_Click()
InputForm.Show vbModeless
End Sub
This code closes the form:
Private Sub CloseForm_Click()
Dim Closing As Integer
Closing = MsgBox("This will exit the form and erase all the data that has been input. You may want to review the documents to ensure they were generated correctly before closing the form. Click Yes to proceed to close the form or No to go back to the form.", vbYesNo + vbQuestion, "Exit Confirmation")
If Closing = vbYes Then
Unload InputForm 'This closes the Input Form and returns the user to Word
End If
End Sub
The macro does not save the information input into the form. Whenever it closes, all the information is erased; I want it saved.
Options for saving data in the document
Document.Variables This has been available in Word since the very early days and is available in all versions since Word 2.0 (early 1990s). It's simply a place to store any number of strings, associated with an identifier (name). In essence, a key-value pair. Important to note: It must be a pair, there cannot be a key without a value and a value cannot be a zero-length string. Assigning a value to a name creates the Variable; deleting the value removes the Variable. These are not visible to the user.
Example:
Document.Variables("Name").Value = "John Doe"
Document.CustomDocumentProperties Similar to Variables but data-types other than strings are available. There is a limit to the length of the data that can be stored. They are visible to the user through the "Properties" interface. They must be explicitly created / destroyed. This was introduced in, I believe, Word 97.
Example:
Document.CustomDocumentProperties.Add(Name As String, LinkToContent As Boolean, _
[Type], [Value], [LinkSource])
CustomXMLParts These are XML "pages" saved in the docx zip package. This is a good way to save data, and it's easily accessible from the closed file. The capability was introduced in Word 2010, so won't be available in older versions nor in doc file formats, only docx.
The coding is more complex than for Variables or Properties.
The usual way to work with this in the described scenario is to create the CustomXMLPart in the document (or template - documents created from the template should carry over the CXP). Then all the code needs to do is read/write from the xml nodes already available. If the VBA coder has no background in XML parsing there will be a steep learning curve.
There are many ways to go about reading and writing, here's a snippet that demonstrates the basics of reading, to give an idea of what's involved:
Sub ReadCXP()
Dim cxp As Office.CustomXMLPart, CXPs As Office.CustomXMLParts
Dim doc As Word.Document
Dim sUri As String
Set doc = ActiveDocument
'object, not collection, because ID is unique and can return only one
'ID is unique to each CustomXMLPart - this is won't work as it stands!
Set cxp = doc.CustomXMLParts.SelectByID("{32B20BB8-F4FB-47D6-9DF4-FFAF5A1D8C18}")
Dim xmlNds As Office.CustomXMLNodes
Dim xmlNd As Office.CustomXMLNode
Set xmlNds = cxp.SelectSingleNode("trees").ChildNodes
For Each xmlNd In xmlNds
Debug.Print xmlNd.XML
Next
End Sub

Populating a text box with some text depending on values of dropdowns selected in document

I am making a document in Microsoft Word 2016 and I would like this to be a form that a user will fill out (using Dropdown list content control)- from there I have assigned all of the items in the list with numbers for their values. I need to populate a text box with some words (determined by the sum of the values) and I am having trouble. Never used VBA- I don't even know if my first line is correct. I'm really not sure how to begin the document and how to populate the sum of all the dropdowns. I'm not using macros from another document, I just want to populate based upon what users select in the word document. I named my field with a tag on a text box that I want the text "account" "account 2" etc to appear in. Thanks!
Set myField = Selection.FormFields(1)
If myField.Type = wdFieldFormDropDown Then
Num = myField.DropDown.ListEntries.Count
If Num >= 75 Then
myField.Value = "Account 1"
End If
If Num > 50 Then
myField.Value = "Account 2"
End If
If Num <= 50 Then
myField.Value = "Account 3"
End If
End If
End Sub
If you plan to use Content Controls then FormField object in your code is not correct. Form fields are "legacy" - they're still quite useful, but it's a different part of the object model.
Content Controls are "embedded" in the document and have events to trigger code. That means you need to double-click the ThisDocument entry in the Project for the parent document in the VB Editor. Then select "Document" from the dropdown to the left of the Code Window and "ContentControlOnExit" from the dropdown on the right. That will insert the event stub in the code window for ThisDocument (see picture).
A Content Control event will trigger for all content controls in the document, which means you need to distinguish which content control triggered it. The event passes in a ContentControl object which can be tested for this purpose, usually using Select Case.
The sample code shows how to do this, how to get the count of the ListEntries, how to select a specific content control by its title (or by its tag) and how to write content to the content control. (Since what you want to do with Num in your code, above, is not clear to me I don't try to include that. But it looks like here, as well, you could use Select Case.)
Private Sub Document_ContentControlOnExit(ByVal ContentControl As ContentControl, _
Cancel As Boolean)
Dim cc As Word.ContentControl
Dim doc As Word.Document
Dim countListEntries As Long
Set doc = ContentControl.Parent
Select Case ContentControl.Title
Case "SelectAccount"
countListEntries = ContentControl.DropdownListEntries.Count
Set cc = doc.SelectContentControlsByTitle("Account").Item(1)
'Set cc =doc.SelectContentControlsByTag("Account").Item(1)
cc.Range.Text = ContentControl.Range.Text & " " & countListEntries
Case Else
End Select
End Sub

How to find and disable a content control by tag to delete it and its contents?

I have the unfortunate task of being forced to design a Word-based electronic production card for the unit at my company, even though I've never worked with VBA. I would much rather have done this in Excel since I wouldn't have to wrestle with content control and hard-to-find locations in various tables over the pages, but the company's documentation-system forces this particular one to be in Word.
My issue is that for proper form of the production card I need to use tables, and I need the production card to be dynamic to limit its size to what operations that are relevant for a specific order. My chosen solution is to create a full form, and to use a user form/prompt where they can choose which parts to use and which parts to ommit, and the ommitted ones will then be deleted. Part of reason for the solution is because that is how their previous (and Excel-based) production card works, so it would make it more familiar for the end user.
Because MS Word is finicky I need to use content control within these tables to not have the end user accidentally destroy half of it, but after a full workday I still cannot figure out how find and shut off the content control of the cells in tables that I want to delete. I do have the content controls tagged since that seems like the only reasonable way to find them.
This is my current code for the subprocedure, but for some reason I cannot get the ID through the ccID line, even though I have verified that the string supplied as argument is correct.
Private Sub DeleteCCByTag(ccTag As String)
Dim cc As ContentControl
Dim ccID As String
ccID = ThisDocument.SelectContentControlsByTag(ccTag).Item(1).ID
'MsgBox ccID 'Debug prompt
Set cc = ThisDocument.ContentControls(ccID)
cc.LockContentControl = False
cc.LockContents = False
cc.Delete (False)
End Sub
First of all- your code is working find for me but...
ContentControls tag is case-sensitive which could be a problem in your situation
You could solve your problem without searching for ID value in this way:
Private Sub DeleteCCByTag_Alternative(ccTag As String)
Dim cc As ContentControl
Set cc = ThisDocument.SelectContentControlsByTag(ccTag).Item(1)
With cc
.LockContentControl = False
.LockContents = False
.Range.Delete 'to delete CC content
.Delete (False)
End With
End Sub
CC.Delete in your code deletes only ContentControl objects itself but not its content. To delete content you need to add additional line which I did in my code above.
I would add this as a comment to KazJaw's answer but I don't have the rep.
According to Microsoft's documentation, if you pass True to the Delete method it removes both the content control and its contents.
So: just get the Item as KazJaw showed, without jumping through the hoop of getting its ID:
Set cc = ThisDocument.SelectContentControlsByTag(ccTag).Item(1)
then call .Delete(True) on it.

Identify and populate a listbox

It's a riddle for me: what is the syntax to populate a listbox? But first: how do you identify a listbox? In many forums I read: ListBox1.Additem... But how do they know it's 'ListBox1'?
That's the default name for a ListBox control when you add it to your form. VB and VBA automatically name new or unnamed controls with the name of the type of control, suffixed with an integer identifier.
It's completely irrelevant what your control is called. The point of the sample code is to demonstrate a concept. You should replace ListBox1 with whatever your control is named.
And you should definitely name your controls something other than the default, because as you observe here, it's not very descriptive.
It used to be recommended by everyone to name controls following some type of quasi-Hungarian notation with a 3-letter prefix indicating the type of control, followed by your regular descriptive name. Over the past few years, there's been a big backlash against anything that looks like Hungarian notation, but I think it's still quite useful with regards to naming GUI controls, so I still use it. For a ListBox control, I might call it something like: lstCustomers or lstItemsForSale. But it's completely optional: again, what you choose to name your controls is irrelevant to how the code works or how the application will behave.
So, to populate a listbox in VB 6 and/or VBA, you'd use the following code, where myListBox is the name of your ListBox control:
' Add an item to the end of the list
myListBox.AddItem "Peaches"
' Insert an item at the top of the list
myListBox.AddItem "Apples", 0
' Remove the first item in the list
myListBox.RemoveItem 0
ListBox1.AddItem is for loading a single column ListBox (CodyGrey's answer covers that).
If you are using a multi column ListBox (property .ColumnCount > 1), then you need to load an Array into .List. The following snippet loads 3 rows into a 2 column ListBox
Dim dat(0 To 2, 0 To 1) As Variant
dat(0, 0) = "A"
dat(0, 1) = "B"
dat(1, 0) = "C"
dat(1, 1) = "D"
dat(2, 0) = "E"
dat(2, 1) = "F"
ListBox1.List = dat
Accessing the List Box: (this will vary for different versions of Word, this is for 2003)
To access the ListBox properties:
Display the "Control Toolbox" toolbar
Go To Design Mode
Click the control, and select Properties on the "Control Toolbox"
The Name property should now be visible and editable
Other properties, such as ColumnCount etc can also be set
When using VBA in Access, a good way to fill in the whole listbox at once is to use the RowSource property when the ListBox is a ValueList. The AddItem method is a bit slow when adding many entries.
For example, to fill in a list of files from a directory, you could use:
Dim src As String
Dim S as String
S = Dir(FullPathToCurrentDirectory & "\*.*")
While Len(S) <> 0
src = src & """" & S & """;"
S = Dir
Wend
If Len(src) = 0 Then src = """"";"
Me.lstFiles.RowSource = Left(src, Len(src) - 1) ' leave off the last ;