VBA: evaluation order - vba

when VBA executes this line:
GetClass1().Test(GetParam())
the GetParam function is evaluated before the GetClass1() call.
What is a good way to change this behaviour?
the only thing I came up with is this workaround:
With GetClass1
.Test(GetParam())
End With
here's the full example code, so that you can easily test it:
Class1
Option Explicit
Public Function Test(ByVal sText As String) As String
Debug.Print "Class1.Text: " & sText
Test = "Class1.Text: " & sText
End Function
Module1
Option Explicit
Private Function GetClass1() As Class1
Set GetClass1 = New Class1
Debug.Print "GetClass1()"
End Function
Private Function GetParam() As String
GetParam = "Param"
Debug.Print "GetParam()"
End Function
Private Sub Test()
Debug.Print "Test=" + GetClass1().Test(GetParam())
With GetClass1
Debug.Print "TestWith=" + .Test(GetParam())
End With
End Sub
Output when you run Test()
GetParam()
GetClass1()
Class1.Text: Param
Test=Class1.Text: Param
GetClass1()
GetParam()
Class1.Text: Param
TestWith=Class1.Text: Param

The evaluation order here is ok i think. The calling order of nested functions is from the inner most one to the outer most one which can't be done differently because outer most function needs to know its arguments and this arguments are evalueted only after the inner function was executed.
In your code (the first way) the object of type Class1 is created after the function GetParam() was called and this is because the object is created at the moment when function GetClass1() is called. In the second way with With GetClass1 the object is created immedialtelly after With and the call stack looks differently indeed.
What you can do is to create another class say 'Wrap' and this class will be responsible for creation of instance of type Class1.
E.g. like this:
' Class module Wrap
Private m_class1 As Class1
Public Function GetClass1() As Class1
Set GetClass1 = m_class1
Debug.Print "GetClass1()"
End Function
Private Sub Class_Initialize()
Set m_class1 = New Class1
End Sub
' Module code
Private Function GetParam() As String
GetParam = "Param"
Debug.Print "GetParam()"
End Function
Private Sub Test()
Dim wp As Wrap
Set wp = New Wrap
Debug.Print "Test=" + wp.GetClass1().Test(GetParam())
Debug.Print "---------------------------------------"
With New Wrap
Debug.Print "TestWith=" + .GetClass1.Test(GetParam())
End With
End Sub
Here the instance of class Class1 is created exactly at the moment you call New for class Wrap. So Set wp = New Wrap executes and creates the instance and the same way works With New Wrap, it executes and creates the instance as well.
But do not do it like this:
Dim wp As New Wrap
... then you will have the same behaviour like you had when the function GetClass1() was part of the Module1, which is: 'the instance is not created until it is needed' so you do not have the control of the moment of creation. HTH

Related

Access VBA tries to terminate return value of Function

I'm building an Access database using some VBA code.
I've made a very simple class and a function which creates an instance of that class. Executing this function yields an error 91: object variable or With block variable not set. After debugging I found out Access tries to terminate the newly created object, ending the terminate function results in the error.
My intention was to create a factory for MyObject in order to create an object and initialize it with given arguments. I've stripped all this away to find the problem and now I'm dumbfounded. Can someone point out what I'm doing wrong because I have no idea anymore.
The class I made, called MyObject:
Private Sub Class_Initialize()
'does literally nothing
End Sub
Private Sub Class_Terminate()
'does literally nothing
End Sub
The function I made to initialize the class, located in another module:
Public Function createMyObject(someArg As String) As MyObject
Set createMyObject = New MyObject
End Function
I obviously expected the function createMyObject to return an instance of MyObject, but it gives me an error 91. Debugging led me to the Class_Terminate Sub in MyObject where the error gets thrown at the "end sub" line.
Try with using a property:
Option Compare Database
Option Explicit
Private mTest As Variant
Private Sub Class_Initialize()
'does literally nothing
End Sub
Private Sub Class_Terminate()
'does literally nothing
End Sub
Public Property Get Test() As Variant
Test = mTest
End Property
Public Property Let Test(ByVal NewValue As Variant)
mTest = NewValue
End Property
and then:
Public Function CreateMyObject(SomeArg As String) As MyObject
Set CreateMyObject = New MyObject
CreateMyObject.Test = SomeArg
End Function
which you can call like this:
Set o = CreateMyObject("Joe")
Debug.Print o.Test

Is there a way to make a setable function in a user defined object

