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

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.

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

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.

Openform action was canceled in MS-Access VBA code

I am supporting an application which was running for the past 3 years. It was developed completely in MS Access and written in VBA.
Suddenly the application is facing the mentioned error at the following lines:
DoCmd.OpenForm FormName:="frmNewPeerGroup", View:=acNormal, windowmode:=acWindowNormal, OpenArgs:=5
FrmNewPeerGroup code
Private Sub Form_Open(Cancel As Integer)
Dim lDept As Long, lDiv As Long
lType = OpenArgs 'Supplied by caller
lAssmtVer = 1 'Current
sName = ""
sDescription = ""
dtCreatedDate = Format(Now(), "dd/mm/yyyy")
sCreatedBy = UCase(userPerms.NTLoginName)
lSupervisorID = userPerms.userID
lTeam = 0
With cmbBxType
.RowSourceType = "Value List"
.RowSource = GetValueListDict(pgType)
.Value = lType
.Enabled = (OpenArgs = 1)
End With
With cmbBxVersion
.RowSourceType = "Value List"
.RowSource = GetValueListDict(pgAssmtType)
.Value = lAssmtVer
End With
mgLogoDesc.Visible = False
txtBxCreatedDate.Value = dtCreatedDate
txtBxCreatedBy.Value = sCreatedBy
If OpenArgs = 5 Then
lTeam = oActiveAssmt.TeamID
lDept = GetParentID(aTeams(), CInt(lTeam))
lDiv = GetParentID(aDepts(), CInt(lDept))
With cmbBxDivision
.RowSourceType = "Value List"
.RowSource = GetValueListArray(aDivs())
.Value = lDiv
.Enabled = False
End With
With cmbBxDepartment
.RowSourceType = "Value List"
.RowSource = GetValueListArray(aDepts())
.Value = lDept
.Enabled = False
End With
With cmbBxTeam
.RowSourceType = "Value List"
.RowSource = GetValueListArray(aTeams())
.Value = lTeam
.Enabled = False
End With
Else
With cmbBxDivision
.RowSourceType = "Value List"
.RowSource = GetValueListArray(aDivs())
.Enabled = False
End With
cmbBxDepartment.Enabled = False
cmbBxTeam.Enabled = False
End If
End Sub
Many instances of the DoCmd.OpenForm command are giving the error in a message box saying:
The expression On Click you entered as the event property setting
produced the following error: The OpenForm action was canceled.
- The expression may not result in the name of macro, the name of
a user-defined function, or [Event procedure].
- There may have been an error evaluating the function, event, or macro.
This is the error message I am receiving.
My problem is, the same code was running around 3 years, but suddenly some updates to Microsoft or Office might be giving trouble to this code.
Did anyone come across this error in the past weeks? Please let me know what else we can do to make this work again.
This thread is very old but I came across the same error and spent a few hours looking for an answer. I was able to find the cause after some time and thought of posting my answer as it may help someone in a similar situation. Creating a application using Access Forms is new to me, so the error message was not directly intuitive.
My forms were Master table data entry forms and configured to be Pop-up and Modal with Me.Form.Name sent as parameter in the DoCmd.OpenForm command using a button (OnClick event) placed next to the Combo controls on a transaction form to allow user to quickly add new records. This parameter value was picked up in the Form_Open(Cancel As Integer) event and used later to refresh the combo box (Forms!<formname>.Controls!<controlname>.Requery) once data was submitted to the master table using the pop-up form.
It appears that the Open event doesn't occur when you activate a form that's already open (ref: https://msdn.microsoft.com/en-us/library/office/aa211439(v=office.11).aspx). Each time I received the error, my data entry form was open in Design view in Access. So I closed the form in design mode, and repeated the steps. And Voila! no error!
Since I will have more than one forms open, I now need to test and try to use Form_Activate() as recommended in the above MSDN reference link.
I don't know if this qualifies as an answer, but the code in that OnOpen event is dependent on a lot of outside functions. Specifically, the code is assigning value lists for the RowSources of a bunch of combo boxes. The immediate red flag that occurs to me is that non-SQL Rowsources have a finite length, and in Access 97, that limit was 2048 characters (in Access 2003, it's 32,750 -- don't ask me why it's that number!).
So, the immediate thing I see is that perhaps what ever data drives the functions that create those value lists has begun to exceed 2048 characters in length.
If that's the actual answer, then you can write a callback function that will return the values in the arrays, and it won't have the limitation on the returned length. You'd set the RowsourceType to the name of your callback function and leave the Rowsource property blank.
An example of the callback function is found in the A97 help (though I can't find the same example in the A2K3 help). In A97 help, you get there by searching for RowsourceType, and then in the help window, click on the link in the sentence reading "You can also set the RowSourceType property with a ____user-defined function____."
To check this out, you just need to find out the length of the string returned from GetValueListArray() by each of the arrays referenced in the OnOpen event.
It also might be helpful to add an error handler to the OnOpen event, particularly given that there are so many outside dependencies in the code in that particular sub.
And last of all, let me say that it looks like horrible programming. Most of this ought to be settable with default properties, seems to me. I also question that kind of dependency on OpenArgs with such an undocumented input value. What does "5" mean? And what does "1" mean? Is that documented somewhere? It's just terrible, terrible code, in my opinion.
I'd likely do this with a standalone class module instead, because that will be self-documenting in terms of what does what. You'd set a particular named property to 5 and that would control what the form gets from the class module methods for populating the combo boxes. It would all be in one place, and you could use a meaningful property name to make it clear what the values 5 and 1 represent. It's particularly helpful to do this if you have the same kind of code in the OnOpen event of multiple forms. In that case, it's a no-brainer to move it out of the form modules, and the only question is whether you put it in a regular module or in a standalone class module (as I'm suggesting).
Anyway, perhaps none of this is on point, but it might give you some ideas.
Could it be the security settings is Access? All recent versions of Access has a security settings dialog where you can enable (or disable) macros in the application. I think you will get this error if macros are disabled.
Are you sure one of the required references (VBA IDE > Option > References) isn't missing?
If you're referencing Excel/Word or external objects, are you sure that the references to the type libraries are the right ones (if you're using specific versions instead of doing late binding)
Are you building the MDE on a 64 bit machine by any chance?
What is the code on the form frmNewPeerGroup? What version of Access are you using? If it is 2003, sp3 causes problems for which there is a hotfix. Have you tried decompile and / or compact and repair?
If you have an original mdb, check the references to make sure that none of them are marked MISSING. This is quite a likely reason for problem in that it has suddenly occurred.
To check the references, look at Tools->References on the menu for a code window.
If no references are missing, you could try stepping through the form code to get a more exact idea of where the error is occurring.