Dynamic collection of classes - excel

It works, but I don't know how:
I want to create a dynamic number of classes, so I use a dictionary for that.
But I can't add a key to each class of the dictionary.
It works when I add each entry of the 1st dictionary to a second dictionary as a couple item/key.
sub test()
Dim dict As New Dictionary
Dim dict2 As New Dictionary
For i = 1 To 2
Set dict(i) = New Cfgadress
dict(i).Col1 = i
dict2.Add "d" & i, dict(i)
Next i
Debug.Print dict2("d1").Col1
Debug.Print dict2("d2").Col1
End Sub
That was a guess and I'm not sure I understand how it works : is the entry of the first dictionary a class object, while the entry of the second dictionary is a dictionary object (couple item/key) ?

No need for the second dictionary. What's the error you're getting with using just one?
Here is my working code:
Add Module: Module1
Sub test()
Dim col1 As Dictionary
Dim classInstance As Class1
Dim counter As Long
' Init the collection
Set col1 = New Dictionary
For counter = 1 To 2
' Set new instance of class
Set classInstance = New Class1
' Set properties in new class instance
classInstance.testfield = "Test value" & counter
' Add it to the collection
col1.Add "test" & counter, classInstance
Next counter
Debug.Print col1.Item("test1").testfield
Debug.Print col1.Item("test2").testfield
End Sub
Class: Class1
Public testfield As String

I think I get it, correct me if I'm wrong.
When I create a dictionary entry with
Set dict(i) = New Cfgadress
It creates a new dictionary entry: blank object of class Cfgadress associated to the key i
Dict(i) refers to the item contained in entry i of the dictionary dict, i.e. to the class object contained.
To change the key, I need to assign a new couple item/key, but the item exists only as entry i of the dictionnary. So I need either :
to use the entry of the dictionary as an item associated with a key in a new dictionary/collection/array (what I did in my code)
or to assign dict(i) to an object, add it to the with a correct key and remove the former one.
Set object as New class
Set object=dict(i)
dict.Add "key", object
dict.remove "1"
Did I get it ?
Your solution is way simpler and that's what I tried first, but I went to dynamic dictionary because on each iteration, updating the class instance was also updating the previous entries of the collection
Set Wb = ThisWorkbook '==================
Set WsCfg = Wb.Sheets("Cfg") '==================
Dim dict_cfg As New Dictionary
Dim coll As New Collection
Set tcfg_adress = New Cfgadress
For i = 3 To 4 '22
If WsCfg.Cells(i, 10) = 1 And WsCfg.Cells(i, 11) = 1 Then
tcfg_adress.Row1 = WsCfg.Cells(i, 4).Value
tcfg_adress.Col1 = WsCfg.Cells(i, 5).Value
tcfg_adress.Lrow = WsCfg.Cells(i, 6).Value
tcfg_adress.Lcol = WsCfg.Cells(i, 7).Value
tcfg_adress.Nbrow = WsCfg.Cells(i, 8).Value
tcfg_adress.Nbcol = WsCfg.Cells(i, 9).Value
coll.Add tcfg_adress, WsCfg.Cells(i, 1)
End If
Next i
Turns out that setting a new class instance in each loop, as I saw you did in your code, solved my problem. I thought there was nothing to do and was investigating in get arounds. Thanks a lot !

Related

how to get item in vba collection

I'm new to VBA and I got a issue when trying to get item from collection.
I have a self defined class, A, and I have a collection B=[A1,A2...] by using
B.add A
Then I have a dictionary like C={1:B1, 2:B2...} by using
C.add i, Bi
now I want to get the C(i)(j), I build code like following, but it keeps giving me error: object doesn't support this property or method.
dim levels as variant
levels = C.items
dim level as variant
dim newA as A
for i = 0 to levels.count -1
level = levels(i)
for j = 0 to level.count -1
newA = level(j)
next
next
The error happens when I try to assign the collection and class to variant, i.e. level = levels(i) and, newA = level(j)
I know I could use for each to build loop but I need to use the index, and the object it self(might need to modify the object inside the collection later), so wondering what's the best way to do this. Thanks!
Here's an example which works for me.
Class A just has a single field Public id As String
Sub Tester()
Dim C As Object, items, i As Long, objA As A
Set C = CreateObject("scripting.dictionary")
'populate dictionary with a couple of collections of Class A instances
C.Add 1, New Collection
C(1).Add GetAInstance("Id001")
C(1).Add GetAInstance("Id002")
C.Add 2, New Collection
C(2).Add GetAInstance("Id003")
C(2).Add GetAInstance("Id004")
C(2).Add GetAInstance("Id005")
'looping...
items = C.items
For i = LBound(items) To UBound(items)
For Each objA In items(i)
Debug.Print objA.id
Next objA
Next i
'direct access
Debug.Print C(1)(1).id '> "Id001"
Debug.Print C(2)(3).id '> "Id005"
C(2)(3).id = "New id"
Debug.Print C(2)(3).id '> "New id"
Set objA = C(2)(3) 'Set is required for object-type variables
Debug.Print objA.id '> "New id"
End Sub
'function to return an object of class A with supplied id
Function GetAInstance(idValue)
Dim rv As New A
rv.id = idValue
Set GetAInstance = rv
End Function

