Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom callbacks in VBA

Tags:

callback

vba

Note the tag: VBA, not VB6, not VB.NET.

This is specific to VBA in MS Access. I've built a collection of methods in an module I call "Enumerable". It does a lot of things reminiscent of the Enumerable classes and interfaces in .NET. One thing I want to implement is a ForEach method, analagous to the .NET Enumerable.Select method.

I built a version that uses the Application.Run method to call a function for each element, but Application.Run only works with user-defined methods. For example, the following works:

' User-defined wrapper function:
Public Function MyReplace( _
    Expression As String, Find As String, StrReplace As String, _
    Optional Start As Long = 1, _
    Optional Count As Long = 1, _
    Optional Compare As VbCompareMethod = vbBinaryCompare)
    MyReplace = Replace(Expression, Find, StrReplace, Start, Count, Compare)
End Function

' Using Application.Run to call a method by name
Public Sub RunTest()
    Debug.Print Run("MyReplace", "Input", "In", "Out")
End Sub

RunTest prints "Output", as expected. The following does NOT work:

Debug.Print Run("Replace", "Input", "In", "Out")

It throws run-time error 430: "Class does not support Automation or does not support expected interface". This is expected, because the documentation states that Application.Run only works for user-defined methods.

VBA does have an AddressOf operator, but that only works when passing function pointers to external API functions; function pointers created using AddressOf are not consumable in VBA. Again, this is noted in the documentation (or see for example VBA - CallBacks = Few Cents Less Than A Dollar?).

So is there any other way to identify and call a method using a variable? Or will my callback-ish attempts be limited to user-defined functions via the Application.Run method?

like image 844
Joshua Honig Avatar asked Jan 26 '12 15:01

Joshua Honig


1 Answers

No other answers in a week...for resolution's sake here's the best I could come up with:

  1. I built a helper module that resolves a ParamArray to individual arguments for the sake of calling CallByName. If you pass a ParamArray through to CallByName it will mash all the arguments into a single, actual Array and pass that to the first argument in the method you attempt to invoke.
  2. I built two ForEach methods: one that invokes Application.Run, and another that invokes CallByName. As noted in the question, Application.Run only works for user-defined global (public module) methods. In turn, CallByName only works on instance methods, and requires an object argument.

That still leaves me without a way to directly invoke built-in global methods (such as Trim()) by name. My workaround for that is to build user-defined wrapper methods that just call the built-in global method, for example:

Public Function FLeft( _
   str As String, _
   Length As Long) As String
    FLeft = Left(str, Length)
End Function

Public Function FLTrim( _
   str As String) As String
    FLTrim = LTrim(str)
End Function

Public Function FRight( _
   str As String, _
   Length As Long) As String
    FRight = Right(str, Length)
End Function

...etc...

I can now use these to do things like:

' Trim all the strings in an array of strings
trimmedArray = ForEachRun(rawArray, "FTrim")

' Use RegExp to replace stuff in all the elements of an array
' --> Remove periods that aren't between numbers
Dim rx As New RegExp
rx.Pattern = "(^|\D)\.(\D|$)"
rx.Global = True
resultArray = ForEachCallByName(inputArray, rx, "Replace", VbMethod, "$1 $2")   
like image 141
Joshua Honig Avatar answered Oct 17 '22 05:10

Joshua Honig