Can an Excel VBA dictionary be used to call a function? - vba

I have a large number of functions that I need to call which each have the same arguments and I'd like to be able to centralize them to avoid unwieldy blocks of code. Obviously I could use a wrapper function to just call all the others, but I don't always call all of them. My next thought is that I could place the functions in a list or dictionary and call them from there, much like in Python:
def foo():
return "foo"
myDict = { "foo": foo }
myDict["foo"]()
Returns foo
Is something similar possible in VBA? If so, what is the simplest way of doing it?

If the functions are inside objects/Classes then you can call them by name directly.
The below code works inside SHEET1 because it's an object.
It will not work in a MODULE as that is outside the capabilities of CALLBYNAME function.
So you can store the Names in the Key of the dictionary and then just use the key (no function pointer is needed)
Public Sub Something(arg1 As String)
MsgBox arg1
End Sub
Public Sub test()
CallByName Sheet1, "Something", VbMethod, "data 1"
End Sub

There does not appear to be a really straight forward way of doing this, however, it can be done using a combo of a list and the CallByName function.
You can make a list which contains all the function names as strings:
myFunctions = ( "foo" "bar" "baz" )
You can then iterate over the array to call the functions:
For Each functionName in myFunctions
CallByName Sheet1, functionName, VbMethod, listOfArguments
Next functionName
Functions can be filtered out with If statements

Related

Custom Function Disable in Excel

I have created a custom function that I am using excplicitly in another module in VBA. Function looks something like this:
Function Coverts(ByVal inputString As String) As String
'Coverts code here
End Function
It works perfectly fine in both VBA and Excel UI. However, I do not want it to work or appear in Excel UI as I only want to use it in VBA. Is there a way to do this?
Thanks
Put
Option Private Module
at the top of the module containing your function.
From MSDN:
When a module contains Option Private Module, the public parts, for example, variables, objects, and user-defined types declared at module level, are still available within the project containing the module, but they are not available to other applications or projects.
you can add the keywords Public or Private to your functions, subs or global variable to have that specified.
so if you want to only want this function to be accessible by your code and not in excel sheets you can add private:
Private Function Coverts(ByVal inputString As String) As String
'Coverts code here
End Function
You can make the function inoperable if called from the worksheet by identifying the Application.Caller. If called as a function from the XL UI, this will be Range (i.e. the cell the function is within).
Function Coverts(ByVal inputString As String) As String
If TypeName(Application.Caller) = "Range" then
Coverts = cverr(xlerrna)
exit function
end if
'Coverts code here
End Function

calling a function from a form into a module

I have a an array in my module, so I want to display the contents of my array in a form textbox, here is my array
Module Module1
Sub AddCourse()
Dim Subjects() = {"Ms Office 2007", "internet and commmunications", "Lifetime skills"}
For i = 0 To UBound(Subjects) ' FOR LOOP TO WRITE AN ARRAY
i = i +1
Subjects(i)
Next
txtComputer.Text = subjects()
my problem is, when I try to use my texbox txtComputer in my module I get an error.
My question is, how do I make a form textbox to be used in a module
I get an error that reads "Error'txtComputer' is not declared. It may be inaccessible due to its protection level."
My question is based on, how do I get this error fixed?
There are several suggestions I have for you.
First, don't use UBound. That's an old VB6 function that is only provided for backwards compatability. You should instead use Subjects.Length.
Next, when you're incrementing the i variable, you don't need to say i = i + 1. You can just use the += operator for that (e.g. i += 1).
However, you shouldn't be explicitly incrementing i inside your For loop, anyway. The loop automatically increments the variable for you each time it iterates through the loop. If you do it explicitly yourself inside the loop, like that, it will skip every other item.
Next, in this case, you really should just use a For Each loop, rather than an iterator:
For Each subject As String in Subjects
'...
Next
Next, you aren't actually concatenating the items together inside you loop. You should be doing something like this:
For Each subject As String in Subjects
txtComputer.Text += subject
Next
However, in that case, for efficiency sake, you really ought to use a StringBuilder, like this:
Dim builder As New StringBuilder()
For Each subject As String in Subjects
builder.Append(subject)
Next
txtComputer.Text = builder.ToString()
But, all of this is moot because all you really need to do is to call String.Join:
txtComputer.Text = String.Join(", ", Subject)
As far as why you can't access the text box from the module, that is because the module is a separate object, so the text box is entirely out of scope. For instance, what if you had two instances of your form displayed at the same time? How in the world would this module know which form's text box you were referring to? The simplest way to correct that would be to pass a reference to your form into the module's method, like this:
Module Module1
Sub AddCourse(f As MyFormName)
f.txtComputer.Text = "Hello world"
End Sub
End Module
And then you could call it from the form, like this:
AddCourse(Me)
However, that would be exceptionally bad practice. Ideally, nothing outside of the form's code should ever deal directly with any of the controls on the form. So, the far better way to do it would be to simply have the method return the data, and then have the form set it's own control to the data that is returned, for instance:
Module Module1
Function GetCourse() As String
Return "Hello world"
End Function
End Module
And then call it from the form like this:
txtComputer.Text = GetCourse()
You can use String.Join to create a string which separates each subject with Environment.NewLine:
txtComputer.Text = String.Join(Environment.NewLine, Subjects)
The problem with your for loop is that it makes no sense at all since you have already declared and initialized the array in one line.
If you want to use a loop anyway, you can use a StringBuilder to concat all strings together:
Dim subjectBuilder = New System.Text.StringBuilder
For Each subject In Subjects
subjectBuilder.Append(subject).Append(Environment.NewLine)
Next
If subjectBuilder.Length <> 0 Then subjectBuilder.Length -= Environment.NewLine.Length
txtComputer.Text = subjectBuilder.ToString()

