1

Background

Hello there! I'm stuck trying to decide between efficiency and robustness. This is a question of VBA in Excel.

I have a function, GetTable (below) that takes a table name and returns the corresponding ListObject within my Workbook. I prefer this method to explicitly calling the ThisWorkbook.Worksheet.ListObjects("strTableName") because it provides coding flexibility if tables get moved into different sheets.

Originally this was done by looping through each Worksheet and checking whether each ListObject had a matching name with the provided input. This is Option 1 of the code below. This method works well enough though it's not particularly efficient and can slow things down if you have a large workbook with many sheets and tables, and you're grabbing multiple tables in your macro.

To improve efficiency I changed to Option 2, which explicitly calls the table on each sheet. If the table doesn't exist then it throws an error, and error handling allows it to move directly to the next Worksheet. This works perfectly for normal usage of the workbook when error handling is turned on. However when error handling is turned off during debugging this becomes a pain because the code will always get stuck here.

Questions

  1. Is there a way I can programmatically tell if error handling is turned on or off so the function can switch between these two methods depending on the case? I realize this can be risky because it'll run through different code during debugging, but I'd still like to know if this is possible.
  2. If not, is there another way I can accomplish a similar method to Option #2 (or a more efficient one if you have an idea!) without throwing an error?

I know for this function the efficiency gains often won't matter unless you a ridiculous amount of tables, but I share this as part of a larger library of functions for less code savvy coworkers so they may not use it effectively and include it in large loops. Also finding the right approach may come in handy for applying to other functions.

Thanks in advance and good health to you!

Code

Function GetTable(strTableName As String) As ListObject
'This function searches the workbook for a table with the exact name strTableName
'Returns the table object
'If nothing found then display message
On Error Resume Next

Dim sht As Worksheet
Dim tbl As ListObject

'#Option 1: Slower but doesn't throw errors
'For Each sht In ThisWorkbook.Worksheets
'    For Each tbl In sht.ListObjects
'        'Debug.Print sht.Name & " " & tbl.Name      'uncomment to print all table names
'
'        If LCase(tbl.Name) = LCase(strTableName) Then
'            Set GetTable = tbl
'            Exit Function
'        End If
'    Next tbl
'Next sht

'#Option 2: More efficient but causes problems when debugging
For Each sht In ThisWorkbook.Worksheets
    
    Set GetTable = sht.ListObjects(strTableName) 'Generates runtime error 9 if table doesn't exist on sheet
    
    If Err.Number = 0 Then Exit Function  'No error means we've found the answer
    
    Err.Clear

Next sht

'If the code reaches this point it means the table wasn't found.
'This may have negative implications depending on where this function is called.
'This message gives the user an out
Dim ans As Byte
ans = MsgBox("Could not find table with name '" & strTableName & "'." & vbNewLine & vbNewLine & _
      "Would you like to abort code?", vbCritical + vbYesNo, "Table not found")
      
If ans = vbYes Then End

'Set GetTable = Nothing   '#This is redundant

End Function
Community
  • 1
  • 1
