Excel VBA - set collection = nothing taking 100 times longer than creating collection - excel

sorry if similar has already been posted - I couldn't find one.
I have a need to have a collection of classes that each contains a collection of classes. There's expected to be a few hundred thousand entries to the collections.
I am finding it's taking ~1 second to create all of the collections, but taking ~minutes to delete them afterwards.
Below is a snippet of random code that creates the issue I'm seeing.
Is this expected? Why does it take ~100 times longer to delete the collections vs creating it. Is there any way of speeding it up?
Thanks
Module1:
Private p_main_collection As Collection
Sub main()
Debug.Print "Starting: " & Now()
Set p_main_collection = New Collection
Dim i As Long
Dim j As Long
Dim v_class1 As Class1
Dim v_class2 As Class2
For i = 1 To 8
Set v_class1 = New Class1
p_main_collection.Add v_class1
v_class1.Num = i
For j = 1 To 50000
Set v_class2 = New Class2
v_class2.Num = j
v_class1.add_to_collection = v_class2
Next j
Next i
Debug.Print "Deleting: " & Now()
Set p_main_collection = Nothing
Debug.Print "Deleted: " & Now()
End Sub
Class1
Private p_num As Integer
Private p_collection As Collection
Private Sub Class_Initialize()
Set p_collection = New Collection
End Sub
Public Property Let Num(p_num_in As Integer)
p_num = p_num_in
End Property
Public Property Get Num() As Integer
Num = p_num
End Property
Public Property Let add_to_collection(p_collection_in As Class2)
p_collection.Add p_collection_in
End Property
Class2
Private p_num As Long
Public Property Let Num(p_num_in As Long)
p_num = p_num_in
End Property
Public Property Get Num() As Long
Num = p_num
End Property
Result
Starting: 08/01/2019 23:15:01
Deleting: 08/01/2019 23:15:02
Deleted: 08/01/2019 23:16:19

Related

Custom class methods in Excel

I'm trying to create my own custom class module to handle custom class object.
Let's say i want to create a method witch would double the result of object property...
Class1:
Public Property Get MyObject() As Workbooks
Set MyObject = Application.Workbooks
End Property
When i create code like this:
Module1:
Function test()
Dim clstest As New Class1
Debug.Print "Result is: " & clstest.MyObject.Count
End Function
will give me
Result is: 1
since there is one worksheet in my object.
What i`m trying to achive is to make a custom function like this:
test.MyObject.DoubleResult '<- to make the result equals 2
I can`t get the idea how to pass the object to the new class function.
Class 2
Public Function DoubleResult() As Integer
DoubleResult = (The_Object_I_Created_earlier.Count) * 2
End Function
How i cant refer to the object "The_Object_I_Created_earlier"?
As far as I know you have to keep the count in an External variable.
You declare the public Long variable in a Module
Public ClassCount As Long
And on your class initialize and terminate you add and remove from it.
Private Sub Class_Initialize()
ClassCount = ClassCount + 1
End Sub
Private Sub Class_Terminate()
ClassCount = ClassCount - 1
End Sub
The design intent is not clear at all. But I can offer a simple primer with custom classes in VBA, and how one can contain the other.
Here is the code
MyClass1
Private m_value As Long
Private Sub Class_Initialize()
m_count = 0
End Sub
Public Sub InitializeWithCount(ByVal n As Long)
m_count = n
End Sub
Public Property Get Count() As Long
Count = m_count
End Property
MyClass2
Private m_obj As MyClass1
Private Sub Class_Initialize()
Set m_obj = New MyClass1
End Sub
Public Sub InitializeWithObject(ByVal x As MyClass1)
Set m_obj = x
End Sub
Public Sub InitializeWithValue(ByVal n As Long)
Set m_obj = New MyClass1
m_obj.InitializeWithCount n
End Sub
Public Property Get MyResult() As MyClass1
Set MyResult = m_obj
End Property
Module
for testing the above
Public Sub SO_Test()
Dim t1 As MyClass1
Dim t2 As MyClass2
Set t1 = New MyClass1 ' New Class1
t1.InitializeWithCount 100 ' Set value once to Long
Set t2 = New MyClass2 ' New Class2
t2.InitializeWithObject t1 ' Set value once to Class1
Debug.Print t2.MyResult.Count
' 100
t2.InitializeWithValue 200
Debug.Print t2.MyResult.Count
' 200
End Sub

