How can get a list of included records in a Word Mail Merge Document? - vba

I have a Mail Merge Document which is controlled via VBA. After the user selects the records he wants to print, I want these to get a print date set in the database. For that I need a list of the records included in the mail merge.
I tried using the .Included property, setting ThisDocument.MailMerge.DataSource.ActiveRecord = wdFirstRecord (I later removed the wdFirstRecord in favour of 1 since it was giving me trouble)
and then checking for ThisDocument.MailMerge.DataSource.Included to be true but got a 5852 runtime-error "Object not available". EDIT: I used the following code to iterate over the records. When I remove either of the commented .Included statements, I get said error. (The execution does not feel as slow as yesterday anymore, though it is not especially fast.)
Function outputRecords(Optional limitRecords = -1)
With ThisDocument.MailMerge.DataSource
Dim str
For currentFieldNameIndex = 1 To .FieldNames.Count
str = str & .FieldNames(currentFieldNameIndex) & vbTab
Next
Debug.Print str
For currentRecordIndex = 1 To .RecordCount
If currentRecordIndex <= limitRecords Or limitRecords < 0 Then
.ActiveRecord = currentRecordIndex
str = ""
For currentDataFieldIndex = 1 To .DataFields.Count
str = str & .DataFields(currentDataFieldIndex) & vbTab
Next
'Debug.Print str
End If
'.Included = True
'Debug.Print .Included
Next
End With
End Function
Is there a solution to know which records are selected by the user?
About my document: After some general computation, the data source is linked to the document using
Dim sql As String
sql = "SELECT * FROM `Sheet0$` "
sql = sql & "WHERE ((`" & photoPathHeader & "` > '') AND (`" & photoLastEditHeader & "` >= #" & Format(printFromDate, "yyyy-mm-dd") & "#)) "
sql = sql & "ORDER BY `klasse#name` ASC"
ThisDocument.MailMerge.OpenDataSource _
name:=ThisDocument.Path & "\" & ThisDocument.Variables("masterDataFileName"), _
SQLStatement1:=sql, _
ReadOnly:=True, LinkToSource:=True
a dialog where the user can select individual records for printing is shown. I used this code for that:
Application.Dialogs(wdDialogMailMergeRecipients).Display
And finally, the Mail Merge is executed using
ThisDocument.MailMerge.Execute
Many thanks in advance!

This Answer is based on the comments so far.
I do not get the problem with .Included here unless ThisDocument is the wrong document. However, since your loop code does not appear to throw an error when accessing other members of ThisDocument.MailMerge.DataSource it is not obvious what is wrong. However, I don't think you need to use .Included.
I see an immediate slowdown as soon as I try to access any of the .Datasource.DataFields items. I do not know why that is. There is a delay for each access, not just the first access for each record. The one idea I had is that as soon as you change the active record, Word refreshes the values of the merge fields in the main document, and that might cause a problem, particularly if Word needed to access the printer driver for some reason. But nothing I tried (e.g. change the View to Draft) changes that.
You don't really need .Included because you can iterate the records like this:
With ActiveDocument.MailMerge.DataSource
previousRecord = 0
.ActiveRecord = wdFirstRecord
While .ActiveRecord <> previousRecord
previousRecord = .ActiveRecord
Debug.Print .ActiveRecord ' (just lists the record number)
previousRecord = .ActiveRecord
.ActiveRecord = wdNextRecord
DoEvents ' advisable if you need to stop the code
Wend
If you need to iterate all the records then you need to use wdFirstDataSourceRecord and wdNextDataSourceRecord instead.
The only way I was able to speed up access to the data was to modify the document to include all the fields I wanted to list, then get the values from the document rather than from .DataSource.Datafields. Personally I do not think modify the Mail Merge main document is ideal - in particular, something might stop you from inserting material at the beginning, as I have done. But my test code (needs more error trapping etc. is like this):
Sub checkincluded()
Dim b As Word.Bookmark
Dim bVMMFC As Boolean
Dim bSFC As Boolean
Dim i As Integer
Dim previousRecord As Long
Dim p As Word.Paragraph
Dim r As Word.Range
With ActiveWindow.View
bSFC = .ShowFieldCodes
.ShowFieldCodes = False
End With
With ActiveDocument.MailMerge
bVMMFC = .ViewMailMergeFieldCodes
.ViewMailMergeFieldCodes = False
With .DataSource
Set r = ActiveDocument.Range(0, 0)
Set p = ActiveDocument.Range(0, 0).Paragraphs.Add
For i = 1 To .FieldNames.Count
If i > 1 Then
r.Text = vbTab
r.Start = p.Range.End - 1
r.End = p.Range.End - 1
End If
r.Fields.Add r, WdFieldType.wdFieldMergeField, .FieldNames(i), False
r.Start = p.Range.End - 1
r.End = p.Range.End - 1
Next
r.Start = 0
r.End = p.Range.End - 1
Set b = r.Bookmarks.Add("recorddata")
Set r = Nothing
previousRecord = 0
.ActiveRecord = wdFirstRecord
While .ActiveRecord <> previousRecord
previousRecord = .ActiveRecord
Debug.Print .ActiveRecord, b.Range.Text
previousRecord = .ActiveRecord
.ActiveRecord = wdNextRecord
DoEvents
Wend
b.Delete
Set b = Nothing
p.Range.Text = ""
Set p = Nothing
End With
.ViewMailMergeFieldCodes = bVMMFC
End With
ActiveWindow.View.ShowFieldCodes = bSFC
End Sub
Finally, it is probably worth pointing out that there are other ways to exclude records, e.g. using NEXT, NEXTIF, SKIP and SKIPIF fields. But that is perhaps another question.

