Excel VBA 438 Error Raised on PC but not on Mac - excel

I have VBA code which adds to a custom Edge class object to a dictionary of dictionaries. I am using this Dictionary class.
This works as expected on Mac, but when I try to run my spreadsheet command on a PC I get a 438: Object doesn't support this property or method error.
The line of code raising the error is edges_dict(user)(Provider) = created_edge where edges_dict(user) is a dictionary, Provider a String, and creaded_edge an Edge.
If I instead use edges_dict(user).Add Provider, created_edge everything works well, but I want the overwrite ability the first call provides. Additionally, edges_dict(key) = value works. The issue seems to arise from my nesting.
Here is the code where I create the dictionary of dictionaries:
Public edges_dict As New Dictionary 'Stores in degrees
Public s_array() As String
Public single_node As Dictionary 'Dictionary keyed by source node holding in degree edges for a certain node
Sub Generate_Matrix()
'Code to populate s_array() here
'Populate dictionary with key as node, value as array of inbound edges to be filled
Set single_node = New Dictionary
edges_dict.Add s_array(I), single_node
Next I
End Sub

edges_dict(user)(Provider) = created_edge
There's a lot going on, crammed into that little instruction. If either key lookup fails, how do you know which key failed? You don't, and you can't know.
Split it up, there's no use trying to compact as much functionality as possible into a single instruction.
Dim providers As Dictionary
Set providers = edges_dict.Item(user)
As a bonus, you get IntelliSense and early binding!
creaded_edge an Edge.
Assuming an Edge is an object, the bug isn't with the Win32 code blowing up with error 438 - the bug is with the Mac code "working" when it should absolutely be complaining about the missing default member on the Edge class.
The error is indirectly telling you that you need a Set keyword to assign that object reference:
Set providers(provider) = created_edge
Without the Set keyword, you are let-coercing the created_edge object, which means the dictionary item associated to that key is NOT an object on the "working" Mac code, but whatever data type the class' default member is... assuming it has a default member (and it doesn't... so it's unclear how/why the Mac code isn't blowing up) - here's your "nested" instruction, with the implicit code in square brackets:
[Let] edges_dict[.Item](user)[.Item](Provider) = created_edge[.DefaultMember]
The reason why .Add works in both cases, is because no let-coercion is happening in that case: the object reference itself is being added as a value.
Error 438 is being thrown because let-coercing an object that doesn't have a default member (as seems to be the case), is supposed to fail.
Adding Set in front of the assignment would fix it... but does not behave the same as .Add: .Add will throw a duplicate key error if you try to re-add an existing key, while assigning the keyed value like you did will silently replace the value associated to an existing key.

Related

How can I downcast an object using only late-binding in VBA?

I am writing a VBA application, and for a specific function, I am using only late-binding, as most of the users of the application won't have the reference installed (and won't use this specific function).
The object I am using behaves like:
class PISDK{
PIServer GetServer(string hostName)
}
The GetServer method returns a PIServer object, but a more specific interface exists, implementing PIServer:
interface IGetPoints2 : PIServer{}
I would like to downcast the PIServer object to a IGetPoints2 object.
Without doing anything, I get a PIServer object:
Dim PiSdk As Object
Dim PiServer As Object
Set PiSdk = CreateObject("PISDK.PISDK")
Set PiServer = PiSdk.GetServer("foo")
Looking at PiServer in the debugger confirms that.
Using a strongly typed variable should work, but I do not want to reference any of the types used here.
How can I downcast this object using late-binding only?
Please read this:
As you are not adding a reference to the PI SDK Type Library, I
believe you cannot use "rtInterpolated" as the second parameter of
the ArcValue method; instead, you can use the corresponding number
(which is 3 for "rtInterpolated" in the RetrievalTypeConstants
enumeration).
VBA with late binding is tricky with optional
parameters, as we cannot omit them when calling a method. Instead,
you need to use either Nothing (in case the optional parameter is
an object) or "" (in case the optional parameter is a string) as
"parameter placeholders" (by the way, the same happens in
VBScript, a scripting language for scripts contained in files with
.vbs extension that run independently from any application).
able to put some functional sample code together, which is more
complete and will hopefully help you.

VBA subclass reference