VBA - Trying to understand how to call Class Modules

I'm learning VBA through Google, YouTube, etc.. and I came across Class Modules.
I have a Tracker Template.
Every few days I get a report sent to me ("Ice cream FG Inv.xlsm")
While trying to understand Class Modules, I found a template that created a Class Module (within the Tracker Template) WBIceCreamFGINVxlsm creating a CodeName for all of the worksheets within the Ice Cream FG Inv.xlsm Workbook.
Example:
Public Property Get wsinventory() As Worksheet
Set wsinventory = Workbook.Worksheets("Inventory")
End Property
In my module, I want to reference wsinventory, but not understanding exactly how to 'call' the Class Module..
Both Workbooks are Open.
I tried to start with:
Dim Data As Variant
Data = wsinventory.Range("A1").CurrentRegion.Value (**Variable not Defined**)
Then I tried:
Dim wsinventory As Worksheets
With wsinventory
Dim Data As Variant
Data = .Range("A1").CurrentRegion.Value (**Object variable or With variable not set**)
End With
Do I still need to use:
Dim DataSource As Workbook
Set DataSource = Workbooks("Ice Cream FG Inv.xlsm")
With DataSource.Worksheets("Inventory")
End With
If so, what would be the reasoning for using Class Modules?
You need to create a class object before you can access the properties of that class.
Assuming you have this Class and naming it TestClass:
Private pwsinventory As Worksheet
Public Sub init()
Set pwsinventory = Worksheets("Inventory")
End Sub
Public Property Set wsinventory(lwsinventory As Worksheet)
Set pwsinventory = lwsinventory
End Property
Public Property Get wsinventory() As Worksheet
Set wsinventory = pwsinventory
End Property
You can set / get the properties like so:
Sub test()
Dim datacls As TestClass
Dim data As Worksheet
Set datacls = New TestClass
Set datacls.wsinventory = Worksheets("inventory")
Set data = datacls.wsinventory
Debug.Print data.Name
End Sub
This, however, is kind of weird and when you have a property you don't want to set (you need to pass an argument) you should use an initiate function. Unfortunately there is no way I know of to do this without manually calling that sub after the class object is created.
Sub Test2()
Dim datacls As TestClass
Set datacls = New TestClass
datacls.init
Debug.Print datacls.wsinventory.Name
End Sub
The most common case I use classes for is better containers. Generally storing many of the same class type inside an array / dictionary so it is clear what I'm calling, especially if I need to modify the data in the same manner for each instance.
I am going to give another example. Create a class definition and name it ArrayData, and define multiple initialization subroutines
ArrayData.cls
Private m_data() As Variant
Private Sub Class_Initialize()
End Sub
Public Sub IntializeEmpty(ByVal rows As Long, ByVal columns As Long)
ReDim m_data(1 To count, 1 To columns)
End Sub
Public Sub InitializeFromRange(ByRef target As Range)
If target.rows.count > 1 Or target.columns.count > 1 Then
m_data = target.Value2
Else
ReDim m_data(1 To 1, 1 To 1)
m_data(1, 1) = target.Value
End If
End Sub
Public Sub InitializeFromArray(ByRef data() As Variant)
m_data = data
End Sub
Public Property Get RowCount() As Long
RowCount = UBound(m_data, 1) - LBound(m_data, 1) + 1
End Property
Public Property Get ColCount() As Long
ColCount = UBound(m_data, 2) - LBound(m_data, 2) + 1
End Property
Public Property Get Item(ByVal row As Long, ByVal col As Long) As Variant
Item = m_data(row, col)
End Property
Public Property Let Item(ByVal row As Long, ByVal col As Long, ByVal x As Variant)
m_data(row, col) = x
End Property
Module
To test the code in a code module initialize the class with the New keyword and then call one of the custom initialization subroutines.
Public Sub TestArray()
Dim arr As New ArrayData
arr.InitializeFromRange Sheet1.Range("A2").Resize(10, 1)
Dim i As Long
For i = 1 To arr.RowCount
Debug.Print arr.Item(i, 1)
Next i
End Sub
PS. Also read this article on how to designate one property as the default. In the example above if Item was the default property then you could write code such as
Debug.Print arr(5,2)
instead of
Debug.Pring arr.Item(5,2)