Thanks to #yokki's answer, I was able to combine that with what I know and ended up with the following
Sub listIncluded()
Dim LastRecord As Long
With ActiveDocument.MailMerge.DataSource
' Storing the index of the last record for later use
.ActiveRecord = wdLastRecord
LastRecord = .ActiveRecord
.ActiveRecord = wdFirstRecord
Do
Debug.Print .ActiveRecord
DoEvents
If .ActiveRecord = LastRecord Then Exit Do
.ActiveRecord = wdNextRecord
Loop Until .ActiveRecord > LastRecord ' Note this will never be satisfied and the loop will always be exited via the Exit Do statement
End With
End Sub
I find the WdMailMergeActiveRecord enumeration a bit confusing but this works.
The original answer threw a 5853 Invalid parameter error on the wdNextRecord if the last included record had already been reached and was not equal to the last record overall (i.e. the last record was not included). I circumvented this issue by storing the index of the last record into a local variable and using that to check if the last included record was reached.
Thank you very much to everyone who helped!
As briefly pointed out in a comment, I already have a list of all the
data in the data source as an array since I also use it in other parts
of the script. I used the following statement for that
numRows = wks.Range("A1").CurrentRegion.Rows.Count
masterData = wks.Range("A1").CurrentRegion.Offset(RowOffset:=1).Resize(RowSize:=numRows).Value
wks refers to an Excel worksheet object.

Related

MS Project VBA - finding the last row of the last sub project inserted in a master project

