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
Related
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
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.
I wanted to first thank you all for the help you've given me implicitly over the last few months! I've gone from not knowing how to access the VBA IDE in Excel to writing fully integrated analysis programs for work. I couldn't have done it without the community here.
I'm currently trying to overhaul the first iteration of a data analysis program I wrote while learning how to code in VBA. While purpose driven and only really legible to myself, the code worked; but was a mess. From folks on this site I picked up Martin's Clean Code and gave it a read on how to try and be a better programmer.
From Martin's Clean Code, it was impressed on me to prioritize abstraction and decoupling of my code to allow for higher degrees of maintenance and modularization. I found this out the hard way since very minor changes requested above my pay grade would require massive and confusing rewrites! I'm trying to eliminate that problem going forward.
I am attempting to rewrite my code in terms of single responsibility classes (at least, where it is possible) and I am a bit confused. I apologize if my question isn't clear or if I'm using the wrong terminology. I want to be able to generate a collection of specific strings (the names of our detectors to be specific) with no duplicates from raw instrument data files from my lab. The purpose of this function is to assemble a bunch of metadata in a class and use it to standardize our file system and prevent clerical errors from newbies and old hands when they use the analysis program.
The testing initialization sub is below. It pops open a userform asking for the user to select the filepaths of the three files in the rawdatafiles class; then it kills the userform to free memory. The metadata object is currently for testing and will be rewritten properly when I get the output I want:
Sub setup()
GrabFiles.Show
Set rawdatafiles = New cRawDataFiles
rawdatafiles.labjobFile = GrabFiles.tboxLabJobFile.value
rawdatafiles.rawdatafirstcount = GrabFiles.tboxOriginal.value
rawdatafiles.rawdatasecondcount = GrabFiles.tboxRecount.value
Set GrabFiles = Nothing
Dim temp As cMetaData
Set temp = New cMetaData
temp.labjobName = rawdatafiles.labjobFile
'this works fine!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
End Sub
The cMetadata class I have currently is as follows:
Private pLabjobName As String
Private pDetectorsOriginal As Collection
Private pDetectorsRecheck As Collection
Private Sub class_initialize()
Set pDetectorsOriginal = New Collection
Set pDetectorsRecheck = New Collection
End Sub
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
When I step through the code it starts reading the "public property get rawdatafirstcount() as string" and throws the error after "End Property" and points back to the "temp.detectorsOriginal = rawdatafiles.rawdatafirstcount" line in the initialization sub.
I think I'm at least close because the temp.labjobName = rawdatafiles.labjobFile code executes properly. I've tried playing around with the data types since this is a collection being assigned by a string but I unsurprisingly get data type errors and can't seem to figure out how to proceed.
If everything worked the way I want it to, the following function would take the filepath string from the rawdatafiles.rawdatafirstcount property and return for me a collection containing detector names as strings with no duplicates (I don't know if this function works exactly the way I want since I haven't been able to get the filepath I want to parse properly in the initial sub; but I can deal that later!):
Function getDetectors(filePath As String) As Collection
Dim i As Integer
Dim detectorsCollection As Collection
Dim OriginalRawData As Workbook
Set OriginalRawData = Workbooks.Open(fileName:=filePath, ReadOnly:=True)
Set detectorsCollection = New Collection
For i = 1 To OriginalRawData.Worksheets(1).Range("D" & Rows.Count).End(xlUp).Row
detectorsCollection.Add OriginalRawData.Worksheets(1).Cells(i, 4).value, CStr(OriginalRawData.Worksheets(1).Cells(i, 4).value)
On Error GoTo 0
Next i
getDetectors = detectorsCollection
Set detectorsCollection = Nothing
Set OriginalRawData = Nothing
End Function
Thanks again for reading and any help you can offer!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
It throws an error because, as others have already stated, the Set keyword is missing.
Now with that out of the way, a Set keyword is NOT what you want here. In fact, sticking a Set keyword in front of that assignment will only buy you another error.
Let's look at this property you're invoking:
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
You're trying to assign detectorsOriginal with what appears to be some String value that lives in some TextBox control on that form you're showing - but the property's type is Collection, which is an object type - and that's not a String!
Now look at the property that does work:
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
This one is a String property, with a Property Let mutator that uses the fileName parameter it's given.
The broken one:
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Is a Set mutator, takes a Collection parameter, and doesn't use the originalFilepath parameter it's given at all!
And this is where I'm confused about your intention: you're passing what has all the looks of a String except for its type (Collection) - the calling code wants to give it a String.
In other words the calling code is expecting this:
Public Property Let detectorsOriginal(ByVal originalFilepath As String)
See, I don't know what you meant to be doing here; it appears you're missing some pOriginalFilepath As String private field, and then detectorsOriginal would be some get-only property that returns some collection:
Private pOriginalFilePath As String
Public Property Get OriginalFilePath() As String
OriginalFilePath = pOriginalFilePath
End Property
Public Property Let OriginalFilePath(ByVal value As String)
pOriginalFilePath = value
End Property
I don't know what you're trying to achieve, but I can tell you this:
Don't make a Property Set member that ignores its parameter, it's terribly confusing code.
Don't make a Property (Get/Let/Set) member that does anything non-trivial. If it's not trivially simple and has a greater-than-zero chance of throwing an error, it probably shouldn't be a property. Make it a method (Sub, or Function if it needs to return a value) instead.
A word about this:
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
Whenever you Dim something As New, VBA will automatically instantiate the object whenever it's referred to. In other words, this wouldn't throw any errors:
Dim FSO As New FileSystemObject
Set FSO = Nothing
pLabjobName = FSO.GetBaseName(fileName)
Avoid As New if you can. In this case you don't even need a local variable - use a With block instead:
With New FileSystemObject
pLabjobName = .GetBaseName(fileName)
End With
May not be your issue but you're missing Set in your detectorsOriginal Set/Get methods:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
So the error is one I've made a time or two (or more). Whenever you assign an object to another object, you have to use the Set reserved word to assign the reference to the Object.
In your code do the following:
In Sub setup()
Set temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
And in the cMetadata class change the Public Property Set detectorsOriginal(originalFilepath As Collection) property to the following:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Also in your function Function getDetectors(filePath as String) as Collection change the statement afterNext i` to
Set getDetectors = detectorsCollection
Also, I'm very glad to hear that you've learned how to use VBA.
When you're ready to create your own Custom Collections, check out this post. Your own custom Collections.
I also book marked Paul Kelly's Excel Macro Mastery VBA Class Modules – The Ultimate Guide as well as his Excel VBA Dictionary – A Complete Guide.
If you haven't been to Chip Pearson's site you should do so. He has a ton of useful code that will help your delivery your projects more quickly.
Happy Coding.
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.
How do I set the connection string value of this property?
Dim mdbDataStoreADOConnection As ADODB.Connection
Public Property Set DataStoreConnection(ByVal dbNewDataStoreConnection As ADODB.Connection)
Set mdbDataStoreADOConnection = dbNewDataStoreConnection
End Property
I want to be able to set the connection string of this connection variable, for example:
mdbDataStoreADOConnection.ConnectionString = "Provider=MSDASQL;Driver={MySQL ODBC 5.2 ANSI Driver};Server=localhost;Database=myDataBase;User=myUsername;Password=myPassword;Option=3;"
I am still learning VB6 and am unsure how to assign a value to this property?
What you are currently doing is setting an object. What you say you want to do is to set a property of the object. You have a couple of choices with your existing code. If you already have a matching Get property you can access the ConnectionString property of the DataStoreConnection object.
Public Property Get DataStoreConnection() As ADODB.Connection
Set DataStoreConnection = mdbDataStoreADOConnection
End Property
You would then set the ConnectionString property with
DataStoreConnection.ConnectionString = "Provider=MSDASQL;Driver={MySQL ODBC 5.2 ANSI Driver};Server=localhost;Database=myDataBase;User=myUsername;Password=myPassword;Option=3;"
There is no error handling, and the code as it stands is likely to cause an error if misused. For instance if the DataStoreConnection has not been set, your variable, mdbDataStoreADOConnection, is nothing and attempting to access properties will cause an error.