WithEvent in Class module not getting variable from Let Property in Excel VBA

I think I'm missing something simple here. Trying to use a variable in a Class module. Let and Get work fine. But if I try to use the variable in a different sub in the Class module I just get a value of 0.
Class Module clsCombobox
Public WithEvents ComboBox As MSForms.ComboBox
Public WithEvents ComboTextBox As MSForms.TextBox
Public Num As Long
Public Property Let Number(Value As Long)
Num = Value
End Property
Public Property Get Number() As Long
Number = Num
End Property
Private Sub ComboBox_Change()
Me.ComboTextBox.Value = Num
'this gives value of 0
End Sub
Userform Module
Dim obEvents as clsCombobox
Set obEvents = New clsCombobox
obEvents.Number = 52
MsgBox obEvents.Number 'this prints 52
Sub that sets ComboBox
Private Sub GroupCombobox()
Dim i As Long
Dim obEvents As clsCombobox
Set collCombobox = New Collection
For i = 1 To 5
Set obEvents = New clsCombobox
Set obEvents.ComboBox = Me.Controls("cbAbility" & i)
Set obEvents.ComboTextBox = Me.Controls("tbAbility" & i & "Text")
collCombobox.Add obEvents
Next i
End Sub
It doesn't look like you ever set the value. I assume you mean to do that in the loop? Perhaps not with the value of i, but here you can see the idea...
For i = 1 To 5
Set obEvents = New clsCombobox
'Set the value here
obEvents.Number = 52 ' 52 or whatever is needed as Number
Set obEvents.ComboBox = Me.Controls("cbAbility" & i)
Set obEvents.ComboTextBox = Me.Controls("tbAbility" & i & "Text")
collCombobox.Add obEvents
Next i

Excel VBA: Is there a way to reference an instance of a class stored in a dictionary?

