I have been looking through old code to get familiar with the system I use and found a piece of code that I feel can be used better.
What goes on here is some data gets added to the collection(around 150 string variables, some with two variables(variableName/VariableValue), most with only one(VariableName)). It will try to set a module level string variable to the item of the collection passing it the index(variableName) then if there's a value setting the VariableVAlue to the module level variable.
What I feel needs work is that if the collection is passed a variable and the variable doesn't have a value it will return a "" which would cause a runtime error hence there's a On Error GoTo Handler code to manually add a "" to the collection. I feel there's a better way to do this rather than knowing there will be a runtime issue then solving it after catching it. Would there be a way to have a return "" not throw an exception or would the use of an Array also work here since it's a "collection" as well?
Here's an example to try to help visualize:
Public Function GetCollectionVariable(ByVal varName as string) as String
If collection1 Is Nothing Then
m_collection1 = New Collection
End If
On Error GoTo Handler
GetCollectionVariable = collection1.Item(VarName)
exit function
Handler:
collection1.add("", VarName)
GetCollectionVariable = ""
End FUnction
Thanks for your time!!
If Collection1 is a dictionary, you can use TryGetValue.
Related
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.
Say you have one slide with one chart on it, and you run this code(in a version of Office later than 2007):
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
Set result = pptWorkbook.ContentTypeProperties
You will generate an error:
Application-defined or object-defined error
I believe this is because "Smart tags are deprecated in Office 2010."(Source), Generally to avoiding this sort of issue from throwing an error and exiting your VBA you can take one of two different approaches:
//Method 1
If OfficeVersion <= 2007
Set result = pptWorkbook.ContentTypeProperties
//Method 2
On Error Resume Next // or GOTO error handler
Set result = pptWorkbook.ContentTypeProperties
Method one requires that you know the specific reason why the property would cause an error, which is easy in this case but may not be as easy with other properties. Method two requires that you use some form of error handling to deal with the error AFTER the fact, my understanding of most other Microsoft languages is that is typically discouraged(example, another example). Is this standard practice in VBA?
In VBA, is there any other way to determine whether a property of an object would throw an error if invoked, BEFORE invoking that property, and without knowing the specifics of that invoked property?
What I like to do for this situation is create a separate function that checks if the property exists and returns a Boolean. In this case it would look something like this:
Public Function CheckIfExists(targetObj As Object) As Boolean
Dim testObj As Object
On Error GoTo failedTest:
Set testObj = targetObj.ContentTypeProperties
CheckIfExists = True
Exit Function
failedTest:
CheckIfExists = False
End Function
Which would return false if that property causes an error and true if not-
Then modify your sub to be:
Public Sub FooSub()
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
If CheckIfExists(pptWorkbook) Then
Set result = pptWorkbook.ContentTypeProperties
End If
... rest of your code or appropriate error handling...
Hope this helps,
TheSilkCode
I have dificulties figuring how a function can return an object in Excel VBA.
For example, in Java, I am used to write it like this:
Private ArrayList<> getARandomArrayList() {
//... My code
return anArrayList;
}
This method should return an arrayList that I can use.
If I do this in Excel, I believe it is supposed to look like this:
Function getARandomArrayList() As System.Collections.ArrayList
'... My code
getARandomArrayList = anArrayList
End Function
When I try to use this kind of function, I get a "Compile error: User-defined type not defined" error window. If I use variables type like Double or String, I have no problem. It is only with objects that I get errors.
A VBA.Collection may work for you, depending on your needs. (Edit: And as Vincent points out in the comments, no additional references are required.) Here's what it'd look like to use one:
Function getSomeCollection() As VBA.Collection
'Declare a collection
Dim newCollection As VBA.Collection
'Initialize the collection
Set newCollection = New VBA.Collection
'Add a string.
'Other methods are Item (access by index), Count, and Remove (by index)
newCollection.Add "hello"
'Reference the collection (note it's 1-based)
MsgBox newCollection(1)
'Set the return value
Set getSomeCollection = newCollection
End Function
As Rory said:
You have to set a reference to the relevant object library in order to be able to declare a variable as a type contained in it
I went in reference and activated System librairies.
I have a need to get properties and their values dynamically. My code below is failing. Can someone give me a hand? I have tried numerous examples but nothing so far.
Dim seriesName As String = s.SeriesName
If model.Settings.ShowNativeLanguage Then
Dim propInfo As System.Reflection.PropertyInfo = s.GetType().GetProperty(model.Country)
seriesName = CStr(propInfo.GetValue(s, Nothing))
End If
This code produces the error "Object does not match target type."
The question was already answered here for C# Object does not match target type using C# Reflection
The solution is to change this line of your code:
seriesName = propInfo.GetValue(propInfo, Nothing).ToString()
to this:
seriesName = propInfo.GetValue(s, Nothing).ToString()
You need to pass the object of which you want to get the value. (More information in MSDN)
Update:
You should always check reflection results for Nothing values. So first store the output of propInfo.GetValue(s, Nothing) in a temporary variable and later on only call the ToString()-function if the object is not Nothing
Surely that should be:
... propInfo.GetValue(s) ...
Normally you must pass the object representing the this instance as the first parameter. You are getting that error because it's expecting the instance s, not a PropertyInfo instance.
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.