I have a simple Master plan with 3 inserted small plans as a prototype for a much larger and more complex project.
I want to find out what the value in Text1 is for the last item in the master / sub project plan.
I have a macro which links up dependencies across the sub plans based on a unique reference - loop through the tasks, when you find a reference loop through all the tasks again to find a match and build the dependency link.
This works brilliantly unless there isn't a matching reference in the plan (for instance when there is an external dependency which doesn't appear in the sub plans). At this point it just links to the last item that it found which is not good.
To get around this I have established how many rows there are in the plan and will ignore anything which is returned at the end of the "sub search"
''''
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
If LCase(t.Text1) = LCase("Dep_in") Then
ref = t.Text2
n = 0
For Each t_check In ActiveProject.Tasks
n = n + 1
If t_check Is Nothing Then
'do nothing
Else
If LCase(t_check.Text2) = LCase(ref) And LCase(t_check.Text1) = LCase("Dep_out") Then
ID = t_check.ID
Source = t_check.Project
If n < max_tasks Then t.ConstraintType = pjASAP
If n < max_tasks Then t.Predecessors = Dep_path & Source & ".mpp\" & ID
End If
End If
Next t_check
End If
End If
Next t
The issue with this method is that if there is a legitimate Deliverable on the last row of the last sub plan it will never be picked up.
Unless there is a neat way to handle the situation where there isn't a match in the sub loop how can I test the lastrow.text1 to see if it contains DEP and if so issue a message warning of this fact?
The only way I can think to do this would be the rather inelegant:
n = 0
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
n = n + 1
End If
Next t
max_tasks = n
n = 0
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
n = n + 1
If n = max_tasks Then Debug.Print t.Name
End If
Next t
Thanks
When working with master projects it is important to remember that the Tasks collection only contains the tasks in the master project. In the example posted in the question, ActiveProject.Tasks will contain 3 tasks--one for each of the subprojects.
To loop through all of the tasks, expand the schedule so that all are shown, select all, then loop through the selection.
FilterClear
SelectAll
OutlineShowAllTasks
SelectAll
Set allTasks = ActiveSelection.Tasks
To find the matching task to link, there are at least two options: 1) make a copy of the collection of tasks (allTasks2) and loop through that, or 2) use the Find method.
The Find method shines when looking for a single match in a single field. For example, configure Text3 with a formula that concatenates Text1 and Text2 and this is all that's needed:
If Find("Text3", "equals", t.Text1 & t.Text2) Then
Set tskOut = ActiveCell.Task
t.ConstraintType = pjASAP
t.Predecessors = Dep_path & Source & ".mpp\" & tskOut.ID
End If
However, the Find method can still be used efficiently by knowing that the method moves the active cell to the next match, if found. In this way, the Find method can be used in a loop to find the correct match, or indicate if no match was found.
The main body of the code can be reduced to this:
For Each t In allTasks
If Not t Is Nothing Then
If LCase(t.Text1) = LCase("Dep_in") Then
Dim tskOut As Task
Set tskOut = FindDepOutTask(t)
If tskOut.UniqueID <> t.UniqueID Then
t.ConstraintType = pjASAP
t.Predecessors = Dep_path & Source & ".mpp\" & tskOut.ID
End If
End If
End If
Next t
Using the helper function:
Function FindDepOutTask(depInTask As Task) As Task
' start at Dep In Task
Find "Unique ID", "equals", depInTask.UniqueID
Dim tskOut As Task
Set tskOut = depInTask
Do
Find "Text2", "equals", depInTask.Text2
Set tskOut = ActiveCell.Task
Loop Until tskOut.UniqueID = depInTask.UniqueID Or LCase(tskOut.Text1) = LCase("Dep_out")
Set FindDepOutTask = tskOut
End Function

Word VBA Progress Bar with Unknown Number of Steps

I have a macro that loops through an unknown number of times. The number of times varies based on a total number of rows in multiple tables in a reference document, and that number of rows will vary across reference documents that may be used. The relevant snippet of code for the loop is below:
For Each oRow In oTbl.Rows
p = p + 1
Helper.ProgressIndicator_Code (p)
strPhrase = Split(Trim(oRow.Range.Cells(1).Range.Text), vbCr)(0)
strRule = Split(Trim(oRow.Cells(2).Range.Text), vbCr)(0)
If strPhrase <> "" Then
If Not strStartWord = vbNullString Then
'Process defined sections
arrEndWords = Split(strEndWord, "|")
For lngIndex = 0 To UBound(arrEndWords)
Set oRng = GetDocRange(strStartWord, arrEndWords(lngIndex))
If Not oRng Is Nothing Then Exit For
Next lngIndex
Else
'Process whole document
Set oRng = m_oDocCurrent.Range
End If
If Not oRng Is Nothing Then
Set oRngScope = oRng.Duplicate
With oRng.Find
.Text = strPhrase
Do While .Execute
If Not oRng.InRange(oRngScope) Then Exit For
oRng.HighlightColorIndex = wdTurquoise
If strRule <> "" Then
Set oComment = m_oDocCurrent.Comments.Add(Range:=oRng, Text:=strUsr & ": " & strRule)
oComment.Author = UCase("WordCheck")
oComment.Initial = UCase("WC")
End If
Loop
End With
End If
End If
Next oRow
The progress bar is a classic progress bar for which a label field width is updated using the below code based on a value of p as updated in the above code:
Sub progress(pctCompl As Integer)
ProgressIndicator.Text.Caption = pctCompl & "% Completed"
ProgressIndicator.Bar.Width = pctCompl * 2
DoEvents
End Sub
Here's my problem: The value of p varies based on which reference document is used, so my progress bar is never even approximately accurate with respect to the processing of the VBA macro. The progress bar doesn't have to be exact, merely close and to indicate that progress is being made and nothing has hung.
I'm not looking for written code, just would be very grateful for suggestions or advice as to approaches for making my progress bar more accurate so that I can learn (e.g., I just ran the macro for three different reference documents - one gave me 25%, one gave 44%, and one gave 82%; none showed even close to 100% when completed). Essentially I need to divide i by an unknown number to get my percentage, which is clearly impossible, so some function for a close approximation is needed.
Edit: New code based on #macropod suggestion.
Dim strCheckDoc As String, docRef As Document, projectPath As String, _
j As Integer, i As Integer, k As Integer, oNumRows as Long
j = 1
For i = 0 To UBound(strUsr)
strCheckDoc = [path to reference document unique to each strUsr]
Set docRef = Documents.Open(strCheckDoc, ReadOnly:=True, Visible:=False)
For k = 1 To docRef.Tables.Count
oNumRows = oNumRows + docRef.Tables(i).Rows.Count
Next k
Next i
Then the code to update the progress bar is:
Dim pctCompl As Single
pctCompl = Round((p / oNumRows) * 100)
ProgressIndicator.Text.Caption = pctCompl & "% Completed"
ProgressIndicator.Bar.Width = pctCompl * 2
DoEvents
The progress bar now gets to 64% when complete (i.e., it should be at 100%). I'm also working on a way to make oNumRows only count a row if the row has content in the first column.

ListRows.Add doesn't appear to work

I've got a really odd case… hopefully someone is able to help me out, I've search many forums looking for a solution, the closest I could find related to it (kinda) is here, though I've tried all the suggestions to no avail…
I'm trying to run a function to return a data list in a string delimitated by a semicolon from an oracle stored function. (This value function call seems to work fine).
I then loop through the string for each data value and print it to a blank table (0 rows) declared in my subroutine. which I use to load into an access data base. (just trust it make sense in the big picture…).
The issue, fundamentally is that no information is printed into the table. However when I step through the code it works fine.
After troubleshooting I THINK (see my test scenarios below code) the issue comes up after the listrows.add line... though not obviously.
I don't think this line is executed by the time the first value is trying to print to the table.
The most confusing part is I'm running through 2 nearly identical procedures (call function -> Return value -> print values to table) immediately before this portion of the code and they work without fail.
Code Excerpt:
'run function to get string ... this works
DoEvents ' not in original design
RelRtnStr = Prnt(Cat, "A Third Oracle Function Name")
DoEvents ' not in original design
RelChopVar = RelRtnStr
StrFldCnt = 0
Checking = True ''' CodeBreak Test 1
DoEvents ' not in original design
AppendRlLmTbl.ListRows.Add ''''''''This isn't appearing to work...
DoEvents ' not in original design
Debug.Print Now ' not in original design
Application.Wait (Now + TimeValue("0:00:3")) ' not in original design
Debug.Print Now ' not in original design
While StrFldCnt < 80 And (Len(RelChopVar) - Len(Replace(RelChopVar, ";", ""))) > 0 And Checking
'## Count String Position
StrFldCnt = StrFldCnt + 1
'## Find Current String Value & Remainder String
If InStr(RelChopVar, ";") <> 0 Then
'Multiple Values Left
FldVal = Replace(Left(RelChopVar, InStr(RelChopVar, ";")), ";", "")
RelChopVar = Right(RelChopVar, Len(RelChopVar) - InStr(RelChopVar, ";"))
Else
'Last Value
FldVal = RelChopVar
Checking = False
End If
'## Get Field Name For Current Value & Print to Table
FldNm = CStr(RefRtrn(2, CStr(StrFldCnt))) ''' CodeBreak Test 2
AppendRlLmTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal '''CodeBreak 2 error thrown
Debug.Print StrFldCnt & FldNm & FldVal
Wend
AppendRlLmTbl.ListColumns("Catalogue").DataBodyRange.Value = Cat
So far I've tested a ton of options suggested online, not necessarily understanding each test... This is what I've gleaned.
If I step through the code, it works
If I set a breakpoint at "CodeBreak Test 1" and "F5" the rest, it works …
If I set a breakpoint at "CodeBreak Test 2" I get an "Object with variable not set" error thrown …
Things I've tried …
Wrapping anything and everything with DoEvents
setting a wait time after the listObjects.add row
Validated the code performs the While loop when running the "full procured" (as opposed to stepping through)
The worst part, I have no idea why the object won't declare properly when setting a break point after the add row line but sets properly when break point is set before and has no error thrown when running the full procedure (I have no on error declarations.)...
It of course must be related in my mind but I can't find any information online and unfortunately have no formal VBA background and 1 undergrad course as a programming background in general. Aka I'm out of my depth and super frustrated.
PS. first post, so please be nice :p
Full Code Below:
Option Explicit
'## Here's my attempt to clean up and standardize the flow
'## Declare my public variables
' WorkBook
Public WB As Workbook
' Sheets
Public Req2ByWS As Worksheet
Public ReqSpecsWS As Worksheet
Public ReqInstrcWS As Worksheet
Public ConfigReqWS As Worksheet
Public AppendReqWS As Worksheet
Public AppendRlLmWS As Worksheet
' Objects (tables)
Public ReqConfigTbl As ListObject
Public SpecConfigTbl As ListObject
Public CurrRegIDTbl As ListObject
Public AppendReqTbl As ListObject
Public AppendRlLmTbl As ListObject
'## ##
'## Get Data from Tom's Functions ##
Sub GetSpotBuyData()
'## Preliminary Config ##
'## Turn OFF Warnings & Screen Updates
Application.DisplayAlerts = False
Application.ScreenUpdating = False
'## Set global Referances to be used in routine
' WorkBooks
Set WB = Workbooks("MyWb.xlsm")
' WorkSheets
Set Req2ByWS = WB.Sheets("MyWb Pg1")
Set ReqSpecsWS = WB.Sheets("MyWb Pg2")
Set ConfigReqWS = WB.Sheets("MyWb Pg3")
Set AppendReqWS = WB.Sheets("MyWb Pg4")
Set AppendRlLmWS = WB.Sheets("MyWb Pg5")
' Tables
Set ReqConfigTbl = ConfigReqWS.ListObjects("MyWS Tbl1")
Set SpecConfigTbl = ConfigReqWS.ListObjects("MyWS Tbl2")
Set CurrRegIDTbl = ConfigReqWS.ListObjects("MyWS Tbl3")
Set AppendReqTbl = AppendReqWS.ListObjects("MyWS Tbl4")
Set AppendRlLmTbl = AppendRlLmWS.ListObjects("MyWS Tbl5")
'## Declare Routine Specefic Variables
Dim Doit As Variant
Dim Checking As Boolean
Dim Cat As String
Dim CatRtnStr As String
Dim CatChopVar As String
Dim SpecRtnStr As String
Dim SpecChopVar As String
Dim RelRtnStr As String
Dim RelChopVar As String
Dim FldVal As String
Dim FldNm As String
Dim StrFldCnt As Integer
'## 1) General Set-Up ##
'## Unprotect tabs (loop through All Tabs Unprotect)
Doit = Protct(False, WB, "Mypassword")
'## Refresh Data
Doit = RunUpdateAl(WB)
'## 2) Find the Catalgue we are playing with ##
'## Grab Catalogue input from ISR
If [Catalogue].Value = "" Then
MsgBox ("Please Enter a Catalogue")
GoTo ExitSub
Else
Cat = [Catalogue].Value
End If
'## 3) Run Toms Function and print the results to the form & Append Table ##
'## 3a) Do it for Cat Info Function
'## Get Cat Info String From Function
CatRtnStr = Prnt(Cat, "An Oracle Functions Name")
CatChopVar = CatRtnStr
If CatChopVar = "No Info" Then
MsgBox ("No Info Found in Catalogue Data Search.")
GoTo SkipCatInfoPrint
End If
'## Loop Through Data String & Write to Form
StrFldCnt = 0
Checking = True
AppendReqTbl.ListRows.Add
While Checking
'## Count String Position
StrFldCnt = StrFldCnt + 1
'## Find Current String Value & Remainder String
If InStr(CatChopVar, ";") <> 0 Then
'Multiple Values Left
FldVal = Replace(Left(CatChopVar, InStr(CatChopVar, ";")), ";", "")
CatChopVar = Right(CatChopVar, Len(CatChopVar) - InStr(CatChopVar, ";"))
Else
'Last Value
FldVal = CatChopVar
Checking = False
End If
'## Get Field Name For Current Value & Print to Form
FldNm = CStr(RefRtrn(1, CStr(StrFldCnt)))
If FldNm <> "CustomerSpecification" And FldNm <> "ShiptoAddress" Then
'Take Value as is
Req2ByWS.Range(FldNm).Value = FldVal
AppendReqTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal
ElseIf FldNm = "CustomerSpecification" Then
'Replace : with New Line
FldVal = Replace(FldVal, " : ", vbLf)
Req2ByWS.Range(FldNm).Value = FldVal
AppendReqTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal
ElseIf FldNm = "ShiptoAddress" Then
'Replace - with New Line
FldVal = Replace(FldVal, " - ", vbLf)
Req2ByWS.Range(FldNm).Value = FldVal
AppendReqTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal
End If
Wend
'## 3b) Do it for Spec Function
SkipCatInfoPrint:
'## Get Spec Info String From Function
SpecRtnStr = Prnt(Cat, "Another Oracle Functions Name")
SpecChopVar = SpecRtnStr
If SpecChopVar = "No Info" Then
MsgBox ("No Info Found in Data Search.")
GoTo SkipSpecInfoPrint
End If
'## Loop Through Data String & Write to Form
StrFldCnt = 0
Checking = True
While StrFldCnt < 80 And (Len(SpecChopVar) - Len(Replace(SpecChopVar, ";", ""))) > 0 And Checking
'## Count String Position
StrFldCnt = StrFldCnt + 1
'## Find Current String Value & Remainder String
If InStr(SpecChopVar, ";") <> 0 Then
'Multiple Values Left
FldVal = Replace(Left(SpecChopVar, InStr(SpecChopVar, ";")), ";", "")
SpecChopVar = Right(SpecChopVar, Len(SpecChopVar) - InStr(SpecChopVar, ";"))
Else
'Last Value
FldVal = SpecChopVar
Checking = False
End If
'## Get Field Name For Current Value & Print to Form
FldNm = CStr(RefRtrn(2, CStr(StrFldCnt)))
ReqSpecsWS.Range(FldNm).Value = FldVal
AppendReqTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal
Wend
'## 3c) Do it for Rel Limits Function
SkipSpecInfoPrint:
'## Get Rel Limits String From Function
RelRtnStr = Prnt(Cat, "A Third Functions Name")
RelChopVar = RelRtnStr
If RelChopVar = "No Info" Then
MsgBox ("No Info Found in Data Search.")
GoTo ExitSub
End If
'## Loop Through Data String & Write to Form
StrFldCnt = 0
Checking = True
AppendRlLmTbl.ListRows.Add
While StrFldCnt < 80 And (Len(RelChopVar) - Len(Replace(RelChopVar, ";", ""))) > 0 And Checking
'## Count String Position
StrFldCnt = StrFldCnt + 1
'## Find Current String Value & Remainder String
If InStr(RelChopVar, ";") <> 0 Then
'Multiple Values Left
FldVal = Replace(Left(RelChopVar, InStr(RelChopVar, ";")), ";", "")
RelChopVar = Right(RelChopVar, Len(RelChopVar) - InStr(RelChopVar, ";"))
Else
'Last Value
FldVal = RelChopVar
Checking = False
End If
'## Get Field Name For Current Value & Print to Form
FldNm = CStr(RefRtrn(2, CStr(StrFldCnt)))
AppendRlLmTbl.ListColumns(FldNm).DataBodyRange.Value = FldVal
Wend
AppendRlLmTbl.ListColumns("SpecificFieldName").DataBodyRange.Value = Cat
'## 4) Re-Format and Clean Up Program ##
ExitSub:
'## Clean-Up Formatting
Req2ByWS.Range("F:F", "C:C").ColumnWidth = 30
Req2ByWS.UsedRange.Rows.AutoFit
Req2ByWS.UsedRange.Columns.AutoFit
Req2ByWS.Range("G:G").ColumnWidth = 15
Req2ByWS.Range("J:R").ColumnWidth = 12
Req2ByWS.Range("D:D").ColumnWidth = 12
'## Protect tabs (loop through All Tabs Protect)
'Doit = Protct(True, WB, "Mypassword", Req2ByWS.Name)
'Req2ByWS.Unprotect ("Mypassword")
'Application.Wait (Now + TimeValue("0:00:10"))
Req2ByWS.Select
'## Turn ON Warnings & Screen Updates
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
I stupidly had an enable background refresh for that specific table. An early call to refresh all data triggered the refresh, code would execute and the refresh would finally complete shortly after the code finished executing... in break mode the refresh would complete prior too. Thanks PEH for helping me look into this.

