Userform button does react on user actions while runing the code - excel

I created a progress bar with a UserForm with a STOP button. This UserForm works well and executed while my scripts are running.
The issue is that I cannot move the userFom Window with the mouse or click on the STOP button when the macro is running.
I guess the CPU is used for executing the code and then the UserForm has "no time" to get user inputs/events.
How can I solve this issue ?
I appreciate your help :)

To tell Excel to leave the CPU-consuming code for a moment and handle events from the user you have to insert
DoEvents
in your code.
E.g. If you have a long for loop you can write DoEvents inside the for loop. So at every iteration Excel will handle user inputs.
We don't have a sample code from you, here it's mine.
Public Sub YourRoutine()
Dim row_i As Long
'CPU Time consuming loop
For row_i = 1 To 9999999
Cells(row_i,1).Value = "I'm row number" & vbCrLf & row_i & vbCrLf & "."
Next i
End Sub
With the above code Excel will not answer to user inputs until the Sub ends.
Adding DoEvents as below code Excel will answer to user inputs every time row_i grows.
Public Sub YourRoutine()
Dim row_i As Long
'CPU Time consuming loop
For row_i = 1 To 9999999
Cells(row_i,1).Value = "I'm row number" & vbCrLf & row_i & vbCrLf & "."
'Interrupt for events
DoEvents
Next i
End Sub
You may also want to do 10 steps and after those iterations listen for user inputs
Public Sub YourRoutine()
Dim row_i As Long
'CPU Time consuming loop
For row_i = 1 To 9999999
Cells(row_i,1).Value = "I'm row number" & vbCrLf & row_i & vbCrLf & "."
'Interrupt for events every 10 iterations
If row_i Mod 10 = 0 Then
DoEvents
End If
Next i
End Sub
If you don't have a loop but a generic long code put DoEvents in every point you want Excel to listen to user events

Related

Does using multiple DoEvents in a nested loop make any sense in Excel VBA?