DavidN
  • 630
  • 1
  • 4
  • 13
  • Use error handling. Just before `Set GetTable =...` line, add the following: `On Error Resume Next`. Just after (and before `If Err.Number = 0...` line) add the following: `On Error Goto 0`. Now even if that line throws an error, your code wont stop and `If Err.Numbre...` line will get triggered – Zac Mar 27 '20 at 15:05
  • @Zac thanks for the suggestion, but this doesn't resolve the issue when error handling is off. Also `On Error Resume Next` already exists at the beginning of the function so it's already in effect. Moving it right before `Set GetTable` doesn't make a difference. – DavidN Mar 27 '20 at 15:08
  • I missed the initial `On Error Resume Next` statement and you are correct, if you already have it, there is no point of having it there. Just out of curiosity, why is your error handling off? – Zac Mar 27 '20 at 15:13
  • To trace the source of an unexpected (but handled) error and resolve the underlying issue. – DavidN Mar 27 '20 at 15:29
  • 1
    You can use the "slow" option and add a static dictionary to your function to cache the tables by name: it will only be "slow" the first time you call it. Of course if you're adding new tables you'll need to manage that... Or change your error handlers to `If Not DEBUG Then On Error Goto whatever` and have a global DEBUG flag variable. – Tim Williams Mar 27 '20 at 15:31
  • @TimWilliams, thanks for the suggestions. Can you clarify what you mean by the "slow" option? Are you just calling that method slow or is there some VBA mechanic like `Option Slow` that I'm unaware of? Also by caching the table names do you mean actually typing them into the code? – DavidN Mar 27 '20 at 15:42
  • The "slow" was your description of Option 1 "loop over all tables until you find a matching name". – Tim Williams Mar 27 '20 at 15:55

3 Answers3

2

This is what I mean by cache:

Function GetTable(ByVal strTableName As String, _
                         Optional reset As Boolean = False) As ListObject
    Static dict As Object 'Static, so persists between calls
    Dim sht As Worksheet
    Dim tbl As ListObject, nm
    If reset Then Set dict = Nothing  '<< clear the cache
    If dict Is Nothing Then
        Set dict = CreateObject("scripting.dictionary")
        For Each sht In ThisWorkbook.Worksheets
            For Each tbl In sht.ListObjects
                nm = LCase(tbl.Name)
                If Not dict.exists(nm) Then dict.Add nm, tbl
            Next tbl
        Next sht
    End If
    strTableName = LCase(strTableName)
    If dict.exists(strTableName) Then Set GetTable = dict(strTableName)
End Function

The first time you call it, it scans all the listobjects, but after that it will use the dictionary as a lookup.

You need to be aware of when you might need to clear the cache, to take into account added or deleted listobjects.

Tim Williams
  • 154,628
  • 8
  • 97
  • 125
1

Replace If ans = vbYes Then End with Replace If ans = vbYes Then Exit Function because End is the "self destrcution" button for the code. Look in the MS documentation for further reading.

The End statement stops code execution abruptly, without invoking the Unload, QueryUnload, or Terminate event, or any other Visual Basic code. Code you have placed in the Unload, QueryUnload, and Terminate events of forms and class modules is not executed. Objects created from class modules are destroyed, files opened by using the Open statement are closed, and memory used by your program is freed. Object references held by other programs are invalidated.

PS If you are talking about the error option which can be set via the VBE you might be out of luck, see Set the appropriate error handling level

Storax
  • 11,158
  • 3
  • 16
  • 33
  • Your linked edit was exactly what I was trying to find, thank you. Good to know this is an option in other applications just not Excel. And to your first point as I'm sure you've figured out `End` was the desired statement here. Exit function is what's going to happen otherwise. – DavidN Mar 27 '20 at 15:15
  • Ok, I was not sure if `End` was on intention. Normally one should avoid it unless you really want to end the program completely. – Storax Mar 27 '20 at 15:17
1

More as a comment to your question about a more efficient approach....

I personally don't like this approach because of the unqualified Range call and the Activate, but you may be able to use a function like the following, instead of looping through all the worksheets and tables:

Private Function GetTable(ByVal TableName As String, Optional ByVal wb as Workbook) As ListObject
    If wb Is Nothing Then Set wb = ThisWorkbook
    wb.Activate
    Set GetTable = Range(TableName).ListObject
End Function

Of course making this more robust in case no table with that name exists in the workbook... (which basically means I haven't answered the crux of your question).

BigBen
  • 46,229
  • 7
  • 24
  • 40
  • I didn't realize you could access ListObjects this way so thank you, and this is certainly more efficient. I have to agree though about not liking `.Activate` and unqualified `Range`. One question though, is there a reason you didn't specify `ByVal TableName as String`? – DavidN Mar 27 '20 at 15:47