Duplicate removal for VBA Word not working effectively

I have a program to remove duplicates and everything is working properly. It is just freezing with large data sets i.e. 1 to 2.5 million words.
What is wrong with my approach? Is there a better one?
Sub DeleteDuplicateParagraphs()
Dim p1 As Paragraph
Dim p2 As Paragraph
Dim DupCount As Long
Dim StartTime As Double
Dim SecondsElapsed As Double
StartTime = Timer
For Each p1 In ActiveDocument.Paragraphs
If p1.range.Text <> vbCr Then
For Each p2 In ActiveDocument.Paragraphs
If p1.range.Text = p2.range.Text Then
DupCount = DupCount + 1
If p1.range.Text = p2.range.Text And DupCount > 1 Then p2.range.Delete
End If
Next p2
End If
DupCount = 0
Next p1
SecondsElapsed = Round(Timer - StartTime, 2)
MsgBox "This code ran successfully in " & SecondsElapsed & " seconds", vbInformation
DupCount = 0
End Sub
Try this (first add a reference to the Microsoft Scripting Runtime to your VBA project):
Sub DeleteDuplicateParagraphs()
Dim p As Paragraph
Dim d As New Scripting.Dictionary
Dim t As Variant
Dim i As Integer
Dim StartTime As Single
StartTime = Timer
' collect duplicates
For Each p In ActiveDocument.Paragraphs
t = p.Range.Text
If t <> vbCr Then
If Not d.Exists(t) Then d.Add t, New Scripting.Dictionary
d(t).Add d(t).Count + 1, p
End If
Next
' eliminate duplicates
Application.ScreenUpdating = False
For Each t In d
For i = 2 To d(t).Count
d(t)(i).Range.Delete
Next
Next
Application.ScreenUpdating = True
MsgBox "This code ran successfully in " & Round(Timer - StartTime, 2) & " seconds", vbInformation
End Sub
This makes use of the fact that the Scripting.Dictionary is a hash table that is geared towards very quickly associating unique keys with values. It is therefore very good at spotting duplicate keys. Dictionary keys have to be strings, conveniently we can use the paragraph texts for that.
For values we use more dictionary objects, solely for the fact that they work a lot better than VBA's arrays. In them we collect the references to the actual paragraph instances with the same text.
Actually deleting duplicate paragraphs is a very simple matter afterwards.
Note: The duplicate detection part in the above code is very fast. However, if Word becomes unresponsive in large documents then it's in the duplicate removal part, namely because of Word's undo buffer.
The culprit is that the paragraph ranges are deleted one after another, causing Word to build a very large undo buffer. Unfortunately there is no way (that I know of) to either
delete multiple separate ranges in one step (which would result in only a single entry in the undo buffer), or
disable the undo buffer altogether from VBA
Calling UndoClear periodically in the "eliminate duplicates" loop might help, disabling ScreenUpdating is also not a bad idea:
' eliminate duplicates
Dim x As Integer
Application.ScreenUpdating = False
For Each t In d
x = x + 1
For i = 2 To d(t).Count
d(t)(i).Range.Delete
Next
If x Mod 50 = 0 Then ActiveDocument.UndoClear
Next
ActiveDocument.UndoClear
Application.ScreenUpdating = True
First of all, Just wanted to thank you so much for the time and effort you have put in to helping me.
Your idea behind the method is really impressive. I did change the code slightly and would like you to peruse it when you have the time, to see if it is of optimal standard. Again, I truly thank you, the code ran 20 splits faster than the previous and that is not even over a larger data set.
> Sub DeleteDuplicateParagraphs()
>
> Dim p As Paragraph
> Set d = CreateObject("Scripting.Dictionary")
> Dim t As Variant
> Dim i As Integer
> Dim StartTime As Single
>
> StartTime = Timer
>
> ' collect duplicates For Each p In ActiveDocument.Paragraphs
> t = p.range.Text
> If t <> vbCr Then
> If Not d.Exists(t) Then d.Add t, CreateObject("Scripting.Dictionary")
> d(t).Add d(t).Count + 1, p
> End If Next
>
> ' eliminate duplicates For Each t In d
> For i = 2 To d(t).Count
> d(t)(i).range.Delete
> Next Next
>
> MsgBox "This code ran successfully in " & Round(Timer - StartTime,
> 2) & " seconds", vbInformation
>
> End Sub

