Constructor in VBA - Runtime error 91 "Object variable not set" - vba

I am trying to write some code in excel VBA using the Object Oriented Concept. Therefore I wanted to initialize my objects with constructors, like we usually do in Java. However I discovered that the default Class_Initialize() Sub that is offered in VBA does not take arguments. After searching a bit, I found that the answer for this Question proposed a pretty good alternative.
Here is a sample of my Factory Module (I Named it Creator):
Public Function CreateTool(ToolID As Integer) As cTool
Set CreateTool = New cTool
CreateTool.InitiateProperties (ToolID) '<= runtime error 91 here
End Function
The class cTool:
Private pToolID As Integer
Private pAttributes As ADODB.Recordset
Private pCnn As ADODB.Connection
Public Sub InitiateProperties(ToolID As Integer)
Dim sSQL As String
Set pCnn = connectToDB() 'A function that returns a connection to the main DB
pToolID = ToolID
sSQL = "SELECT Tool_ID, Status, Type, Tool_Number " _
& "FROM Tool WHERE Tool_ID = " & pToolID
pAttributes.Open sSQL, pCnn, adOpenKeyset, adLockOptimistic, adCmdText
End Sub
This is how I call the constructor:
Dim tool As cTool
Set tool = Creator.CreateTool(id)
My issue is that when I run the code, I get the following error:
Run-Time error '91' : Object Variable or With Block Variable not Set
The debug highlights the CreateTool.InitiateProperties (ToolID) line of my CreateTool Function.
I know that this usually happens when someone is setting a value to an object without using the keyword Set but it does not seem to be my case.
Any help, advice to resolve this issue would be greatly appreciated!
Thanks.

Might not be the cause of your error, but this:
Public Function CreateTool(ToolID As Integer) As cTool
Set CreateTool = New cTool
CreateTool.InitiateProperties (ToolID) '<= runtime error 91 here
End Function
Is problematic for a number of reasons. Consider:
Public Function CreateTool(ByVal ToolID As Integer) As cTool
Dim result As cTool
Set result = New cTool
result.InitiateProperties ToolID
Set CreateTool = result
End Function
Now, looking at the rest of your code, you're doing the VBA equivalent of doing work in the constructor, i.e. accessing database and other side-effects to constructing your object.
As #Jules correctly identified, you're accessing the unitialized object pAttributes inside InitiateProperties - that's very likely the cause of your problem.
I'd strongly recommend another approach - if you come from Java you know doing work inside a constructor is bad design... the same applies to VBA.
Get your code working, and post it all up on Code Review Stack Exchange for a full peer review.

Related

VBA using function return values instead of variables

I've tried searching the internet for a definitive answer to this two-part scenario but couldn't find anything conclusive. I've been writing VBA procedures for sometime now in both Access and Excel and while trying to streamline some code I came across something of a dilemma
The first part is about using functions to return objects. The example below is generally what I've seen on the web for a function to return an ADODB.Recordset (I've simplified the code so no error handling etc.).
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Dim rst As ADODB.Recordset
Set rst = New ADODB.Recordset
Call rst.Open(SQL, Connection)
Set CreateADORecordset = rst
End Function
The first part of the question is, why do I need a variable called rst when I could rewrite the function as this:
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, Connection)
End Function
Is there anything fundamentally wrong with the above rewrite of the function? As the function returns an ADO recordset, why declare a variable separately?
I can take this a step further:
Public Function CreateADOConnection(ConnectionString As String) As ADODB.Connection
Set CreateADOConnection = New ADODB.Connection
Call CreateADOConnection.Open(ConnectionString)
End Function
Public Function CreateADORecordset(SQL As String, ConnectionString As String) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, CreateADOConnection(ConnectionString))
End Function
Again, is this a particularly bad thing to do by using function return objects over declaring objects within the procedures via Dim?
In the grand scheme of things, I've been writing VBA code to transfer the contents of a recordset via GetRows into an Excel range. The function declaration line is:
Public Sub TransferRecordsetArray(GetRows As Variant, Destination As Range)
As TransferRecordsetArray works correctly, I've not included the code.
The dilemma I'm in now is in this scenario, I've reached a point where I don't need to declare any variables for the code to run correctly and I'm unsure how much of a good or bad thing this in terms of functions returning objects, scope and variables, etc.
In order to run the code correctly, I only need one of two lines without variables:
Call TransferRecordsetArray(CreateADOConnection(ConnectionString).Execute(SQL).GetRows, Target)
or
Call TransferRecordsetArray(CreateADORecordset(SQL, CreateADOConnection(ConnectionString)).GetRows, Target)
Any advice/recommendations on this way of writing VBA code would be greatly appreciated. I have used the task manager to keep an eye on memory usage on both methods and it doesn't seem to greatly differ and, it appears that VBA destroys the function return objects after a while, despite them not being explicitly destroyed by setting them to Nothing.
Many thanks.
The first part of the question is, why do I need a variable called rst when I could rewrite the function as this
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, Connection)
End Function
You don't need a separate variable. Your code is perfectly fine.
I can take this a step further:
Public Function CreateADOConnection(ConnectionString As String) As ADODB.Connection
Set CreateADOConnection = New ADODB.Connection
Call CreateADOConnection.Open(ConnectionString)
End Function
Public Function CreateADORecordset(SQL As String, ConnectionString As String) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, CreateADOConnection(ConnectionString))
End Function
Yes, absolutely. Nothing wrong with that.
I've reached a point where I don't need to declare any variables for the code to run correctly
Congratulations, keep it up. :)
Further reading: Is there a need to set Objects to Nothing

