VBA: Using Let / Get Default Properties with Arrays - vba

This is my first class with a default member and I'm still getting a feel for it. I have a let / get property data:
class ArrayClass
private D as variant
Property Get data() As Variant
data = D
End Property
Property Let data(arg1 As Variant)
D = arg1
End Property
I have added the following in my .cls file (using notepad) to make data the default parameter which looks like this:
Property Get data() As Variant
Attribute data.VB_UserMemId = 0
data = D
End Property
I'm just testing this to see how well this works using an array for "D":
Dim testArray As ArrayClass
Dim passArray(5, 1) As Variant
passArray(0, 0) = 1
passArray(1, 0) = 2
passArray(2, 0) = 3
passArray(3, 0) = 4
passArray(4, 0) = 5
passArray(5, 0) = 6
passArray(0, 1) = 7
passArray(1, 1) = 8
passArray(2, 1) = 9
passArray(3, 1) = 10
passArray(4, 1) = 11
passArray(5, 1) = 12
Set testArray = new ArrayClass
testArray = passArray
testArray(1, 1) = 5
Debug.Print testArray(2, 1)
It "mostly" works. The "testArray = passArray" calls the "Let Data" property, and assigns the passArray to parameter "D" inside the object.
Also, "Debug.Print testArray(2,1)" also works. That calls the "Get Data" property, and it returns the index values of 2,1 of the "D" parameter in the object.
My problem is the "testArray(1,1) = 5" instruction. The intent was to assign the 1,1 index to parameter D to the number 5. But what happens is it calls the "Get Data" property, instead of the "Let Data" property.
To be clear, I wasn't really expecting it to work because I'm not yet sure how to do it. But I'm at a loss on why its calling the "get property" instead of the "let property" being that the instruction is on the left side of the equal sign.
Anyone have any ideas on how to make it work? Thanks.

What you're experiencing is standard VBA behaviour.
The line testArray(1,1) = 5 first makes a copy of the of the D array (indeed calling Get) and then value 5 is assigned to the 1,1 index of the new/copy array.
You can only call Let to pass a single value as that's what you definition expects:
Property Let data(arg1 As Variant)
D = arg1
End Property
You can't call it with testArray(1,1) because that passes 2 values. Obviously, you made it clear that you intend to update just one member of the internal D array but that's simply not possible via that Let property.
What you could do is to define a new property that expects 3 parameters:
Property Let item(ByVal index1 As Long, ByVal index2 As Long, ByVal newValue As Variant)
D(index1, index2) = newValue
End Property
and call it with testArray.item(1, 1) = 5 or maybe define this new property as the default.
Consider declaring D as an array - as it currently stands, you can pass anything e.g. testArray = "test" which I don't think is what you want. So, maybe declare it as Private D() As Variant and then update the data properties to receive and return an array of Variant type:
Property Get data() As Variant()
data = D
End Property
Property Let data(arg1() As Variant)
D = arg1
End Property

I was able to figure out how to do this the way I want to do it. The secret is using the parameter arrays in the input fields of my Get / Let properties.
Property Let data(ParamArray sizes() As Variant, data As Variant)
end property
Property Get data(ParamArray sizes() As Variant) As Variant
Attribute data.VB_UserMemID = 0 'This line makes the data property the default property.
'It is Only visible/editable in .cls file opened in text editer
end property
So it turns out that the let property has two different types of input arguments a "arglist" and a value. The last argument in the list is your value (and is the number to the right of the equal sign). But since VBA (and all other languages to my knowledge) requires if you use an optional input, then all inputs to the right of it must also be optional, including the value input.
But if you use ParamArray, then VBA knows which parameters are the inputs, and which one is the value, thus enabling you to have optional inputs on your let property. All my inputs for these properties are optional. Then I just use if statements to map what I'm supposed to output based on the number of input values.
So now I can access my array parameter inside my class using the exact same syntax as an array outside my class:
passArray(0, 0) = 1
passArray(1, 0) = 2
passArray(2, 0) = 3
passArray(0, 1) = 4
passArray(1, 1) = 5
passArray(2, 1) = 6
testArray = passArray
Debug.Print testArray(2, 1) 'returns 6
testArray(2, 1) = 7.5
Debug.Print testArray(2, 1) 'returns 7.5

Related

How to set 2 arguments on Let function in a class in vba (for excel)?