I have a procedure which can run very long. Yesterday it took 14 hours to complete. This piece of code is looping over the values of a column, which holds filenames of images, and searches through an array that holds all the files including the path from a location that the user selected. In this particular case, the filename column contained nearly 2600 filenames and the array to search more than 12000 records. (that's over 31 million iterations, any suggestions, if this can be improved, are welcome ;-))
In this procedure I use DoEvents to keep Excel responsive. But I just wondered if it makes sense to have two DoEvents. One in every loop (see code below). All the processing is done in this piece of code. Which in this case ran more than 14 hours.
For Each cell In ActiveSheet.Range("A1:A" & Range("A1").End(xlDown).row)
DoEvents
fileCopied = False
fileName = cell.Value
If Not (IsStringEmpty(fileName)) Then
DoEvents
For i = LBound(imgArray) To UBound(imgArray)
If Not (IsStringEmpty(CStr(imgArray(i)))) Then
If ExactMatch Then
If (fsoGetFileName(imgArray(i)) = fileName) Then
If DoesFileExist(moveToPath & GetFileName(imgArray(i))) And Not OverwriteExistingFile Then
FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i)) & "-" & Format(Now, "yyyymmddhhmmss")
Else
FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i))
End If
fileCopied = True
If fileCopied Then
If fileCopied Then
Range("B" & cell.row).Value = imgArray(i)
End If
End If
End If
End If
End If
Next i
End If
Next
As you can see, I added two DoEvents. But if only one is enough what would be the best place to add it. In the main loop or in the nested loop.
UPDATE:
Rereading the article DoEvents and DoEvents (automateexcel) made clear not to use multiple DoEvents. DoEvents are necessary in this case due to the long-running procedure. But I don't call it on every iteration now. As suggested I use:
If i Mod 100 = 0 Then DoEvents
UPDATE:
Thanks to FreeFlow I was able to gain significant performance improvements. By using the filter function available instead of looping over the Array which contained more than 12000 records. Using the filter function, speeded the process up from hours to seconds.
UPDATE:
The end result is:
fileNameString = GetFilesUsingCMD(filePath)
If Not (IsStringEmpty(fileNameString)) Then
Dim imgArray As Variant: imgArray = Split(fileNameString, "|")
rowCount = ActiveSheet.Range("A:A").Cells.SpecialCells(xlCellTypeConstants).Count
fileNameArray = Application.Transpose(ActiveSheet.Range("A:A"))
activeRow = 0
For fn = LBound(fileNameArray) To UBound(fileNameArray)
fileName = fileNameArray(fn)
If Not (IsStringEmpty(fileName)) Then
If fn Mod 10 = 0 Then
Progress.Update fn, rowCount, "(Nr. of files:" & CStr(UBound(imgArray)) & ") Executing time: " & CStr(Format((Timer - StartTime) / 86400, "hh:mm:ss")), fileName, True
DoEvents
End If
If Not ExactMatch Then
resultArray = Filter(imgArray, fileName, True, vbTextCompare)
Else
resultArray = Filter(imgArray, fileName)
End If
If (UBound(resultArray) > -1) Then
For i = LBound(resultArray) To UBound(resultArray)
If Not OverwriteExistingFile Then
If i = 0 Then
newFileName = GetFileName(resultArray(i))
Else
newFileName = CreateFileName(GetFileName(resultArray(i)), CStr(i))
End If
Else
newFileName = GetFileName(resultArray(i))
End If
FileCopy resultArray(i), moveToPath & newFileName
If Not OrgLocationAsLink Then
ActiveSheet.Cells(fn, i + 2).Value = imgArray(i) & " (" & newFileName & ")"
Else
ActiveSheet.Hyperlinks.Add ActiveSheet.Cells(fn, i + 2), Address:=resultArray(i)
End If
Next i
Else
ActiveSheet.Range("B" & fn).Value = "** NOT Available **"
ActiveSheet.Range("B" & fn).Font.Color = RGB(250, 0, 0)
End If
End If
Next fn
End If
As said, because of the Filter-function (Filter Function) I could get rid of the nested loop which iterated over 12000 times for each row on the sheet.
One or more do events will not solve the basic problem. There are a number of optimisations you can make which will speed up things immensely.
Copy excel ranges to VBA arrays (or other collection object) so that you don't do multiple accesses to Excel.
Get directory listings from your target destinations, convert the text to an array or collection object and work with that rather than multiple disk accesses to get individual filenames.
Use ArrayLists and Scripting.Dictionaries (collection objects) so you can use the contains or exists methods to avoid doing specific If then comparisons.
Don't do individual disk copies. Create a list of copy/move instructions that can be run as a shell script when you have processed all your data.
I would remove the DoEvents in the main loop, and remain the nested loop one.
By the way, I will add Application.ScreenUpdating = False at the beginning of Sub.
The post below could be helpful.
https://wellsr.com/vba/2018/excel/vba-doevents-and-when-to-use-it/

How to speed up a data file import in Excel VBA