In this post there is the description of "subclass" usage in VBA. I'm looking for the next step of it: when I have first subitem added I want to use it and don't know how.
When I write baseItem(1).itemName it doesn't work.
I assume it's just because that baseItem is not a collection or an array, but I don't know any other way.
Welcome to SO!
In vba like many other languages it is a Property. You call them directly and not numerically, but you need to write GET/LET methods unless the class is public (that is not really encapsulation to simply access directly).
baseItem.itemName would be your call.
But ... As I said before, better that you write accessor(s) to your class as methods.
Here is a guy that sets the tone for OOP in VBA (for me):
https://stackoverflow.com/a/45570268/8716187
He is a driver of the Rubberduck project.
I would ask you to ask yourself if you really need a class, I will often use 4-10 dictionaries of keys holding arrays. I could wrap them in a class but why bother? What I need is a searchable and editable ("array"-the dictionary of arrays).
I have very few class modules written, it seems that one can operate without it many times.
-WWC
I assume it's just because that baseItem is not a collection or an
array...
The baseItem itself is not a collection nor an array. It is just an object of type BaseClass. But this object baseItem wrapps a collection so we maybe could say it is almost a collection.
The problem with this object though is, that as it is defined now in the answer you mentioned, it provides no way how the clients can get to this collection. The class BaseClass needs to be modified so it provides access to this inner collection for the client of this class. With access e.g. some public function is meant which will return something from this collection.
Add something like this to the BaseClass:
Public Function getSubItem(index As Variant) As SubClass
Set getSubItem = subClassCollection.Item(index)
End Function
Now objects which will be at runtime created based on this class definition will provide access to the inner collection via this function getSubItem. The code which will use this function will look like this. So it is now almost that what you are trying to achieve.
Dim name As String
name = baseItem.getSubItem(1).itemName
Debug.Print name ' Prints "Something" in output window
But it could be made even exactly to what you are trying to achieve. When exporting the file of BaseClass.cls and adding Attribute Value.VB_UserMemId = 0 to the very beginning of function getSubItem and importing it again to project.
Public Function getSubItem(index As Variant) As SubClass
Attribute Value.VB_UserMemId = 0
Set getSubItem = subClassCollection.Item(index)
End Function
Now you can really write your code exactly that way you wanted. HTH
Dim name As String
name = baseItem(1).itemName
Debug.Print name ' Prints "Something" in output window
For more information about Creating A Default Member In VBA have a look e.g. here.

Working in the dark on an OLE application

I am working in the dark. There is an application installed on my PC by the corporate fathers that allows programmatic access to a library of reports. I can't get anyone to tell me the application's properties or methods (apart from a couple methods found in some scripts....)
I'm using Access VBA to get to the application, and it does load it up (it shows a GUI when the CreateObject() statement is executed.)
How can I get it to list its properties and methods once I invoke it? You can see my effort, but it fails saying "Object doesn't support this property or method" when it executes the "for each" statement.
Sub StartDataNav()
Set oleDataNav = CreateObject("DataNavigator.Application")
Dim p As Object
For Each p In oleDataNav.Properties
Next p
End Sub
If need be, I can change to C#.net, but I'm not as experienced invoking what I assume is a non-managed application....
You can use the TlbInf32.dll (TLI) to inspect the public members of the target application. http://msdn.microsoft.com/en-us/magazine/bb985086.aspx seems to be a good start.
Or just inspect TLI itself using the object browser (after referencing it in the VBE).
Put a breakpoint on your line
Dim p As Object
and open the locals window (View --> Locals).
This will let you see all the properties of your object by expanding the oleDataNav object in the locals window.

Getting 'Invalid use of property' when using Property Set in VBA

I know there are tons of threads and questions about this and it's pretty obvious, generally, where the error is. Most folks aren't using the SET keyword when moving objects around. I am.
Here's what's happening:
This is in an excel sheet so I've made a little set of functions to keep track of the columns and make an index so that each time the app runs it will reindex the columns so I can do things like .Cells(row_num, pCust_index("custID")) in case of column changes.
I have a form called custContagions. It's just a little modal window that allows users to add/remove/edit customer's contagious status. It contains a property:
Private pCust_index as dictionary
It also contains this property setter:
Public Property Set set_cust_index(ByRef custIndex As Dictionary)
Set pCust_index = New Dictionary
Set pcust_index = custIndex
End Property
Pretty straight forward right? Takes a dictionary object, resets my index and points to the existing passed object.
Now, on the calling form I have the other side:
Private Sub newCustContagious_LBL_Click()
Dim contForm as New custContagions
Call contForm.set_cust_index(pCust_index) 'note pCust_index is a private here too
Call contForm.Show
...
I'm getting the Invalid Use of Property compiler error on the set_cust_index call.
What did I miss?
Most folks aren't using the SET keyword when moving objects around
Then they are not moving objects around. The Set keyword is the way to move object around.
There is also CopyMemory to directly copy the ObjPtr, but I do not believe most folks do that.
Pretty straight forward right?
Not quite. You create a dictionary, immediately discard it and replace with another dictionary passed as a parameter. You should remove the first of the two lines, and make the param ByVal:
Public Property Set set_cust_index(ByVal custIndex As Dictionary)
Set pcust_index = custIndex
End Property
I'm getting the Invalid Use of Property compiler error
You declared a property and then use it as a sub. With a property, you should have done:
Set contForm.set_cust_index = pCust_index
At which point the set_cust_index name does not look great. It would make a sensible name for a sub (Public Sub set_cust_index(ByVal custIndex As Dictionary)), but for a property you would be better off with Public Property Set cust_index(ByVal custIndex As Dictionary).