I am creating a Class in vba (for excel) to process blocks of data. After some manipulation of a text file I end up with blocks of data (variable asdatablock() ) which I want to process in a For Loop
I created my own Class called ClDataBlock from which I can get key data by a simple call of the property required. 1st pass seems to work and I am now trying to expand my Let function to 2 argument but it’s not working. How do I specify the 2nd argument?
Dim TheDataBlock As New ClDataBlock
For i = 0 to UBound(asdatablock)
asDataBlockLine = Split(asdatablock(i), vbLf) ‘ split block into line
TheDataBlock.LineToProcess = asDataBlockLine(5) ‘allocate line to process by the class
Dvariable1 = TheDataBlock.TheVariable1
‘and so on for the key variables needed base don the class properties defined
Next i
In the Class Module the Let function takes 2 arguments
Public Property Let LineToProcess(stheline As String, sdataneeded As String)
code extract of what I am looking at -
'in the class module
Dim pdMass As Double
Private pthelineprocessed As String
Public Property Let LineToProcess(stheline As String, sdataneeded As String)
pthelineprocessed = DeleteSpaces(Replace(stheline, vbLf, ""))
Dim aslinedatafield() As String
Select Case sdataneeded
'THIS IS AN EXTRACT FROM THE FUNCTION
'THERE ARE AS NUMBER OF CASES WHICH ARE DEALT WITH
Case Is = "MA"
aslinedatafield() = Split(pthelineprocessed, " ")
pdbMass = CDbl(aslinedatafield(2))
End select
End function
Public Property Get TheMass() As Double
TheMass = pdMass
End Property
'in the "main" module
Dim TheDataBlock As New ClDataBlock
For i = 0 to UBound(asdatablock)
TheDataBlock.LineToProcess = asDataBlockLines(5) 'Need to pass argument "MA" as well
dmass = TheDataBlock.TheMass
'and so on for all the data to be extracted
Next i
When a Property has 2 or more arguments, the last argument is what is getting assigned. In other words, the syntax is like this:
TheDataBlock.LineToProcess("MA") = asDataBlockLine(5)
This means you need to change the signature of your property:
Public Property Let LineToProcess(sdataneeded As String, stheline As String)

Reference a variable using a string

I have a few variables called: _2sVal, _3sVal, _4sVal, etc
I want to change each of their values through a loop.
Like:
For i = 1 To 10
'set the value
Next
I've tried putting them in a dictionary like:
Dim varDict As New Dictionary(Of String, Integer)
varDict.Add("2sVal", _2sVal)
varDict.Add("3sVal", _3sVal)
varDict.Add("4sVal", _4sVal)
I can retrieve the value using
MsgBox(varDict(i.ToString & "sVal"))
But when I try to change it like
varDict(i.ToString & "sVal") = 5
It doesn't do anything. No errors or exceptions either, just the value stays unchanged
When you are using
varDict.Add("4sVal", _4sVal)
You are not putting the _4sVal variable inside the dictionary, but its value.
Then, changing the dictionary will not change the _4sVal, since there is no reference of it inside the dictionary.
What I mean is
varDict("4sVal") = 5
will change the value of dictionary but not the variable _4sVal itself.
I think the correct to do is define that variables as Properties, defined like:
Property _4sVal As Integer
Get
Return varDict("4sVal")
End Get
Set(value As Integer)
varDict("4sVal") = value
End Set
End Property
This way you will not have to change anything in the rest of your code. It will be transparent.

How to dynamically reference an object property in VBA

I'm trying to write a VBA function that counts the objects in a collection based on the value of one of the object's properties. I need the examined object property to be dynamic, supplied by the function parameters. I could use an if then statement, but that would have many, many elseif clauses, each with identical procedures, except the property name.
I'd like to avoid repeating my code over and over for each property name. Here's what I have so far.
Private Function getTicketCount(c As Collection, f As String, s As String) _
As Long
' #param c: collection of Ticket objects.
' #param f: property to filter.
' #param s: filter string.
'
' Function returns number of tickets that pass the filter.
Dim x As Long
Dim t As Ticket
x = 0
For Each t In c
If t.f = s Then x = x + 1 ' Compiler throws "Method or data member not found."
Next t
getTicketCount = x
End Function
The issue I'm having is that the compiler is looking for the "f" property of t instead of the value-of-f property of t. The exact error is commented in the code block above. How do I use the value of f instead of "f" to reference the object property?
I believe you want to use the CallByName method CallByName MSDN Link
Private Function getTicketCount(c As Collection, f As String, s As String) _
As Long
' #param c: collection of Ticket objects.
' #param f: property to filter.
' #param s: filter string.
'
' Function returns number of tickets that pass the filter.
Dim x As Long
Dim t As Ticket
x = 0
For Each t In c
If CallByName(t, f, VbGet) = s Then x = x + 1 ' Compiler throws "Method or data member not found."
Next t
getTicketCount = x
End Function