This array is fixed or temporarily locked

I am using split function and assigning the value in a variable and running the code in loop after few iterations its giving an error of "This array is fixed or temporarily locked (Visual Basic)"..
e.g; here value of movies_cat1 read from excel is in form of this------
"Movies->List All Movies , Movies->World Cinema->Asia , Movies->Movies by Language->Sinhalese , Movies->Drama"
For crow = 1 To 100
Value = Worksheets("Movies_categories").Range("A" & crow).Value
cat_final = Worksheets("Movies_categories").Range("B" & crow).Value
If Value = "y" Or Value = "Y" Then
'Loop for reading the data from tabsheet- Movies
For crowss = 5 To 3000
movies_cat1 = Worksheets("Movies").Range("B" & crowss).Value
movies_language = Worksheets("Movies").Range("C" & crowss).Value
If movies_language = "English" Then
Temp = Split(movies_cat, ",") 'run time Error:10 occurs here..
For Each boken_c In Temp
flag = 0
boken_c = Trim(boken_c)
If RTrim(LTrim(boken_c)) = LTrim(RTrim(cat_final)) Then
flag = 1
GoTo Line4:
End If
Next boken_c
End If
Next crowss
End If
Line4: Next crow
Error occurs at this statement: Temp = Split(movies_cat, ","), it says that the array is fixed or temporarily locked, because i think initially its taking 'temp' as a variable, but while returning the value of split function, variable 'Temp' becomes array after completion of first loop(i.e after crow = 6,7....)
Your line4 label is outside the for loop on the temp variable so when you goto it leaves it locked.
You really should restructure your code to not use a goto inside the for each loop.
Maybe:
For crow = 1 To 100
Value = Worksheets("Movies_categories").Range("A" & crow).Value
cat_final = Worksheets("Movies_categories").Range("B" & crow).Value
If Value = "y" Or Value = "Y" Then
'Loop for reading the data from tabsheet- Movies
For crowss = 5 To 3000
movies_cat1 = Worksheets("Movies").Range("B" & crowss).Value
movies_language = Worksheets("Movies").Range("C" & crowss).Value
If movies_language = "English" Then
Temp = Split(movies_cat, ",") 'run time Error:10 occurs here..
For Each boken_c In Temp
flag = 0
boken_c = Trim(boken_c)
If RTrim(LTrim(boken_c)) = LTrim(RTrim(cat_final)) Then
flag = 1
**Exit For**
End If
**If flag = 1 Then Exit For**
Next boken_c
End If
**If flag = 1 Then Exit For**
Next crowss
End If
Next crow
(Note the **d lines.)
I had this problem too with VBA. I cannot say I am proud of how I managed to get it, but it is supplied here just in can anyone else accidentally slips up on this.
It is quite interesting to debug as the failure occurs at the call to a sub or function - not at the point of failure. Luckily, you can follow the code through to the offending line of the called routine before it reports the error.
Call Sub1(gArray(3))
debug.print gArray(3)
...
Sub Sub1(i as integer)
Redim gArray(0)
End sub
Clearly the VB RT is not going to like this as by the time the debug.print executes, the array dimension does not exist. Ok why the hell would you want to pass a globally declared array anyway? Guilty as charged.
So in the example above you get the error at the call to Sub1, but the thing causing it is the Redim in the sub.
The moral to the story is do not pass global variables as parameters to subs. Another simple fix is to declare the Sub1 slightly differently:
Sub Sub1(ByVal i as integer)
This means the i variable is copied (but not returned) by the Sub.
Thanks, Deanna for the answer! I have a similar message on ReDim Preserve statement at the last line in next fragment of code:
If StrComp(Mid(s, 1, lE), txtE, vbBinaryCompare) = 0 Then
For i = iMdl To 1 Step -1
With blks(i)
If .lnEnd = 0 Then ' ".lnEnd" is a member of blks(i)
.lnEnd = ln
GoTo NXT
End If
End With
Next
errBegWith = errBegWith & ln & ", "
NXT:
End If
'...
ReDim Preserve blks(iMdl)
And after extracting assignment .lnEnd = ln from inside of the With the program works fine:
If StrComp(Mid(s, 1, lE), txtE, vbBinaryCompare) = 0 Then
For i = iMdl To 1 Step -1
If blks(i).lnEnd = 0 Then
blks(i).lnEnd = ln
GoTo NXT
End If
Next
errBegWith = errBegWith & ln & ", "
NXT:
End If
'...
ReDim Preserve blks(iMdl)