VB .NET NullReferenceException, "Object reference not set to an instance of an object" when it seemingly is

as the title reads, I am getting a NullReferenceException that says "Object reference not set to an instance of an object". This is a legacy WinForms app that I am working on so keep in mind the structure of the code is not my doing. I am trying to compare a recordset before and after a change is made to it. I am hitting my issue in trying to make a copy of the old recordset.
Here is my code to populate the copy of the old data, which is in a module:
Public Function Prevdata(ByRef table As ADODB.Recordset) As ADODB.Recordset
Dim i As Integer
Dim pData As New ADODB.Recordset
table.MoveFirst()
If Not table.BOF And table.EOF Then
While Not table.EOF
For i = 0 To table.Fields.Count - 1
pData.AddNew()
pData(table.Fields(i)).Value = table.Fields(i).Value
Next
pData.Update()
table.MoveNext()
End While
End If
PrevData = pData
End Function
And here is my code in one of the forms. These are just with my other variable declarations at the top of the file. GetData just simply creates a recordset:
Public pData As ADODB.Recordset = GetData(tableName)
Public pDataC As ADODB.Recordset = PrevData(pData)
I get the error in the module in PrevData anywhere where "table" is referenced. I appreciate any help.

Creating a new IUI Automation Handler object in order to subscribe to an automation event

So, here it goes. To start, A disclaimer, I understand that MS Access is not built for this kind of work. It is my only option at this time.
I have done just a bit of Automation using UIAutomationClient and I have successfully used its other features, however I cannot for the life of me get it to subscribe to events.
Normally, it is supposed to be a bit like this:
Dim CUI as new CUIAutomation
Dim FocusHandler as IUIAutomationFocusChangedEventHandler
Set FocusHandler = new IUIAutomationFocusChangedEventHandler(onFocusChanged)
C.AddFocusChangedEventHandler(Element,TreeScope_Children, null, FocusHandler)
end function
'
'
Function onFocusChanged(src as Object, args as AutomationEventArgs)
''my code here
end function
Yet when I attempt this, I get the error "expected end of statement" on the line:
FocusHandler = new IUIAutomationFocusChangedEventHandler(onFocusChanged)
additionally, if I leave off the (onFocusChanged) I get the error "Invalid use of new Keyword".
It seems like I am missing a reference somewhere. The usual drop down when using "new" does not contain the IUI handler classes though they are in the object library.
I am not sure if there is just some piece I am not accounting for in the code since I am using vba, but all examples seem to be for .net or C#/C++. Any help would be appreciated.
Additionally, I have no problem finding the element in question and all other pieces work fine. If you need any other pieces of the code let me know.
Edit: added set to line 3. No change in the problem though.
After two years this probably isn't relevant any more, but perhaps somebody else encounters this problem... The answer is to create a new class that implements the HandleAutomationEvent method.
Here I created a class named clsInvokeEventHandler and (importantly) set the Instancing property to PublicNotCreatable:
Option Explicit
Implements IUIAutomationEventHandler
Private Sub IUIAutomationEventHandler_HandleAutomationEvent(ByVal sender As UIAutomationClient.IUIAutomationElement, ByVal eventId As Long)
Debug.Print sender.CurrentName
End Sub
And to use it:
Sub StartInvokeHandler()
Dim oUIA As New CUIAutomation8
Dim oRoot As IUIAutomationElement
Dim InvokeHandler As clsInvokeEventHandler
Set InvokeHandler = New clsInvokeEventHandler
Set oRoot = oUIA.GetRootElement
oUIA.AddAutomationEventHandler UIA_Invoke_InvokedEventId, oRoot, TreeScope_Descendants, Nothing, InvokeHandler
End Sub