What are the benefits of using Classes in VBA? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 3 years ago.
Improve this question
I am doing some VBA programming in Excel and have one workbook where all the datasheets are to be copied from into another sheet. The new sheet will have several header rows, and I would like to keep track of where they are situated so I don't have to find words in them constantly.
Is the simplest thing to use classes and keep them running while the Excel workbook is open? Or will this make it heavy and hard to handle, and I should keep working with subroutines? What are the benefits of using classes? It is not like I have several objects, only sheets and validation on columns.
The advantage of using classes instead of just subroutines is that classes create a level of abstraction that allow you to write cleaner code. Admittedly, if you've never used classes before in VBA, there is a learning curve, but I believe it's certainly worth the time to figure it out.
One key indication that you should switch to classes is if you're constantly adding parameters to your functions and subroutines. In this case, it's almost always best to use classes.
I've copied an explanation of classes from one of my previous Stack Overflow answers:
Here's a long example of how using a class might help you. Although this example is lengthy, it will show you how a few principles of object-oriented programming can really help you clean up your code.
In the VBA editor, go to Insert > Class Module. In the Properties window (bottom left of the screen by default), change the name of the module to WorkLogItem. Add the following code to the class:
Option Explicit
Private pTaskID As Long
Private pPersonName As String
Private pHoursWorked As Double
Public Property Get TaskID() As Long
TaskID = pTaskID
End Property
Public Property Let TaskID(lTaskID As Long)
pTaskID = lTaskID
End Property
Public Property Get PersonName() As String
PersonName = pPersonName
End Property
Public Property Let PersonName(lPersonName As String)
pPersonName = lPersonName
End Property
Public Property Get HoursWorked() As Double
HoursWorked = pHoursWorked
End Property
Public Property Let HoursWorked(lHoursWorked As Double)
pHoursWorked = lHoursWorked
End Property
The above code will give us a strongly-typed object that's specific to the data with which we're working. When you use multi-dimension arrays to store your data, your code resembles this: arr(1,1) is the ID, arr(1,2) is the PersonName, and arr(1,3) is the HoursWorked. Using that syntax, it's hard to know what is what. Let's assume you still load your objects into an array, but instead use the WorkLogItem that we created above. This name, you would be able to do arr(1).PersonName to get the person's name. That makes your code much easier to read.
Let's keep moving with this example. Instead of storing the objects in array, we'll try using a collection.
Next, add a new class module and call it ProcessWorkLog. Put the following code in there:
Option Explicit
Private pWorkLogItems As Collection
Public Property Get WorkLogItems() As Collection
Set WorkLogItems = pWorkLogItems
End Property
Public Property Set WorkLogItems(lWorkLogItem As Collection)
Set pWorkLogItems = lWorkLogItem
End Property
Function GetHoursWorked(strPersonName As String) As Double
On Error GoTo Handle_Errors
Dim wli As WorkLogItem
Dim doubleTotal As Double
doubleTotal = 0
For Each wli In WorkLogItems
If strPersonName = wli.PersonName Then
doubleTotal = doubleTotal + wli.HoursWorked
End If
Next wli
Exit_Here:
GetHoursWorked = doubleTotal
Exit Function
Handle_Errors:
'You will probably want to catch the error that will '
'occur if WorkLogItems has not been set '
Resume Exit_Here
End Function
The above class is going to be used to "do something" with a colleciton of WorkLogItem. Initially, we just set it up to count the total number of hours worked. Let's test the code we wrote. Create a new Module (not a class module this time; just a "regular" module). Paste the following code in the module:
Option Explicit
Function PopulateArray() As Collection
Dim clnWlis As Collection
Dim wli As WorkLogItem
'Put some data in the collection'
Set clnWlis = New Collection
Set wli = New WorkLogItem
wli.TaskID = 1
wli.PersonName = "Fred"
wli.HoursWorked = 4.5
clnWlis.Add wli
Set wli = New WorkLogItem
wli.TaskID = 2
wli.PersonName = "Sally"
wli.HoursWorked = 3
clnWlis.Add wli
Set wli = New WorkLogItem
wli.TaskID = 3
wli.PersonName = "Fred"
wli.HoursWorked = 2.5
clnWlis.Add wli
Set PopulateArray = clnWlis
End Function
Sub TestGetHoursWorked()
Dim pwl As ProcessWorkLog
Dim arrWli() As WorkLogItem
Set pwl = New ProcessWorkLog
Set pwl.WorkLogItems = PopulateArray()
Debug.Print pwl.GetHoursWorked("Fred")
End Sub
In the above code, PopulateArray() simply creates a collection of WorkLogItem. In your real code, you might create class to parse your Excel sheets or your data objects to fill a collection or an array.
The TestGetHoursWorked() code simply demonstrates how the classes were used. You notice that ProcessWorkLog is instantiated as an object. After it is instantiated, a collection of WorkLogItem becomes part of the pwl object. You notice this in the line Set pwl.WorkLogItems = PopulateArray(). Next, we simply call the function we wrote which acts upon the collection WorkLogItems.
Why is this helpful?
Let's suppose your data changes and you want to add a new method. Suppose your WorkLogItem now includes a field for HoursOnBreak and you want to add a new method to calculate that.
All you need to do is add a property to WorkLogItem like so:
Private pHoursOnBreak As Double
Public Property Get HoursOnBreak() As Double
HoursOnBreak = pHoursOnBreak
End Property
Public Property Let HoursOnBreak(lHoursOnBreak As Double)
pHoursOnBreak = lHoursOnBreak
End Property
Of course, you'll need to change your method for populating your collection (the sample method I used was PopulateArray(), but you probably should have a separate class just for this). Then you just add your new method to your ProcessWorkLog class:
Function GetHoursOnBreak(strPersonName As String) As Double
'Code to get hours on break
End Function
Now, if we wanted to update our TestGetHoursWorked() method to return result of GetHoursOnBreak, all we would have to do as add the following line:
Debug.Print pwl.GetHoursOnBreak("Fred")
If you passed in an array of values that represented your data, you would have to find every place in your code where you used the arrays and then update it accordingly. If you use classes (and their instantiated objects) instead, you can much more easily update your code to work with changes. Also, when you allow the class to be consumed in multiple ways (perhaps one function needs only 4 of the objects properties while another function will need 6), they can still reference the same object. This keeps you from having multiple arrays for different types of functions.
For further reading, I would highly recommend getting a copy of VBA Developer's Handbook, 2nd edition. The book is full of great examples and best practices and tons of sample code. If you're investing a lot of time into VBA for a serious project, it's well worth your time to look into this book.
If there are lots of subroutines or subroutines are very long then structuring the code in to classes may help. If there are only a couple of subroutines, say, each being only 10 lines of code each then this is over kill. The benefit of structuring the code in to classes is that it is easier to read and change when you come back to it down the line. So another reason to structuring the code into classes is if the code is likely to need changing down the line
There is one other thing you could add to the advantages other contributors have stated (sorry if it's somewhere in Ben McCormack's excellent answer and I missed it). Classes can have their uses if your VBA script is likely to be re-programmed at some point.
For instance, I am designing a sort of order management system. It is to be used by several colleagues for quite a while, but it may need re-progamming if ordering rules change. I have therefore designed a basic stock item class, which gathers all the information about a stock item. The rules about how this data is analyzed for any order are, however, written in easily accessible and well commented sub routines. By doing this, I hope that future VBA programmers can easily change the mathematical rules by which orders are generated, without having to deal with how all the data is gathered about a particular stock item (this is all done by subroutines and functions within the class, which are activated when the class is handed a stock number). A Class' public properties are also picked up by intellisense, allowing the next programmer, as well as yourself, to have an easier time of it.
I guess the point is that classes can make life easier for later users in this way if they encode some basic set of information, or some conceptual object, that is always likely to be relevant to the context of the program's use.

Resources