I currently have instances of classes stored using the data structure presented in the image below. Each -List item is a dictionary and each -Info item is an instance of a class.
I read elsewhere that if you Set an instance variable equal to another instance, it just references the original instance. Is this correct?
I have been able to create a reference for fileInfo(1) (in the image) using the following code.
Dim prflInfo As File_Info
Set prflInfo = New File_Info
Set prflInfo = fileList.Items(0)
I have attempted to reference the branchInfo instance using the following code, but I get a Run-time error 13: Type mismatch when I attempt to do so.
Dim prbrInfo As Branch_Info
With prflInfo
Set prbrInfo = New Branch_Info
brKey = .getbrKey(0)
Set prbrInfo = .getbrItem(brKey)
End With
Edit: Included below is the code for the File_Info class. All other classes follow this basic model.
'Class Module: File_Info
'Initialise class variables
Private pfileID As Integer
Private pfilePath As String
Private pfileName As String
Private pbranchList As Scripting.Dictionary
'Declare variantcopy subroutine
Private Declare Sub VariantCopy Lib "OleAut32" (pvarDest As Any, pvargSrc As Any)
Private Sub Class_Initialize()
Set pbranchList = New Scripting.Dictionary
End Sub
Public Property Let fileID(pfileIDi As Variant)
pfileID = pfileIDi
End Property
Public Property Get fileID() As Variant
fileID = pfileID
End Property
Public Property Let filePath(pfilePathi As Variant)
pfilePath = pfilePathi
End Property
Public Property Get filePath() As Variant
filePath = pfilePath
End Property
Public Property Let fileName(pfileNamei As Variant)
pfileName = pfileNamei
End Property
Public Property Get fileName() As Variant
fileName = pfileName
End Property
Public Sub addbrConn(branch As Branch_Info)
pbranchList.Add branch.branchID, branch.brConn
Debug.Print "addbrConn ID: " & branch.branchID
End Sub
Public Sub addBranch(branch As Branch_Info)
pbranchList.Add branch.branchID, branch
Debug.Print pbranchList.Count
End Sub
Public Function countbrList()
countbrList = pbranchList.Count
End Function
Public Function getbrKey(Key As Variant)
getbrKey = pbranchList.Keys(Key)
End Function
Public Function getbrItem(Key As Variant)
getbrItem = GetByRefVariant(pbranchList.Items(Key))
End Function
Public Sub dpbrList()
With pbranchList
Debug.Print pbranchList.Count
For k = 1 To pbranchList.Count
Debug.Print .Keys(k - 1), .Items(k - 1)
Next k
End With
End Sub
Public Sub updbrList(branch As Branch_Info)
Dim branchID As String
branchID = branch.branchID
If pbranchList.exists(branchID) Then
pbranchList.Remove (branchID)
pbranchList.Add branchID, branch
Debug.Print "Complete: " & branchID & " added."
Else
Debug.Print "Error: " & branchID & "does not exist."
End If
End Sub
Private Function GetByRefVariant(ByRef var As Variant) As Variant
VariantCopy GetByRefVariant, var
End Function
Is there a way to reference the branchInfo class, to make it easier to extract the data within it?
Thanks!
Eeshwar
I do things differently in that I iterate through the keys list using a For ... each loop rather than referring to an item number. Here is a snippet that works using two levels.
You can ignore the lines where the property values are written to an array, but they were part of the original code.
cf.Dependents is a dictionary of cDependents within the cFamily object
'Declarations in Main Module
Dim dF As Dictionary, cF As cFamily, cD As cDependents
Dim I As Long, J As Long
Dim V As Variant, W As Variant
...
For Each V In dF
I = I + 1
Set cF = dF(V)
With cF
vRes(I, 1) = .FirstName
vRes(I, 2) = .LastName
vRes(I, 3) = .Birthdate
J = 2
For Each W In .Dependents
J = J + 2
Set cD = .Dependents(W)
With cD
vRes(I, J) = .Relation
vRes(I, J + 1) = .DepName
End With
Next W
End With
Next V
Note that in the sequence, as you show in your question:
set Obj = new Obj
set Obj = myClass(0)
the first line is unnecessary.
IMO it is possible to use simple VBA.Collection, here example for the FileList and BranchList. In this example List classes have Items and Info classes have reference to List where List is wrapper for a VBA.Collection. HTH
For more reading have a look e.g. here.
FileList Class
Option Explicit
Private m_fileInfoCollection As FileInfoCollection
Private Sub Class_Initialize()
Set m_fileInfoCollection = New FileInfoCollection
End Sub
Public Property Get Items() As FileInfoCollection
Set Items = m_fileInfoCollection
End Property
FileInfo Class
Option Explicit
Private m_branchList As BranchList
Private m_fileID As Integer
Private Sub Class_Initialize()
Set m_branchList = New BranchList
End Sub
Public Property Get FileID() As Integer
FileID = m_fileID
End Property
Public Property Let FileID(ByVal vNewValue As Integer)
m_fileID = vNewValue
End Property
Public Property Get BranchList() As BranchList
Set BranchList = m_branchList
End Property
FileInfoCollection Class
Option Explicit
Private m_collection As VBA.Collection
Private Sub Class_Initialize()
Set m_collection = New VBA.Collection
End Sub
Public Sub Add(ByVal newItem As FileInfo)
m_collection.Add newItem, CStr(newItem.FileID)
End Sub
Public Function ItemByKey(ByVal key As String) As FileInfo
Set ItemByKey = m_collection(key)
End Function
Public Function ItemByIndex(ByVal index As Long) As FileInfo
Set ItemByIndex = m_collection(index)
End Function
Public Function Count() As Long
Count = m_collection.Count
End Function
BranchList Class
Option Explicit
Private m_branchInfoCollection As BranchInfoCollection
Private Sub Class_Initialize()
Set m_branchInfoCollection = New BranchInfoCollection
End Sub
Public Property Get Items() As BranchInfoCollection
Set Items = m_branchInfoCollection
End Property
BranchInfo Class
Option Explicit
Private m_branchID As Integer
Public Property Get branchID() As Integer
branchID = m_branchID
End Property
Public Property Let branchID(ByVal vNewValue As Integer)
m_branchID = vNewValue
End Property
BranchInfoCollection Class
Option Explicit
Private m_collection As VBA.Collection
Private Sub Class_Initialize()
Set m_collection = New VBA.Collection
End Sub
Public Sub Add(ByVal newItem As BranchInfo)
m_collection.Add newItem, CStr(newItem.branchID)
End Sub
Public Function ItemByKey(ByVal key As String) As BranchInfo
Set ItemByKey = m_collection(key)
End Function
Public Function ItemByIndex(ByVal index As Long) As BranchInfo
Set ItemByIndex = m_collection(index)
End Function
Public Function Count() As Long
Count = m_collection.Count
End Function
Standard Module
Option Explicit
Sub Demo()
' Fill
Dim bi As BranchInfo
Set bi = New BranchInfo
bi.branchID = 111
Dim fi As FileInfo
Set fi = New FileInfo
fi.FileID = 222
fi.BranchList.Items.Add bi
Dim fl As FileList
Set fl = New FileList
fl.Items.Add fi
' Get
Dim fi1 As FileInfo
Set fi1 = fl.Items.ItemByIndex(1)
Dim bi1 As BranchInfo
Set bi1 = fi1.BranchList.Items(1)
End Sub