Is there a way to set an object method like a property?
I want to be able to just call the function like myObj.myFunc() but I want to set where myFunc will point when i instantiate myObj. As of right now i have it as a public event that i can add a handler to at init but it does seem like the best option.
As #VisualVincent said:
Module Module1
Sub Main()
Dim myObj As New myObj(Sub() Console.WriteLine("1"))
' OR
Dim myObj2 As New myObj(AddressOf myFunc)
myObj2.myFunc.Invoke
End Sub
Sub myFunc()
Console.WriteLine("2")
End Sub
End Module
Class myObj
Public Sub New(myFunc As Action)
Me.myFunc = myFunc
End Sub
Property myFunc As Action
End Class

Calling private sub from module, concatenated reference

I have a couple of forms (i.e. frmTest) with bound comboboxes (i.e. cboTest). I'm trying to solve the NotInList event by a public sub, which calls back a button click sub's in these forms (i.e. btnTest_Click).
Form frmTest:
Private Sub cboTest_NotInList(NewData As String, Response As Integer)
Response = acDataErrContinue
Item_NotInList NewData, Me, "btnTest"
End Sub
Public Sub btnTest_Click
'....
End sub
Module:
Public strNotInList_Text As String
'public variable to store entered text
Public Sub Item_NotInList (strNewData As string, frmForm As Form, strControl As String)
Dim strControl_Sub As String
strNotInList_Text = strNewData
strControl_Sub = "." & strControl & "_Click"
Application.Run frmForm.Name & strControl_Sub
End Sub
Acces returns an error "Program ... didn't found a procedure frmTest.btnTest_Click."
Why ?
Reference frmTest.btnTest_Click looks to be correct. Sub btnTest_Click is declared as public.
Thank you for yor help.
Couldn't you create an interface let say IMyInterface with this method btnTest_Click (should be named differently) and let the forms you want to call this method implement this interface. Then change the signature of Item_NotInList like this:
Public Sub Item_NotInList (strNewData As string, frmForm As IMyInterface, strControl As String)
' ...
frmForm.btnTest_Click
' ...
Because the instance of the form is available in the method Item_NotInList you simply call the target method. Does this help?
Example:
Add class module and name it e.g. IMyInterface (can be named according to your needs). Add empty body of the method (don't add any implementation).
IMyInterface
Public Sub TestClick()
' will be implemented in your forms
End Sub
Then implement this interface in your forms e.g. in Form frmTest and others which should be used with the Item_NotInList method.
Form frmTest example
Implements IMyInterface
Private Sub IMyInterface_TestClick()
' here goes your implementation
End Sub
Standard Module test code
Sub test()
Dim f1 As UserForm1
Set f1 = New UserForm1
Item_NotInList f1
Dim f2 As UserForm2
Set f2 = New UserForm2
Item_NotInList f2
End Sub
Sub Item_NotInList(testForm As IMyInterface)
testForm.TestClick
End Sub
Thats it. HTH

Expression in With statement becomes Nothing in lambda expression in Task

I have discovered that referencing a member variable in a lambda expression executed in a Task throws a NullReferenceException when accessing it using the With statement.
For example I expect the following code to print two lines on the console. The first one accesses the SomeString member via obj.SomeString while the second one uses the With statement and accesses the member via .SomeString. I expected both options to be equivalent but the second one throws an exception.
Class Foo
Public SomeString As String
End Class
Module Module1
Sub Main()
Dim obj As New Foo With {.SomeString = "Hello World"}
With obj
Task.Factory.StartNew(
Sub()
Console.WriteLine("1:" + obj.SomeString) ' works
Console.WriteLine("2:" + .SomeString) ' NullReferenceException here
End Sub)
End With
Console.ReadKey()
End Sub
End Module
When I move the Console.ReadKey() statement into the With statement, the code works.
I fixed the actual code by not using the With statement but I still don't know what concept I'm missing here. Why can I access members of the obj variable in the lambda expression but not the members of the With expression? It has not been garbage collected because I can still see it in the debugger when the exception is thrown. The expression seems to go out of scope (or something like that) but why doesn't the compiler just do what I expect and treats it the same as obj?
It is because of the voodoo that the VB compiler does to support the With block and lambda expressions. If you look at your code through a decompiler like Redgate's Reflector, your code gets converted into something like the code below except that I renamed the variables to ones supported by VB; they can be quite long and include characters that are invalid for VB variable names
<STAThread> _
Public Shared Sub Main()
Dim var1 As New GeneratedClass1
Dim foo As New Foo With {.SomeString = "Hello World"}
var1.objVar = foo
Dim var2 As New GeneratedClass1.GeneratedClass2 With {.var2 = var1, .theWithVariable = var1.objVar}
Task.Factory.StartNew(New Action(AddressOf var2._Lambda___1))
var2.theWithVariable = Nothing
Console.ReadKey()
End Sub
<CompilerGenerated> _
Friend Class GeneratedClass1
' Methods
<DebuggerNonUserCode> _
Public Sub New()
End Sub
<DebuggerNonUserCode> _
Public Sub New(ByVal other As GeneratedClass1)
If (Not other Is Nothing) Then
Me.objVar = other.objVar
End If
End Sub
' Fields
Public objVar As Foo
' Nested Types
<CompilerGenerated> _
Friend Class GeneratedClass2
' Methods
<DebuggerNonUserCode> _
Public Sub New()
End Sub
<DebuggerNonUserCode> _
Public Sub New(ByVal other As GeneratedClass2)
If (Not other Is Nothing) Then
Me.theWithVariable = other.theWithVariable
End If
End Sub
<CompilerGenerated> _
Public Sub _Lambda___1()
Console.WriteLine(("1:" & Me.var2.objVar.SomeString))
Console.WriteLine(("2:" & Me.theWithVariable.SomeString))
End Sub
' Fields
Public theWithVariable As Foo
Public var2 As GeneratedClass1
End Class
End Class
You can see that the compiler creates a class that holds a reference to the With variable and the method of the lambda expression. As soon as the With variable is out of scope it is set to Nothing and hence the null reference expression when task executes.

VBA Factory method with Private Variables

So I Have a Factory Class with method Create_product
Product happens to have Private Variables that I would like to be set by Factory however if they are private how can I make Factory access them?
Preferably I want it to be impossible to change them after new instance of Product is created.
Friend
Modifies the definition of a procedure in a form module or class module to make the procedure callable from modules that are outside the class, but part of the project within which the class is defined. Friend procedures cannot be used in standard modules.
Syntax
[Private | Friend | Public] [Static] [Sub | Function | Property] procedurename
The required procedurename is the name of the procedure to be made visible throughout the project, but not visible to controllers of the class.
Remarks
Public procedures in a class can be called from anywhere, even by controllers of instances of the class. Declaring a procedure Private prevents controllers of the object from calling the procedure, but also prevents the procedure from being called from within the project in which the class itself is defined. Friend makes the procedure visible throughout the project, but not to a controller of an instance of the object. Friend can appear only in form modules and class modules, and can only modify procedure names, not variables or types. Procedures in a class can access the Friend procedures of all other classes in a project. Friend procedures don't appear in the type library of their class. A Friend procedure can't be late bound.
Use a singleton class as is 1
Create form module named Singleton:
Private SingleInsts As Collection
Private instCount As Double
Public isCalled As Boolean
Private Sub UserForm_Initialize()
Me.Hide
Me.isCalled = False
End Sub
Private Function setInstance(name As String) As Object
Dim Obj As Object
If SingleInsts Is Nothing Then Set SingleInsts = New Collection
instCount = SingleInsts.Count
ReDim singleNames(instCount + 1)
Me.isCalled = True
Select Case name
Case "Example"
Set Obj = New Example
Case "Other"
Set Obj = New Other 'etc. etc - Case ... for all classes
Case Else
Err.Raise vbObjectError + 800, Err.Source & "|" & Me.name & "." & "setInstance", name & " is not a classname"
End Select
Me.isCalled = False
SingleInsts.Add Obj, name
Set setInstance = Obj
Exit Function
End Function
Public Function getInstance(name As String) As Object
On Error Resume Next
If (SingleInsts(name) Is Nothing) And False Then
'this way only by error - when SingleInsts(name) doesn't exist
Set getInstance = setInstance(name)
Else
Set getInstance = SingleInsts(name)
End If
End Function
Public Function errNew(errstr As String)
Err.Raise vbObjectError + 703, errstr
End Function
This function has to be in all classes:
Private Sub Class_Initialize()
If Not Singleton.isCalled Then Singleton.errNew TypeName(Me)
End Sub
And then call "factory" method to get object (an object can be instantiated only once - see getInstance method in Singleton)
Sub fo()
set alfa = Singleton.getInstance("Example")
End sub