Update as of June 11, 2019: I still haven’t figured out why practically all of my delay happens in those two lines, but current status is that I put up with the delay. So far, I have about 6000 rows of data in the master document, and an import process takes about 20 seconds regardless of how many rows I import.
—
I have a "master document" and I import data from lots and lots of little documents all day long. I admit I'm not a super-genius here, and a lot of my coding habits come from doing it "old school" so there may be "Excel ways" that I don't know (but want to learn!).
The issue I'm seeing is how much time a data file import can take.
When I started the tool out, data imports took only a few seconds.
Now that I have about 3500 rows of data, data imports take about 15-20 seconds. It doesn't matter if I am importing one row or a hundred rows. I expect this to keep going up. By the time I get to 7000 rows or 10,000 rows, I expect it to become unbearable.
By using message boxes (remember: "old school"), I've been able to narrow the speed bottleneck down to two lines of code. Between "Step 1" and "Step 2" is about 30% of my delay, and between "Step 2" and "Step 3" is about 70% of my delay.
I've included the whole sub below to make sure I'm not missing something obvious, but I made sure to UNINDENT my message boxes so you can go r-i-g-h-t to the code I suspect. Also, I included the entire sub because usually one of the first responses is “can you show the whole sub so I have better context?”
Thank you kindly for any thoughts or suggestions you might have. :)
Private Sub Btn_ImportDataFiles_Click()
' Search the current worksheet and assign the next TransactionID
Dim TransactionCounter As Integer
Dim TransactionID As Long ' This is the next available Transaction ID
TransactionID = Application.WorksheetFunction.Max(Range("a:a")) + 1
' open the file and import the data
Dim customerBook As Workbook
Dim filter As String
Dim caption As String
Dim customerFilename As String
Dim customerWorkbook As Workbook
Dim targetWorkbook As Workbook
' make weak assumption that active workbook is the target
Set targetWorkbook = Application.ActiveWorkbook
' get the customer workbook
filter = "Text files (*.xlsx),*.xlsx"
caption = "Please Select an input file "
customerFilename = Application.GetOpenFilename(filter, , caption)
If customerFilename <> "False" Then
' If they have uploaded the file before, let them know.
' If they want to keep uploading it, no harm done,
' but no need to stupidly add data that is already present.
' Select the archive sheet
Sheets("Upload_Archive").Select
Dim FileNameHunt As String
Dim cell As Range
Dim ContinueUpload As Boolean
ContinueUpload = True
FileNameHunt = Mid(customerFilename, InStrRev(customerFilename, "\") + 1)
Columns("A:A").Select
Set cell = Selection.Find(what:=FileNameHunt, after:=ActiveCell, LookIn:=xlFormulas, lookat:=xlWhole, searchorder:=xlByRows, searchdirection:=xlNext, MatchCase:=False, searchformat:=False)
If cell Is Nothing Then ' Add the new filename to the archive
Sheets("Upload_Archive").Select
Rows(1).Insert shift:=xlDown
Range("a1:a1").Value = FileNameHunt
Sheets("MasterSheet").Select
Application.Cells.Font.Name = "Calibri Light"
Application.Cells.Font.Size = "8"
Application.Cells.Font.Bold = False
Else
response = MsgBox("This data file has previously been uploaded. " & vbCrLf & "Do you want to cancel this upload?" & vbCrLf & vbCrLf & "Pressing [yes] will cancel the process." & vbCrLf & "Pressing [no] will continue with the file upload" & vbCrLf & "and add the data to the tracking sheet.", vbYesNo)
If response = vbYes Then
ContinueUpload = False
Sheets("MasterSheet").Select
Exit Sub
End If
End If ' If cell Is Nothing Then...
If ContinueUpload = True Then
' Continue with data upload procedure
Sheets("MasterSheet").Select
Set customerWorkbook = Application.Workbooks.Open(customerFilename)
' Copy data from customer to target workbook
Dim targetSheet As Worksheet
Set targetSheet = targetWorkbook.Worksheets(1)
Dim sourceSheet As Worksheet
Set sourceSheet = customerWorkbook.Worksheets(1)
Dim ImportRecordCount As Integer
ImportRecordCount = sourceSheet.Range("B1")
Dim ReconciliationID As String
ReconciliationID = ""
If sourceSheet.Range("E3") = "Removed from Depot" Then ReconciliationID = "1"
MsgBox ("Step 1")
targetSheet.Range("A1").EntireRow.Offset(1).Resize(ImportRecordCount).Insert shift:=xlDown ' Add the blank rows
MsgBox ("Step 2")
targetSheet.Range("B2:AB" & ImportRecordCount + 1).Value = sourceSheet.Range("A3:AA" & ImportRecordCount + 2).Value ' Bring in the big pile of data
MsgBox ("Step 3")
targetSheet.Range("AJ2:AJ" & ImportRecordCount + 1).Value = ReconciliationID ' To help with reconciling shipments
targetSheet.Range("AK2:AK" & ImportRecordCount + 1).Value = ReconciliationID ' To help with deployment timing
'targetSheet.Range("AI2:AI" & ImportRecordCount + 1).Value = "=COUNTIFS($D:$D, D2, $F:$F, F2)" ' This is the helper formula for identifying duplicates (deprecated, but I'm saving the code)
For TransactionCounter = 2 To ImportRecordCount + 1 ' Create and add the new Transaction ID values
targetSheet.Range("a" & TransactionCounter) = TransactionID + ImportRecordCount - TransactionCounter + 1
Next
' Close customer workbook
customerWorkbook.Close
' Format the sheet properly
Application.Cells.Font.Name = "Calibri Light"
Application.Cells.Font.Size = "8"
Application.Cells.Font.Bold = False
Application.Range("1:1").Font.Size = "10"
Application.Range("1:1").Font.Bold = True
' Query the User -- delete the file?
If MsgBox("Delete the local client-generated data file?" & vbCrLf & vbCrLf & "(this will NOT affect your email)", vbYesNo, "Confirm") = vbYes Then
Kill customerFilename
' MsgBox ("File: " & vbCrLf & customerFilename & vbCrLf & "has been deleted.")
End If
End If ' If ContinueUpload = True Then
End If ' If customerFilename <> "False" Then
End Sub
edit
I edited your original question to highlight things I found as suspect. These are things I felt are worth pointing out to you. I shaved everything else out as to focus on these particular issue. Review them and do soem research to see if you can find yourself in a better situation.
MsgBox ("Step 2")
'Ive never moved large amounts of data using this method. Ive always just used arrays. I have moved smaller bits of data though.
' I suspect that this might take a moment if the data set is large. Again use arrays to grab the data and move it.
' Edward says “This step takes about 70% of my delay — even if bringing in only a single line of data.”
targetSheet.Range("B2:AB" & ImportRecordCount + 1).Value = sourceSheet.Range("A3:AA" & ImportRecordCount + 2).Value ' Bring in the big pile of data
MsgBox ("Step 3")
' this loop is probably your main culprit of your performance issue.
' Edward says “Nope, this flies by. It is not the issue at all. I have verified this already.”
' Learn how to construct an array of data on the fly and then learn how to dump the entire array to
' sheet using a simple method.
For TransactionCounter = 2 To ImportRecordCount + 1 ' Create and add the new Transaction ID values
targetSheet.Range("a" & TransactionCounter) = TransactionID + ImportRecordCount - TransactionCounter + 1
Next
It looks like you have a lot of good things going here. A few things that I saw that could potentially be changed to improve your performance.
First, between "Step 1" and "Step 2": In my experience, adding rows takes longer than using rows that already exist. It looks like you are basically "pushing" everything down to make room for the new data, such that the newly entered data is at the top and the oldest data is at the bottom. (Correct me if I am wrong on any of this.) If you were to simply add the data to the end of the sheet, you would probably see some performance improvements, although I don't know how big of an improvement it would be.
Second, between "Step 2" and "Step 3": I have found that using .Value2 as opposed to .Value can give you some performance improvements, and the larger the data the bigger the improvement. This has a down side - Value2 does not retain any of the formatting that might be present, meaning that the number type (date, accounting, etc) does not pull over correctly. If this is something that you do not need, then you can use Value2.
Finally, other methods: When I run extensive macros, I always try to do everything I can to get a performance boost. You can get slight boosts across the board by using tricks like turning off screen updating (Application.ScreenUpdating = False), just be sure to turn it back on at the end of the macro.
I hope that this helps you figure it out! If all else fails, you can do it once or twice by hand to remember how much faster it is using the macro! Haha. Good Luck!
Have you tried using .value2? In some scenarios it might bring you better performance. Check some performance comparatives here: https://fastexcel.wordpress.com/2011/11/30/text-vs-value-vs-value2-slow-text-and-how-to-avoid-it/
It's difficult seeing where's the issue without having access to the original sheets. Maybe the issue is with the data itself instead of your VBA code and sometimes you might need to clean your source data of the heavy stuff and then add it again if needed.
You could also look into doing some parts with Python but I guess that's out of the question if you don't want to add additional software layers to your solution.
Try adding this at the beginning and end of your script. Just be sure to set everything back to TRUE!!
Application.ScreenUpdating = False
Application.DisplayAlerts = False
...CODE HERE...
Application.ScreenUpdating = True
Application.DisplayAlerts = True

Memory runs out by running continuous loop of saveas 2003 xls, reopen xlsm, close 2003 xls

[EDIT] I solved this: Integer doesn't have enough memory for the number of seconds overnight. So, changed to Long type and it works fine. Now I have a continuously updating Excel, database, CAD program and linked Excel fully automated 24-7! #SoHappy #CodeUpdatedBelow
From ThisWorkbook module in Excel, I am using Application.OnTime to call a sub in Module1, which will save a macro-enabled workbook to 2003 xls filetype, open an Access database, refresh a database table which is linked to that 2003 xls, close Access, open the original xlsm again (triggers a new timer) and finally close the 2003 xls. The timer is set on Workbook_Open and killed on Workbook_BeforeClose
For some reason it's leaking memory (I think), so the computer running the code runs out of memory by the afternoon (give or take).
Can anyone spot what I'm doing wrong, i.e. why it's hogging all that memory?
1 thing I'm aware of is I never actually close the xlsm file: it's Saved As a xls. This means the Workbook_BeforeClose event in theory never triggers to cancel the timer. But, since the time (public variable MyTime) is passed by then and it's not a recurring loop... I'm hoping that is not the cause.
I replaced the paths in Module1 with APATH for Access Path and EPATH for Excel Path - those are not erroneous variables, but hard-coded in the original (lazy, me?!)...
ThisWorkbook looks like this:
Dim MyTime As Date
Private Sub Workbook_Open()
'Just in case you need to debug
'Uncomment these 3 lines and click "No" on workbook open
'Dim Ans As Variant
'Ans = MsgBox("Do you want to run RefreshOnTime?", vbYesNo, "Yes/No")
'If Ans = vbYes Then RefreshOnTime
RefreshOnTime
End Sub
Sub RefreshOnTime()
Dim Seconds As Long
Dim OfficeOpens As Integer
Dim OfficeCloses As Integer
Dim Delay As Integer
'Delay in seconds
Delay = 240
OfficeOpens = 7
OfficeCloses = 17
'If in working hours
If Hour(Time) >= OfficeOpens And Hour(Time) < OfficeCloses Then
Seconds = Delay
'If in the morning
ElseIf Hour(Time) < OfficeOpens Then
Seconds = (OfficeOpens - Hour(Time)) * 3600 + Delay
'If after 5pm take 23:00 as highest hour of day, minus current hour
'Add 7 for morning
'Add 1 to take from 2300 to to midnight
ElseIf Hour(Time) >= OfficeCloses Then
Seconds = (23 - Hour(Time) + OfficeOpens + 1) * 3600 + Delay
End If
Debug.Print "Seconds = " & Seconds
MyTime = DateAdd("s", Seconds, Time)
Debug.Print "RefreshData will run at " & MyTime
'REPLACE MODULE1 with the right module
'REPLACE RefreshData with the name of your sub
Application.OnTime MyTime, "Module1.RefreshData"
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
'REPLACE MODULE1 with the right module
'REPLACE RefreshData with the name of your sub
Application.OnTime MyTime, "Thisworkbook.RefreshData", , False
End Sub
Module1 looks like this:
Sub RefreshData()
'Application.ScreenUpdating = False
'Rebuild all calculations
Application.CalculateFullRebuild
'Refresh all data connections
Application.Workbooks("Materials.xlsm").RefreshAll
'Complete all refresh events before moving on
DoEvents
Debug.Print "Data Refreshed at " & Time
Call SaveAsOld
If Application.ScreenUpdating = False Then Application.ScreenUpdating = True
Debug.Print "Operation Complete at " & Time
End Sub
Sub SaveAsOld()
On Error Resume Next
'Disable Screen Updating
'Application.ScreenUpdating = False
'Save Current
ThisWorkbook.Save
DoEvents
Debug.Print "Macro Workbook Saved at " & Time
'Disable alerts
Application.DisplayAlerts = False
'Save As 2003 and overwrite
ThisWorkbook.SaveAs Filename:="EPATH\Materials_2003.xls", FileFormat:=56
Debug.Print "2003 xls copy saved at " & Time
'Enable Alerts
Application.DisplayAlerts = True
'Open the macro copy
Application.Workbooks.Open Filename:="EPATH\Materials.xlsm"
''Enable ScreenUpdating
'If Application.ScreenUpdating = False Then Application.ScreenUpdating = True
ThisWorkbook.Activate
Debug.Print "Macro version opened at " & Time
Call DBOpenClose
'Close the 2003 copy
Application.Workbooks("Materials_2003.xls").Close (SaveChanges = True)
Debug.Print "2003 xls copy closed at " & Time
End Sub
Sub DBOpenClose()
Debug.Print "DBOpenClose Started at " & Time
Dim appAccess As Access.Application
Set appAccess = New Access.Application
appAccess.Visible = True
Call OpenCurrentDatabase("APath\MCMat.mdb")
Debug.Print "Access db opened at " & Time
CurrentDb.TableDefs("CADT").RefreshLink
Debug.Print "CADT Table refreshed at " & Time
Call CloseCurrentDatabase
Debug.Print "Access DB Closed at " & Time
End Sub
Thanks so much for your help!
Seconds required more memory for the number of seconds overnight, that's why it always failed on the last run during open hours. Changed to Long type instead of integer.

Excel macro runs in console and through debugger, but not with button

Error / Expected Output
When I run it from the VBA console, the code executes properly. If I step through the code manually in the debugger, it works.
The expected output is the ID of about 2000 data points. I am checking if a data matches the user input date. If that is the case, I print the ID of the event on the other sheet. Ideally, I will have a list of inputs that have numerical IDs.
When I run the code from the console or debugger, I will get a range of IDs like: 1,2,4,5,6,11,14,166... However, when I run this from the button I consistently get the first two data points, no matter which field (Date, Time, Size).
If I set a break point in the debugger then hit the button, the code is fine. All I have to do is hold run (F5). I think this tells me that the code compiles and works; meaning that the error is not a compile or logic error.
Goal
I would like the button to run the code normally.
More Info
I set a break point at every iteration of each loop. I held the step in key. The code ran flawlessly. This worked for both the console window, and the button.
Because of this, I thought I was experiencing an error due to race conditions. I went on to run the doEvents command. I was given the same results. The console and debugger ran the code properly, but the button did not.
The Code
Sub ThisBookSource()
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Application.DisplayStatusBar = False
Start = Now()
Dim masterRow As Integer
Dim myEvents
masterRow = 9
masterRow2 = 9
ActiveWorkbook.Worksheets("Graphs by Source").Activate
myCode = Range("D" & "2").Value
Range("C9:C2290").Clear
Range("T9:U2290").Clear
ActiveWorkbook.Worksheets("Data").Activate
For I = 3 To 2113
If Range("T" & I) = myCode Then
Worksheets("Data").Range("M" & I).Copy
Worksheets("Graphs by Source").Range("C" & masterRow).PasteSpecial xlPasteValues
masterRow = masterRow + 1
If I Mod 250 = 0 Then
DoEvents
End If
End If
Next I
ActiveWorkbook.Worksheets("Graphs by Source").Activate
Calculate
For I = 9 To 2290
If Range("I" & I).Value <> "NA" Then
Range("T" & masterRow2) = Range("G" & I).Value
Range("U" & masterRow2) = Range("I" & I).Value
masterRow2 = masterRow2 + 1
If I Mod 250 = 0 Then
DoEvents
End If
End If
Next I
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.DisplayStatusBar = True
MsgBox "Done. Started at " & Start & ", and ended at " & Now & "!"
End Sub
Note
I cannot post screen shots because the data is sensitive and confidential.
If you mean to work with ThisWorkbook, work with ThisWorkbook. Doing Range("foobar") [implicitly] works off ActiveSheet, which may or may not be a worksheet in ThisWorkbook - it's a worksheet in whatever workbook is currently active. It makes the whole difference. And causes bugs every day.
Rule of thumb, explicit anything is better than implicit whatever in VBA. The language is already doing way too much stuff behind your back, you need to take control.
Rubberduck (open-source VBE add-in project I manage) can help you find all instances of implicit ActiveSheet references... and many other potential issues.

Create a VBA version of dictionaries with 2 values per key

I am trying to make my excel macro dynamic. The excel macro essentially looks at only 2 columns, one which contains the name and the other contains the numeric part. I have my macro working perfectly, the only problem is that it is hard coded when I created the program. In my code, I hard coded the name in column 2 and the numeric part in column 3. However, that is not the case in real life. The name and numeric data could appear in column 1 and 5, for example. I've been manually rearranging the data in the columns so that it fits into what hard coded. However, I want to make this process dynamic and less manual work for the user.
There are 5 different versions of spreadsheets this macro will be used on and in each spreadsheet, the name and number columns are different. I am looking to make a user form box of some sort, where the user selects "Vendor XYZ" and since Vendor XYZ always sends their data sheets the same way I know that Vendor XYZ's name column is 2 and number is 4. So I was thinking that the dictionary would be something in the form of {Vendor XYZ: 2,4} (where the first number is the name column and the second number is the numeric columnnumber...I know the syntax is wrong)
I think my work around this would be to hard code the different vendors and then use if statements ( I haven't tried it yet)
I will have a user input/dropdown box of 5 different vendors. Then something like
If userinput="A"
then namecol=2 and numcol=1
If userinput="B"
then namecol="3" and numcol="4"
I don't know if that would even work. The problem with that is that the number of vendors is small now, but will be scaling up and I can't do that if we have 100 or 1000 vendors.
Any ideas?
Depending on how your initial dataset is retrieved, you can use something like this:
Public Function GetHeaderIndices(ByVal InputData As Variant) As Scripting.Dictionary
If IsEmpty(InputData) Then Exit Function
Dim HeaderIndices As Scripting.Dictionary
Set HeaderIndices = New Scripting.Dictionary
HeaderIndices.CompareMode = TextCompare
Dim i As Long
For i = LBound(InputData, 2) To UBound(InputData, 2)
If Not HeaderIndices.Exists(Trim(InputData(LBound(InputData, 1), i))) Then _
HeaderIndices.Add Trim(InputData(LBound(InputData, 1), i)), i
Next
Set GetHeaderIndices = HeaderIndices
End Function
This Function takes an array as an input and gives the user a dictionary with the indices of the headers from the input.
If you are smart (and I say this because too many users just don't use tables) you will have your data in a table, and you will have named that table. If you did, you could do something like this:
Sub DoSomething()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
End Sub
So, if you data looked like this:
Foo Baz Bar
1 Car Apple
3 Van Orange
2 Truck Banana
The function would give you a dictionary like:
Keys Items
Foo 1
Baz 2
Bar 3
Then your subroutines could do something like this:
Sub DoEverything()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
DoSomething(MyData)
End Sub
Sub DoSomething(ByRef MyData as Variant)
Dim HeaderIndices as Scripting.Dictionary
Set HeaderIndices = GetHeaderIndices(MyData)
Dim i as Long
' Loop through all the rows after the header row.
For i = LBound(MyData, 1) + 1 to Ubound(MyData, 1)
If MyData(i, HeaderIndices("Baz")) = "Truck" Then
?MyData(i, HeaderIndices("Foo"))
?MyData(i, HeaderIndices("Baz"))
?MyData(i, HeaderIndices("Bar"))
End If
Next
End Sub
This does require a reference to Scripting.Runtime so if you don't want to add a reference you will need to change any reference to As Scripting.Dictionary to As Object and any New Scripting.Dictionary to CreateObject("Scripting.Dictionary").
Alternatively, I use the following code module to take care of adding references programmatically for all my users:
Public Sub PrepareReferences()
If CheckForAccess Then
RemoveBrokenReferences
AddReferencebyGUID "{420B2830-E718-11CF-893D-00A0C9054228}"
End If
End Sub
Public Sub AddReferencebyGUID(ByVal ReferenceGUID As String)
Dim Reference As Variant
Dim i As Long
' Set to continue in case of error
On Error Resume Next
' Add the reference
ThisWorkbook.VBProject.References.AddFromGuid _
GUID:=ReferenceGUID, Major:=1, Minor:=0
' If an error was encountered, inform the user
Select Case Err.Number
Case 32813
' Reference already in use. No action necessary
Case vbNullString
' Reference added without issue
Case Else
' An unknown error was encountered, so alert the user
MsgBox "A problem was encountered trying to" & vbNewLine _
& "add or remove a reference in this file" & vbNewLine & "Please check the " _
& "references in your VBA project!", vbCritical + vbOKOnly, "Error!"
End Select
On Error GoTo 0
End Sub
Private Sub RemoveBrokenReferences()
' Reference is a Variant here since it requires an external reference.
' It isnt possible to ensure that the external reference is checked when this process runs.
Dim Reference As Variant
Dim i As Long
For i = ThisWorkbook.VBProject.References.Count To 1 Step -1
Set Reference = ThisWorkbook.VBProject.References.Item(i)
If Reference.IsBroken Then
ThisWorkbook.VBProject.References.Remove Reference
End If
Next i
End Sub
Public Function CheckForAccess() As Boolean
' Checks to ensure access to the Object Model is set
Dim VBP As Variant
If Val(Application.Version) >= 10 Then
On Error Resume Next
Set VBP = ThisWorkbook.VBProject
If Err.Number <> 0 Then
MsgBox "Please pay attention to this message." _
& vbCrLf & vbCrLf & "Your security settings do not allow this procedure to run." _
& vbCrLf & vbCrLf & "To change your security setting:" _
& vbCrLf & vbCrLf & " 1. Select File - Options - Trust Center - Trust Center Settings - Macro Settings." & vbCrLf _
& " 2. Place a checkmark next to 'Trust access to the VBA project object model.'" _
& vbCrLf & "Once you have completed this process, please save and reopen the workbook." _
& vbCrLf & "Please reach out for assistance with this process.", _
vbCritical
CheckForAccess = False
Err.Clear
Exit Function
End If
End If
CheckForAccess = True
End Function
And I have the following command in each Workbook_Open event (less than ideal, but only good solution I have so far)
Private Sub Workbook_Open()
PrepareReferences
End Sub

Resources