Saving Custom Document Properties in a Loop - vba

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!

Related

Protect custom properties in word

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..

Is there a way in VBA to iterate through specific objects on a form?

I would like to have a subroutine in VBA that conditionally changes the Enabled property of each of 20+ buttons on a form via iteration rather than code them all by hand. These buttons are named similar to tables that they process. For example: A table to process is called "CUTLIST"; its corresponding button is called "but_CUTLIST". There is another table that holds the list of tables to be processed (used for iteration purposes in other subs).
What I have so far...
Private Sub txt_DataSet_GotFocus()
Dim sqlQry as String
Dim butName As String
Dim tableList As Recordset
Dim tempTable As Recordset
Set tableList = CurrentDb.OpenRecordset("TableList") 'names of tables for user to process
tableList.MoveFirst 'this line was corrected by moving out of the loop
Do Until tableList.EOF
sqlQry = 'SQL query that determines need for the button to be enabled/disabled
Set tempTable = CurrentDb.OpenRecordset(sqlQry)
If tempTable.RecordCount > 0 Then
'begin code that eludes me
butName = "but_" & tableList!tName
Me(butName).Enabled False
'end code that eludes me
End If
tableList.MoveNext
Loop
End Sub
If I remember correctly, JavaScript is capable of calling upon objects through a variable by handling them as elements of the document's object "array." Example: this[objID]=objVal Is such a thing possible with VBA or am I just going about this all wrong?
Viewing other questions... is this what's called "reflection"? If so, then this can't be done in VBA. :(
In case more explanation helps to answer the question better... I have a utility that runs SQL queries against a pre-defined set of tables. Each table has its own button, so that the user may process a query against any of the tables as needed. Depending on circumstances happening to data beforehand, any combination of the tables may need to be queried via pressing of said buttons. Constantly referring to the log, to see what was already done, gets cumbersome after processing several data sets. So, I'd like to have the buttons individually disable themselves if they are not needed for the currently focused data set. I have another idea on how to make that happen, but making this code work would be faster and I would learn something.
I'm not an expert on VBA, but I would re-arrange the code to take advantage of the fact that you can iterate through the control collection in the user form
Something like this:
Dim ctrl as Control
For Each ctrl in UserForm1.Controls
If TypeName(ctrl) = "Button" Then
ctrl.Enabled = True
End If
Next
You can pass the button name to some other function (from this loop) to determine whether the button in question should be enabled / disabled.

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.

Is there a simpler way to process check boxes?

In vb.net, I have a form that has a set of four Check Boxes. Each Check Box signifies that (when checked) the user wants to add a special instruction to their order. The code looks like this:
If SpecialInstruction1CheckBox.Checked Then
AddSpecialInstruction(SPECIAL_INSTRUCTION_1_String)
End If
If SpecialInstruction2CheckBox.Checked Then
AddSpecialInstruction(SPECIAL_INSTRUCTION_2_String)
End If
If SpecialInstruction3CheckBox.Checked Then
AddSpecialInstruction(SPECIAL_INSTRUCTION_3_String)
End If
If SpecialInstruction4CheckBox.Checked Then
AddSpecialInstruction(SPECIAL_INSTRUCTION_4_String)
End If
I have a feeling that this code is unnecessarily verbose, feels repetitive, and could be simplified. How would I go about doing this, or is this not as "wrong" as it feels?
The first problem is that your special instructions should not be stored in separate variables. They should be stored in an array or some other kind of list. Then you could access them by index (e.g. specialInstructions(1)).
Then you can loop through the check boxes by index like this:
For i As Integer = 1 to 4
Dim box As CheckBox = DirectCast(Me.Controls("SpecialInstruction" & i.ToString() & "CheckBox"), CheckBox)
If box.Checked Then list.Add(specialInstructions(i))
Next
Alternatively, you could store references to your check boxes in an array and then loop through them more easily, for instance:
Dim checkBoxes() As CheckBox = {
SpecialInstruction1CheckBox,
SpecialInstruction2CheckBox,
SpecialInstruction3CheckBox,
SpecialInstruction4CheckBox}
' ...
For i As Integer = 0 to checkBoxes.Length - 1
If checkBoxes(i).Checked Then list.Add(specialInstructions(i))
Next
Another option would be to store the special instructions in the Tag property of each check box, then you could just retrieve the value from the control, like this:
For Each i As CheckBox In checkBoxes
If i.Checked Then list.Add(i.Tag)
Next
But that only makes sense if you don't need to reuse those special instructions values elsewhere in your code.
Actually the code isn't that bad in itself. It mainly depends on what AddSpecialInstruction does, exactly. Depending on your specifics it might be better to pass it a list of string instructions instead:
Dim list As New List(Of String)
If SpecialInstruction1CheckBox.Checked Then list.Add(SPECIAL_INSTRUCTION_1_String)
If SpecialInstruction2CheckBox.Checked Then list.Add(SPECIAL_INSTRUCTION_2_String)
If SpecialInstruction3CheckBox.Checked Then list.Add(SPECIAL_INSTRUCTION_3_String)
If SpecialInstruction4CheckBox.Checked Then list.Add(SPECIAL_INSTRUCTION_4_String)
AddSpecialInstructions(list)
Since you also required code shrinking, I made If statements holding on one line. Shorter variable names would help on that too.

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 ;