Trying to set up a custom object model using example, not working

I am trying to set up a custom object model using an example I found in an answered question here on stackoverflow.
VBA Classes - How to have a class hold additional classes
Here is the code I have created based on the answer.
Standard Module
Sub test()
Dim i As Long
Dim j As Long
'code to populate some objects
Dim AssemList As Collection
Dim Assem As cAssem
Dim SubAssemList As Collection
Dim SubAssem As cSubAssem
Set AssemList = New Collection
For i = 1 To 3
Set SubAssemList = New Collection
Set Assem = New cAssem
Assem.Description = "Assem " & i
For j = 1 To 3
Set SubAssem = New cSubAssem
SubAssem.Name = "SubAssem" & j
SubAssemList.Add SubAssem
Next j
Set Assem.SubAssemAdd = SubAssemList '<------ Object variable or With Block not Set
AssemList.Add Assem
Next i
Set SubAssemList = Nothing
'write the data backout again
For Each clock In AssemList
Debug.Print Assem.Description
Set SubAssemList = Assem.SubAssems
For Each SubAssem In SubAssemList
Debug.Print SubAssem.Name
Next
Next
End Sub
cAssem Class
Private pDescription As String
Private pSubAssemList As Collection
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
Public Property Get Description() As String
Description = pDescription
End Property
Public Property Let Description(ByVal sDescription As String)
pDescription = sDescription
End Property
Public Property Get SubAssems() As Collection
Set SubAssems = pSubAssemList
End Property
Public Property Set SubAssemAdd(AssemCollection As Collection)
For Each AssemName In AssemCollection
pSubAssemList.Add AssemName ' <------- This is the line that is triggering the error
Next
End Property
cSubAssem Class
Private pSubAssemName As String
Public Property Get Name() As String
Name = pSubAssemName
End Property
Public Property Let Name(ByVal sName As String)
pSubAssemName = sName
End Property
I have not changed anything in the code except class names and variable names and from my limited point of view I cannot understand the cause of the error.
I am just starting to really dig into objects and Class Modules in VBA so I appreciate any knowledge this community could pass my way.
Many Thanks
You have a typo in your sub class initializer:
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
should read:
Private Sub Class_Initialize()
Set pSubAssemList = New Collection
End Sub

Resources