Error in using QueryDef to define parameters in a query

I'm working in Access and trying to use a query with parameters in VBA. I have several queries that I need to use, so I added a routine to generalize the process:
Public Function Execute_query(query) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(query)
For Each prm In qdf.Parameters
prm.Value = Eval(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else: Set Execute_query = qdf.OpenRecordset
End If
End Function
I'm still testing this so there may be other issues, but my immediate question is why the Eval(prm.name) line isn't working. The paramater is [R_Yr] which I've declared as a public variable and have assigned a value - which I can verify in the watch window. But I get an error code 2482 - Access cannot find the name 'R_yr"
This same code seems to work when the parameter value is coming from a form instead of a variable - which is why I had to set it up in the first place - I couldn't access a form control in a query run from VBA.
Forgive me, but I'm having a bit of trouble seeing the real benefit from this extra level of indirection. You have to create variables that correspond to the parameters for the particular query you are planning to invoke, and then you call a generic function to invoke it. Why not just create a QueryDef, pass it the parameters, and invoke it in place? It seems like essentially the same amount of work, and it makes the code easier to follow because "everything is right there".
Eval() takes the string you give it and uses the "expression service" to process it. The problem you're facing is the expression service doesn't know anything about VBA variables. If you're really determined, you may be able to figure out a workaround which builds the variable's value rather than the variable's name into the string you give Eval() to ... um ... evaluate.
But for what you're doing, I suggest you ditch Eval(). Instead give the function a data structure such as a Scripting.Dictionary or VBA Collection which contains the parameter values with your former variable names as keys.
Here is a VBA Collection example ...
Dim MyCol As Collection
Set MyCol = New Collection
MyCol.Add CLng(10), "R_Yr"
MyCol.Add "foo", "MyString"
Debug.Print MyCol("R_Yr"), TypeName(MyCol("R_Yr"))
Debug.Print MyCol("MyString"), TypeName(MyCol("MyString"))
That code gives me this output in the Immediate window ...
10 Long
foo String
So consider building a similar collection in the calling code and passing that collection to a modified Execute_query function.
Public Function Execute_query(ByVal pQdf As String, _
ByRef pCol As Collection) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(pQdf)
For Each prm In qdf.Parameters
prm.Value = pCol(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else
Set Execute_query = qdf.OpenRecordset
End If
End Function

Invalid Use of Property?

I am working with an Access database and it has a form and VBA behind it. It has been quite a while since I dabbled in VBA and my google-fu is failing me right now so bear with me.
I created a simple class, and I am getting a compile error:
Dim oRecordSet As ADODB.RecordSet
Public Property Get RecordSet() As ADODB.RecordSet
RecordSet = oRecordSet '' error here
End Property
Public Property Let RecordSet(ByVal val As ADODB.RecordSet)
RecordSet = val
End Property
I have a couple other identical properties (different names/variables, obviously) that compile just fine; their types are String and Integer.
What am I missing? Thanks!
Also a side note, when I am coding the intellisense shows ADODB.Recordset, but on autoformat (carriage return, compile, etc) it changes it to ADODB.RecordSet. Need I be worried?
It should be:
Public Property Get RecordSet() As ADODB.RecordSet
Set RecordSet = oRecordSet '' error here
End Property