For Each Dictionary loop in Index order

I have tried my hand using for loop with Dictionary but couldn't really achieve what I want to.
I have a certain variable SomeVariable and on the value of this variable I want my foreach to work. SomeVariable can be 1,2,3 or 4
So lets say SomeVariable is 1 I want to retrieve the last item.value from among the first 3 indexes(0,1,2) inside the SomeCollection.
And if SomeVariable is 2 I want to retrieve the last item.value from among the next 3 indexes(3,4,5) inside the SomeCollection.
And so on...
For Each item As KeyValuePair(Of String, Integer) In SomeCollection
If SomeVariable = 1 Then
//....
ElseIf SwitchCount = 2 Then
//....
End If
Next
A dictionary has no defined order, so any order you perceive is transient. From MSDN:
The order of the keys in the .KeyCollection is unspecified, but it is the same order as the associated values in the .ValueCollection returned by the Values property.
Trying to use the Keys collection to determine the order shows how it is transient:
Dim myDict As New Dictionary(Of Integer, String)
For n As Int32 = 0 To 8
myDict.Add(n, "foo")
Next
For n As Int32 = 0 To myDict.Keys.Count - 1
Console.WriteLine(myDict.Keys(n).ToString)
Next
the output prints 0 - 8, in order, as you might expect. then:
myDict.Remove(5)
myDict.Add(9, "bar")
For n As Int32 = 0 To myDict.Keys.Count - 1
Console.WriteLine(myDict.Keys(n).ToString)
Next
The output is: 0, 1, 2, 3, 4, 9 (!), 6, 7, 8
As you can see, it reuses old slots. Any code depending on things to be in a certain location will eventually break. The more you add/remove, the more unordered it gets. If you need an order to the Dictionary use SortedDictionary instead.
You can't access the dictionary by index but you can access the keys collection by index. You don't need a loop for this at all.
So something like this.
If SomeVariable = 1 Then
Return SomeCollection(SomeCollection.Keys(2))
ElseIf SomeVariable = 2 Then
...
End If
If it is truly structured you could do this:
Return SomeCollection(SomeCollection.Keys((SomeVariable * 3) - 1))
You probably need some error checking and ensuring that the length of the dictionary is correct but this should put you on the right track.
You can always use a generic SortedDictionary, I only use C# so here's my example:
SortedDictionary<int,string> dict = new SortedDictionary<int, string>();
foreach( KeyValuePair<int,string> kvp in dict) { ... }

Why won't this list of struct allow me to assign values to the field?

Public Structure testStruct
Dim blah as integer
Dim foo as string
Dim bar as double
End Structure
'in another file ....
Public Function blahFooBar() as Boolean
Dim tStrList as List (Of testStruct) = new List (Of testStruct)
For i as integer = 0 To 10
tStrList.Add(new testStruct)
tStrList.Item(i).blah = 1
tStrList.Item(i).foo = "Why won't I work?"
tStrList.Item(i).bar = 100.100
'last 3 lines give me error below
Next
return True
End Function
The error I get is: Expression is a value and therefore cannot be the target of an assignment.
Why?
I second the opinion to use a class rather than a struct.
The reason you are having difficulty is that your struct is a value type. When you access the instance of the value type in the list, you get a copy of the value. You are then attempting to change the value of the copy, which results in the error. If you had used a class, then your code would have worked as written.
try the following in your For loop:
Dim tmp As New testStruct()
tmp.blah = 1
tmp.foo = "Why won't I work?"
tmp.bar = 100.100
tStrList.Add(tmp)
Looking into this I think it has something to do with the way .NET copies the struct when you access it via the List(of t).
More information is available here.
Try creating the object first as
Dim X = New testStruct
and setting the properties on THAT as in
testStruct.blah = "fiddlesticks"
BEFORE adding it to the list.