Hiding/replacing code in VBA

I have a large chunk of code that manually defines each element of an array for me that is annoyingly long and positioned at the beginning of one of my functions. I would like to hide the code or set it somewhere else WITHOUT changing its current meaning in any way if possible. I would like to avoid making the array global. It's also not reasonable to pass the array from all the places that the function is called.
Is there some way to simply have the code sit somewhere else while VBA sees it as being a part of the function, i.e. as if I had all the elements defined at the beginning of the function? I imagine having some sort of "Sub" that's not actually a Sub (I might call it an "Excerpt") of code with the elements populated there with a single line in the function that calls the "Excerpt" by name.
You can return an array from a function so it could be in a module on its own;
public Function getArr() as string()
Dim arr(10) as string
...
arr(5) = "Cakey"
getArr = arr
End Function
Called with
Dim arry() as string: arry = getArr()
msgbox arry(5)

Calling a Sub or Function contained in a module using "CallByName" in VB/VBA

It is easy to call a function inside a classModule using CallByName
How about functions inside standard module?
''#inside class module
''#classModule name: clsExample
Function classFunc1()
MsgBox "I'm class module 1"
End Function
''#
''#inside standard module
''#Module name: module1
Function Func1()
MsgBox "I'm standard module 1"
End Function
''#
''# The main sub
Sub Main()
''# to call function inside class module
dim clsObj as New clsExample
Call CallByName(clsObj,"ClassFunc1")
''# here's the question... how to call a function inside a standard module
''# how to declare the object "stdObj" in reference to module1?
Call CallByName(stdObj,"Func1") ''# is this correct?
End Sub
I think jtolle's response addressed the question best - the small reference to Application.Run may be the answer. The questioner doesn't want to use simply func1 or Module1.func1 - the reason one would want to use CallByName in the first place is that the desired function.sub name is not known at compile time. In this case, Application.Run does work, e.g.:
Dim ModuleName As String
Dim FuncName As String
Module1Name = "Module1"
FuncName = "func1"
Application.Run ModuleName & "." & FuncName
You can also prepend the Project Name before the ModuleName and add another period ".".
Unfortunately, Application.Run does not return any values, so while you can call a function, you won't get its return value.
Although it is an old question and OP asked for CallByName in a standard module, the correct pieces of advice are scattered through answers and comments, and some may not be that accurate, at least in 2020.
As SlowLearner stated, Application.run DOES return a Variant, and in that way both branchs below are equivalent, except by handling errors, as commented around Horowitz's answer:
Dim LoadEnumAndDataFrom as Variant
'FunctionName returns a Variant Array
if fCallByName then
LoadEnumAndDataFrom = CallByName(ClassObj, "FunctionNameAtClass", VbMethod)
else
'After moving back function for a standard module
LoadEnumAndDataFrom = Application.Run("StandardModuleName" & "." & "FunctionNameAtStandard")
endif
I actually just did this above and had no errors at all, tested in Word, Excel and Access, and all return the same Array.
Unfortunately, there is an exception: Outlook's object Model is too protected and it does not have the Run method.
CallByName works only with class objects.
If your subroutine is in a standard module, you can do this:
Sub Main()
Module1.Func1
End Sub
If it's a function, then you'll probably want to capture the return value; something like this:
Sub Main()
Dim var
var = Module1.Func1
End Sub
Modules in VB6 and VBA are something like static classes, but unfortunately VB doesn't accept Module1 as an object. You can write Module1.Func1 like C.Func1 (C being an instance of some Class1), but this is obviously done by the Compiler, not at runtime.
Idea: Convert the Module1 to a class, Create a "Public Module1 as Module1" in your Startup-module and "Set Module1 = New Module1" in your "Sub Main".
Unfortunately it is not possible to prepend the ProjectName before the ModuleName and add another period "." In MS Word this throws a runtime error 438. The call is restricted to the use of simply ModuleName.ProcName.

How can I evaluate a string into an object in VBA?

In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.