How to create a dictionary nested dictionaries?

I have a dictionary that I want to use to store keys with dictionaries as values.
Essentially, using the below screenshot of my table of data, I want to have the dictionary (in json view) as:
{'Folder/PU01': {'PLCName':'PLCCC', 'DeviceName': 'fasasd', 'Description': '', '....'}
{'Folder/PU02': {'PLCName':'', 'DeviceName': '', 'Description': '', '....'}
etc...
I have the code below that's essentially creating this, however the very bottom line above the Next is where it's going wrong udtInstancesCurrent.Add deviceTagPath, udtInstanceParamsCurrent
This is assigning the udtInstanceParamsCurrent dictionary to the udtInstanceCurrent key, but because it's a reference to the dictionary and not a copy, it then gets overwritten the next time the loop goes around.
My question is: how can I set the value of the dictionary key in udtInstanceCurrent to a copy of the udtInstanceParamsCurrent dictionary and not a reference to the original?
Dim udtInstancesCurrent As Scripting.Dictionary
Dim udtInstanceParamsCurrent As Scripting.Dictionary
Set udtInstancesCurrent = New Scripting.Dictionary
Set udtInstanceParamsCurrent = New Scripting.Dictionary
'''' SAVE PARAMETER VALUES ''''
' For each udt instance tag path, add its param value into a dictionary to save its values
For Each udtTagPathCell In Range(.Cells(INSTANCES_ROW_HEADERS + 1, INSTANCES_COL_UDTTAGPATH), .Cells(INSTANCES_ROW_HEADERS, INSTANCES_COL_UDTTAGPATH).End(xlDown))
udtTagPath = udtTagPathCell.Value
deviceName = .Cells(udtTagPathCell.Row, INSTANCES_COL_DEVICENAME)
deviceParentPath = .Cells(udtTagPathCell.Row, INSTANCES_COL_DEVICEPARENTPATH)
deviceTagPath = deviceParentPath & "/" & deviceName
Row = udtTagPathCell.Row
udtInstanceParamsCurrent.RemoveAll
' For each parameter defined, add into a dictionary
For Each param In Range(.Cells(Row, INSTANCES_COL_PARAMSSTART), .Cells(Row, .Cells(INSTANCES_ROW_HEADERS, 1).End(xlToRight).Column))
paramName = .Cells(INSTANCES_ROW_HEADERS, param.Column)
udtInstanceParamsCurrent.Add paramName, param.Value
Next
' TODO: Dictionary is being overwritten. need to set this to a new instance of the dictionary
udtInstancesCurrent.Add deviceTagPath, udtInstanceParamsCurrent
Next
I worked it out.. I just need to set the udtInstanceParamsCurrent to a new dictionary within the for loop instead of outside of it.
Dim udtInstancesCurrent As Scripting.Dictionary
Dim udtInstanceParamsCurrent As Scripting.Dictionary
Set udtInstancesCurrent = New Scripting.Dictionary
'''' SAVE PARAMETER VALUES ''''
' For each udt instance tag path, add its param value into a dictionary to save its values
For Each udtTagPathCell In Range(.Cells(INSTANCES_ROW_HEADERS + 1, INSTANCES_COL_UDTTAGPATH), .Cells(INSTANCES_ROW_HEADERS, INSTANCES_COL_UDTTAGPATH).End(xlDown))
Set udtInstanceParamsCurrent = New Scripting.Dictionary
udtTagPath = udtTagPathCell.Value
deviceName = .Cells(udtTagPathCell.Row, INSTANCES_COL_DEVICENAME)
deviceParentPath = .Cells(udtTagPathCell.Row, INSTANCES_COL_DEVICEPARENTPATH)
deviceTagPath = deviceParentPath & "/" & deviceName
Row = udtTagPathCell.Row
udtInstanceParamsCurrent.RemoveAll
' For each parameter defined, add into a dictionary
For Each param In Range(.Cells(Row, INSTANCES_COL_PARAMSSTART), .Cells(Row, .Cells(INSTANCES_ROW_HEADERS, 1).End(xlToRight).Column))
paramName = .Cells(INSTANCES_ROW_HEADERS, param.Column)
udtInstanceParamsCurrent.Add paramName, param.Value
Next
' TODO: Dictionary is being overwritten. need to set this to a new instance of the dictionary
udtInstancesCurrent.Add deviceTagPath, udtInstanceParamsCurrent
Next

Why is Excel VBA overwriting these object values

I am trying to create a list of objects in VBA but it seems like new objects are not being created and values are being updated to a single instance of a class.
This is the class
' ---------------------------------------------------------------
'
' Class to represent Program Increment
'
' ---------------------------------------------------------------
Public name As String
Public sprints As New Collection
This is the calling code:
' get the unique pi values
Dim piList As New Collection
For r = firstRow To lastRow
currentVal = Cells(r, 2)
On Error Resume Next
Dim currentPi As New ProgramIncrement
currentPi.name = currentVal
piList.Add currentPi, currentVal
On Error GoTo 0
Next
This is the output for the first pi
And this is the output for the second pi
I'm not seeing what I'm doing wrong base upon online documents such as this.
https://analystcave.com/vba-vba-class-tutorial/
As New creates an auto-instantiated object. Dim statements aren't executable, so there's only one object indeed.
Remove As New and use Set ... = New statements to create new objects.
Dim currentPi As ProgramIncrement
Set currentPi = New ProgramIncrement
Dim being inside the loop makes no difference - on one hand it makes it easy to later refactor and extract the loop body into its own procedure scope; on the other hand it can be read as though a new variable is created at every iteration, but that's not how scopes work in VBA: the smallest scope is procedure scope - blocks (e.g. loop bodies) don't scope anything.
This worked per Mathieu Guindon's answer.
Dim piList As New Collection
Dim currentPi As ProgramIncrement
For r = firstRow To lastRow
currentVal = Cells(r, 2)
Set currentPi = New ProgramIncrement
currentPi.name = currentVal
On Error Resume Next
piList.Add currentPi, currentVal
On Error GoTo 0
Next

Is it possible to access collection which is at the bottom of stack?

The problem that I face is, I have a object/collection which will be created on the bottom of the stack and I have a program 3 levels deep, the collection will be only used at the 3(top) level, the first item will be used and removed from the collection, but I want to keep it for the entirety of the program as the next step needs to use the next item in the collection until the whole thing is done.
The best way I would like to do it is the create that collection in the bottom layer which is where the collection will be used, and keep the collection even if the collection is out of scope.
The way I am doing it right now is to create the collection at the bottom most level and pass it down the chain, cause if I create it in the top layer it will be deleted after it goes out of scope
I feel like there must be a better way solve my problem, but I just can't find it. Does anyone knows the answer?
I just set up some text in excel as follows
(A) (1)
(B) (2)
(C) (3)
(D) (4)
(E) (5)
​
'The Code works, but what I am asking is it possible to dont pass dict through all those sub
Sub Main()
Static dict As New Dictionary
Dim x As Integer
Set dict = readrange
Do While x < 3
somesub dict
x = x + 1
Loop
End Sub
'----------------------- Next Module ----------------------------------------------------
Sub somesub(dict As Dictionary) '<----------------------- Dont want this dict
'some code which doesnt not use the dict
Dictchange dict
End Sub
'----------------------- Next Module ----------------------------------------------------
Sub Dictchange(dict As Dictionary) '<----------------------- Dont want this dict too
Cells(dict(dict.Keys(0)), 4) = "Done"
'Is it possible to call dict in Main without pass the chain
'I cant use public as in the real code, "somesub" and "Dictchange" are in different module
'I could use Global, but i always feel like it just a "Dirty" way to fix thing
dict.Remove dict.Keys(0)
End Sub
'----------------------- Next Module ----------------------------------------------------
'In the real code, this is one function in a class Module
Function readrange() As Dictionary
Dim temp As New Dictionary
For i = 1 To 5
temp.Add Cells(i, 1).Value, Cells(i, 2).Value
Next i
Set readrange = temp
End Function
I hope this would help
As I already told in my comment: Make your dict a global variable.
Option Explicit
Public dict As Dictionary 'this is globally defined in a module
Sub Main()
Dim x As Long
Set dict = ReadRangeToDict
Do While x < 3
SomeProcedure
x = x + 1
Loop
End Sub
Function ReadRangeToDict() As Dictionary
Dim TempDict As New Dictionary
Dim iRow As Long
For iRow = 1 To 5
If Not TempDict.Exists(Cells(iRow, 1).Value) Then 'check if key already exists to prevent errors!
TempDict.Add Cells(iRow, 1).Value, Cells(iRow, 2).Value
End If
Next iRow
Set ReadRangeToDict = TempDict
End Function
So you can access it in any other procedure/function without giving it as a parameter.
Sub SomeProcedure()
'output the dict in the immediate window
Dim itm As Variant
For Each itm In dict
Debug.Print itm, dict(itm)
Next itm
End Sub

Change value of an item in a collection in a dictionary

I'm trying to create a dictionary which has a collection for every key. The reason for this is that I want to retrieve several values from the same key later on. In this example I want to have the total value (val) of a unique key as well as the number of occurrences (n):
sub update()
Dim dict As Dictionary
Dim coll As Collection
Set dict = New Dictionary
Set coll = New Collection
coll.Add 100, "val"
coll.Add 3, "n"
dict.Add "coll", coll
Debug.Print dict.item("coll")("val")
Debug.Print dict.item("coll")("n")
This works fine so far, the problem occurs when I try to update the value in the collection (object doesn't support this):
dict.item("coll")("val") = dict.item("coll")("val") + 100
What I tried:
If I use an array instead of the collection, there is no error but the value doesn't change.
It only works if I read out the collection to variables, change the value, create a new collection, remove the old from the dictionary and add the new collection.
Is there any way to do it like my approach above in a single line?
I would also be happy for an alternative solution to the task.
Once you added an item to the collection, you cannot change it that easily. Such expression:
coll("n") = 5
will cause Run-time error '424': Object required.
You can check it by yourself on the simple example below:
Sub testCol()
Dim col As New VBA.Collection
Call col.Add(1, "a")
col("a") = 2 '<-- this line will cause Run-time error '424'
End Sub
The only way to change the value assigned to the specified key in the given collection is by removing this value and adding another value with the same key.
Below is the simple example how to change the value assigned to a collection with key [a] from 1 to 2:
Sub testCol()
Dim col As New VBA.Collection
With col
Call .Add(1, "a")
Call .Remove("a")
Call .Add(2, "a")
End With
End Sub
Below is your code modified in order to allow you to change the value assigned to the given key in the collection:
Sub update()
Dim dict As Dictionary
Dim coll As Collection
Set dict = New Dictionary
Set coll = New Collection
coll.Add 100, "val"
coll.Add 3, "n"
dict.Add "coll", coll
Debug.Print dict.Item("coll")("val")
Debug.Print dict.Item("coll")("n")
'This works fine so far, the problem occurs when I try to update the value in the collection (object doesn't support this):
Dim newValue As Variant
With dict.Item("coll")
newValue = .Item("val") + 100
On Error Resume Next '<---- [On Error Resume Next] to avoid error if there is no such key in this collection yet.
Call .Remove("val")
On Error GoTo 0
Call .Add(newValue, "val")
End With
End Sub
It is not elegant perhaps, but maybe you can write a sub to update a collection by a key:
Sub UpdateCol(ByRef C As Collection, k As Variant, v As Variant)
On Error Resume Next
C.Remove k
On Error GoTo 0
C.Add v, k
End Sub
Used like this:
Sub Update()
Dim dict As Dictionary
Dim coll As Collection
Set dict = New Dictionary
Set coll = New Collection
coll.Add 100, "val"
coll.Add 3, "n"
dict.Add "coll", coll
Debug.Print dict.Item("coll")("val")
Debug.Print dict.Item("coll")("n")
UpdateCol dict.Item("coll"), "val", dict.Item("coll")("val") + 100
Debug.Print dict.Item("coll")("val")
End Sub
With output as expected:
100
3
200
Here is an approach using a User Defined Object (Class). Hoepfully you can adapt this to your problem.
Rename the Class Module cMyStuff or something else meaningful.
Class Module
Option Explicit
Private pTotalVal As Long
Private pCounter As Long
Public Property Get TotalVal() As Long
TotalVal = pTotalVal
End Property
Public Property Let TotalVal(Value As Long)
pTotalVal = Value
End Property
Public Property Get Counter() As Long
Counter = pCounter
End Property
Public Property Let Counter(Value As Long)
pCounter = Value
End Property
Regular Module
Option Explicit
Sub Update()
Dim cMS As cMyStuff, dMS As Dictionary
Dim I As Long
Set dMS = New Dictionary
For I = 1 To 3
Set cMS = New cMyStuff
With cMS
.Counter = 1
.TotalVal = I * 10
If Not dMS.Exists("coll") Then
dMS.Add "coll", cMS
Else
With dMS("coll")
.TotalVal = .TotalVal + cMS.TotalVal
.Counter = .Counter + 1
End With
End If
End With
Next I
With dMS("coll")
Debug.Print "Total Value", .TotalVal
Debug.Print "Counter", .Counter
End With
End Sub
Results in Immediate Window
Total Value 60
Counter 3

Resources