Tearing Down Circular References - vba

The following code creates a circular reference for each element in the collection. Is the code in the UserForm_Terminate routine sufficient to tear down the relationships to allow the memory to be released? Or is there a requirement to use pointers and weak references?
If so/not what is the best method for testing whether the objects have been released?
Userform Code:
Option Explicit
Implements IBtnClick
Dim coll As Collection
Private Sub UserForm_Initialize()
Dim x As Long
Dim e As CBtnEvents
Set coll = New Collection
For x = 1 To 5
Set e = New CBtnEvents
Set e.btn = Me.Controls.Add("Forms.CommandButton.1")
e.ID = x
e.Register Me
With e.btn
.Height = 30
.Width = 30
.Top = 10
.Left = .Width * x
End With
coll.Add e
Next x
End Sub
Private Sub UserForm_Terminate()
Dim itm
For Each itm In coll
msgbox itm.ID
itm.Unregister
Next itm
End Sub
Private Sub IBtnClick_click(ID As Long)
MsgBox ID
End Sub
IBtnClick Code:
Public Sub click(ID As Long)
End Sub
CBtnEvents Code:
Private WithEvents p_btn As MSForms.CommandButton
Private p_ID As Long
Private click As IBtnClick
Public Property Set btn(value As MSForms.CommandButton)
Set p_btn = value
End Property
Public Property Get btn() As MSForms.CommandButton
Set btn = p_btn
End Property
Public Sub Register(value As IBtnClick)
Set click = value
End Sub
Public Sub Unregister()
Set click = Nothing
End Sub
Private Sub p_btn_Click()
click.click p_ID
End Sub
Public Property Get ID() As Long
ID = p_ID
End Property
Public Property Let ID(ByVal lID As Long)
p_ID = lID
End Property
Private Sub Class_Terminate()
MsgBox p_ID
End Sub
I have included the VB6 tag as I think the question applies equally, but I am using Excel VBA.

This is how we (manually) keep our instance book-keeping collection:
In every class/form/control we place something like this
Option Explicit
DefObj A-Z
Private Const MODULE_NAME As String = "cTransStub"
'=========================================================================
' Constants and member variables
'=========================================================================
' Consts here
' Vars here
#If DebugMode Then
Private m_sDebugID As String
#End If
' Props here
' Methods here
'=========================================================================
' Base class events
'=========================================================================
#If DebugMode Then
Private Sub Class_Initialize()
DebugInstanceInit MODULE_NAME, m_sDebugID, Me
End Sub
Private Sub Class_Terminate()
DebugInstanceTerm MODULE_NAME, m_sDebugID
End Sub
#End If
Sample implementation of helper DebugInstanceInit/Term subs that populate DebugIDs collection:
Public Sub DebugInstanceInit(sModuleName As String, sDebugID As String, oObj As Object)
Dim sCount As String
Dim lObjPtr As Long
Dim sObjCtx As String
On Error Resume Next
sDebugID = sDebugID & GetDebugID()
If DebugIDs Is Nothing Then
Else
...
lObjPtr = ObjPtr(oObj)
DebugIDs.Add sDebugID & " " & LIB_NAME & "." & sModuleName & "|&H" & Hex(lObjPtr) & "|" & Format$(time, "hh:mm:ss") & "|" & sObjCtx & "|", "#" & sDebugID
End If
...
If Not DebugConsole Is Nothing Then
DebugConsole.RefreshConsole
End If
On Error GoTo 0
End Sub
Public Sub DebugInstanceTerm(sModuleName As String, sDebugID As String)
On Error Resume Next
If DebugIDs Is Nothing Then
Else
DebugIDs.Remove "#" & sDebugID
End If
...
If Not DebugIDs Is Nothing Then
If DebugIDs.Count = 0 Then
Debug.Print "DebugIDs collection is empty"; Timer
End If
End If
If Not DebugConsole Is Nothing Then
DebugConsole.RefreshConsole
End If
On Error GoTo 0
End Sub
Upon program termination we warn for any object leaking in DebugIDs collection.

Related

One event sub for multiple textboxes instead of declaring it multiple time?

Is it possible to have one change event procedure for multiple textboxes in the same workbook?
If for example, I have a textbox named "textbox3" in the 1st, 2nd, and 3rd sheet of the workbook and I would like this single code below to work for all of them (the textboxes) rather than having to declare it on each sheet. Right now, I have to declare the same procedure on all the sheets but I only want to declare it once since it does the same thing on all of them.
'my procedure
Sub testObj()
Dim i As Integer, obj As oleobject
Set ac = ThisWorkbook.ActiveSheet
For Each obj In ac.oleobjects
If TypeName(obj.Object) = "TextBox" And obj.name = "TextBox3" Then
i = i + 1
ReDim Preserve TextArray(1 To i)
Set TextArray(i).TextBoxEvents = obj
End If
Next obj
Set obj = Nothing
End Sub
'My class1
Public WithEvents TextBoxEvents As MSForms.TextBox
'Public WithEvents TextBoxEvents As OLEObject
Private Sub TextBoxEvents_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
If KeyCode = vbKeyTab Then
TextBox12.Activate
End If
End Sub
Here's an example
Class module named clsTextBox:
Option Explicit
Public WithEvents TextBoxEvents As MSForms.TextBox
Private Sub TextBoxEvents_Change()
Debug.Print TextBoxEvents.Name & ": " & TextBoxEvents.Text
End Sub
'Private Sub TextBoxEvents_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
' If KeyCode = vbKeyTab Then
' TextBox12.Activate 'unclear what you're aiming for here?
' End If
'End Sub
Regular module:
Dim ColTB As Collection
Sub testObj()
Dim i As Integer, obj As OLEObject, ac As Worksheet
Set ac = ThisWorkbook.ActiveSheet
Set ColTB = New Collection
For Each obj In ac.OLEObjects
If TypeName(obj.Object) = "TextBox" Then
ColTB.Add EventObj(obj.Object)
End If
Next obj
Set obj = Nothing
End Sub
Function EventObj(obj As MSForms.TextBox) As clsTextBox
Dim o As New clsTextBox
Set o.TextBoxEvents = obj
Set EventObj = o
End Function

Dictionary of instances of another Class 'cBeam ' as variable in an instance of Class 'cNode'

When I execute the following sub, I instantiate the class cNode within which another set of class cBeam are instantiated. The instance of cBeam are stored in a dictionary vBeamList which are created within the Private Sub Initialize() section. I checked the values using Debug.Print and it works fine. However, when it enters Public Property Get Beam(MemberNo As Long) As cBeam it shows that vBeamList(MemberNo) is empty!
Sub Main()
Dim dandelion As Object
Dim NodeConnectivityDict As Object
Dim NodeListDict As Object
Set NodeListDict = CreateObject("Scripting.Dictionary")
'1. Creates dictionary of Nodes and connected beams | Node: [Node, b1,b2,b3, , ]
Call SelectNodeConnection(dandelion)
'2. Cleans the empty elements of beam array in #1 and adds end index as multiarray. | Node:[[b1,0],[b2,1],[b3,1]]
Set NodeConnectivityDict = CleanNodeDictAndGetBeamConnectivity(dandelion)
'3. Get the list of all beams connected to node and instantiate a class that shows the Node and beam with the
'LC stored in them
Call DevelopInstancesNodeBeamLC(NodeConnectivityDict, NodeListDict)
Debug.Print NodeListDict(58).Beam(501).ID **<-When the error showsup**
End Sub
Sub DevelopInstancesNodeBeamLC(NodeConnectivityDict, NodeListDict)
Dim nodeInstance As cNode
For Each varKey In NodeConnectivityDict.keys():
Set NodeListDict(varKey) = New cNode
NodeListDict(varKey).Initialize varKey, NodeConnectivityDict(varKey)
Next
End Sub
Class cNode
Private vID As Long
Private vBeamArray() As Variant
Private vNumBeams As Integer
Private vBeamList As Object
Private Sub Class_Initialize()
Set vBeamList = CreateObject("Scripting.Dictionary")
End Sub
Public Sub Initialize(Node, BeamList)
vID = Node
vBeamArray = BeamList
vNumBeams = UBound(vBeamArray) - LBound(vBeamArray) + 1
For Each i In vBeamArray:
vBeamList.Add i(0), New cBeam
vBeamList(i(0)).Initialize i(0), i(1)
Next
End Sub
Public Property Get ID() As Long
ID = vID
End Property
Public Property Get NumberOfBeams() As Integer
NumberOfBeams = vNumBeams
End Property
Public Property Get Beam(MemberNo As Long) As cBeam
Set Beam = vBeamList(MemberNo)
End Property
Private Sub Class_Terminate()
End Sub
Class cBeam
Private vID As Long
Private vConnectivity As Long
Private Sub Class_Initialize()
End Sub
Public Sub Initialize(BeamID, Connectivity)
vID = BeamID
vConnectivity = Connectivity
End Sub
Public Property Get ID() As Long
ID = vID
End Property
Public Property Get Connectivity() As Long
Connectivity = vConnectivity
End Property
Private Sub Class_Terminate()
End Sub
I have run out of ideas. What am I doing wrong?
Using this slightly modified version of your Main sub (and no other changes), I get the expected output.
Sub Main()
Dim NodeConnectivityDict As Object
Dim NodeListDict As Object
Set NodeConnectivityDict = CreateObject("Scripting.Dictionary")
Set NodeListDict = CreateObject("Scripting.Dictionary")
NodeConnectivityDict.Add 33, Array(Array(10, 20), Array(30, 40), Array(50, 60))
NodeConnectivityDict.Add 58, Array(Array(1, 2), Array(3, 4), Array(501, 502))
DevelopInstancesNodeBeamLC NodeConnectivityDict, NodeListDict
Debug.Print NodeListDict(33).Beam(30).ID '30
Debug.Print NodeListDict(33).Beam(30).Connectivity '40
Debug.Print NodeListDict(58).Beam(501).ID '501
Debug.Print NodeListDict(58).Beam(501).Connectivity '502
End Sub

Issue with Assigning Toggle buttons to Class Module

Can anybody help me what I did wrong here? the controls are not attaching to the Class!
My Class Module: CTglBtn
Option Explicit
Public WithEvents tgl1 As MSForms.ToggleButton
Private ac$
Public Property Get ACNumber() As String
ACNumber = ac
End Property
Public Property Let ACNumber(value As String)
ac = value
End Property
Private Sub tgl1_Click()
' do something here
End Sub
and here is where I am assigning the controls of my form to the class.
Dim Ctgl As CTglBtn
Dim Coll As Collection
Private Sub UserForm_Initialize()
Dim aclist As Range
Set aclist = ThisWorkbook.Sheets("panel").Range("acnum")
For i = 1 To 10
Set Ctgl = New CTglBtn
Set Ctgl.tgl1 = Me.Controls("TB" & i)
Ctgl.ACNumber = aclist.Cells(i + 1, 1)
Me.Controls("TB" & i).Caption = Ctgl.ACNumber
Coll.Add Ctgl
Set Ctgl = Nothing
Next
' MsgBox Coll.Count
End Sub
Dim Coll As Collection needs to be a module-level declaration, otherwise it is cleared as soon as the routine ends.
You also need to initialize that variable:
Set coll = New Collection

VBA Classes - How to have a class hold additional classes

I have a challenge that I am trying to solve using classes.
I am logging transactions into a class.
Each transaction has the following:
Name
Date
Time
Description
However each transaction can also have many business related contacts with the following properties
Business Contact Name
Business Area
Percentage of Bill
Are there any examples of how this would be done.
I have tried adding a second class for the business contact and then building a collection inside the transaction class, all with no joy.
I have also tried making the business contact details a collection within the transaction class also with no joy.
Below is what I have so far, but i may have gone down a blind alley and it may not be worth trying to rescue the code
Any help much appreciated.
Thanks
JP
Test sub - trying to write the data in and get it back out
Sub test()
Dim x As Integer
Dim xx As Integer
'code to populate some objects
Dim clocklist As Collection
Dim clock As classClocks
Dim businesscontactlist As Collection
Dim businesscontact As classBusinessContact
Set businesscontactlist = New Collection
Set clocklist = New Collection
For x = 1 To 3
Set clock = New classClocks
clock.LawyerName = "lawyer " & Str(x)
For xx = 1 To 3
businesscontact.Name = "Business Contact " & Str(xx)
businesscontactlist.Add businesscontact
Next xx
clock.BusinessContactAdd businesscontactlist '----- errors here
clocklist.Add clock
Next x
Set businesscontactlist = Nothing
'write the data backout again
For Each clock In clocklist
Debug.Print clock.LawyerName
Set businesscontactlist = clock.BusinessContacts
For Each businesscontact In businesscontactlist
Debug.Print businesscontact.Name
Next
Next
End Sub
Clock Class - this is the transaction class
Private pLawyerName As String
Private pBusinessContactList As Collection
Public Property Get LawyerName() As String
LawyerName = pLawyerName
End Property
Public Property Let LawyerName(ByVal sLawyerName As String)
pLawyerName = sLawyerName
End Property
Public Property Get BusinessContacts() As Collection
Set BusinessContacts = pBusinessContactList
End Property
Public Property Set BusinessContactAdd(ByRef strName() As Collection)
Set pBusinessContactList = New Collection
Dim businesscontact As classBusinessContact
Set businesscontact = New classBusinessContact
For Each businesscontact In strName
businesscontact.Name = strName.Item()
pBusinessContactList.Add businesscontact
Next
End Property
Business contact Class - For the moment it only has one property
Private pBusinessContactName As String
Public Property Get Name() As String
Name = pBusinessContactName
End Property
Public Property Let Name(ByVal sName As String)
pBusinessContactName = sName
End Property
There are a few things that don't do what you expect in your code. I have cleaned it a bit and this new version should be closer to what you want. Let me know if the changes are not self-explanatory.
Main procedure:
Sub test()
Dim i As Long
Dim j As Long
'code to populate some objects
Dim clocklist As Collection
Dim clock As classClocks
Dim businessContactList As Collection
Dim businessContact As classBusinessContact
Set clocklist = New Collection
For i = 1 To 3
Set businessContactList = New Collection
Set clock = New classClocks
clock.LawyerName = "lawyer " & i
For j = 1 To 3
Set businessContact = New classBusinessContact
businessContact.Name = "Business Contact " & j
businessContactList.Add businessContact
Next j
Set clock.BusinessContactAdd = businessContactList
clocklist.Add clock
Next i
Set businessContactList = Nothing
'write the data backout again
For Each clock In clocklist
Debug.Print clock.LawyerName
Set businessContactList = clock.BusinessContacts
For Each businessContact In businessContactList
Debug.Print businessContact.Name
Next
Next
End Sub
classClocks:
Private pLawyerName As String
Private pBusinessContactList As Collection
Private Sub Class_Initialize()
Set pBusinessContactList = New Collection
End Sub
Public Property Get LawyerName() As String
LawyerName = pLawyerName
End Property
Public Property Let LawyerName(ByVal sLawyerName As String)
pLawyerName = sLawyerName
End Property
Public Property Get BusinessContacts() As Collection
Set BusinessContacts = pBusinessContactList
End Property
Public Property Set BusinessContactAdd(contactCollection As Collection)
For Each contactName In contactCollection
pBusinessContactList.Add contactName
Next
End Property
I tend to make everything a class and chain the class calls together to access them. It's not a better way than the one that assylias posted, just different. And you may prefer it.
CClocks (collection class that's the parent of the CClock instances)
Private mcolClocks As Collection
Private Sub Class_Initialize()
Set mcolClocks = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolClocks = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = mcolClocks.[_NewEnum]
End Property
Public Sub Add(clsClock As CClock)
If clsClock.ClockID = 0 Then
clsClock.ClockID = Me.Count + 1
End If
Set clsClock.Parent = Me
mcolClocks.Add clsClock, CStr(clsClock.ClockID)
End Sub
Public Property Get clock(vItem As Variant) As CClock
Set clock = mcolClocks.Item(vItem)
End Property
Public Property Get Count() As Long
Count = mcolClocks.Count
End Property
CClock class
Private mlClockID As Long
Private msLawyer As String
Private mlParentPtr As Long
Private mclsContacts As CContacts
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(dest As Any, Source As Any, ByVal bytes As Long)
Public Property Set Contacts(ByVal clsContacts As CContacts): Set mclsContacts = clsContacts: End Property
Public Property Get Contacts() As CContacts: Set Contacts = mclsContacts: End Property
Public Property Let ClockID(ByVal lClockID As Long): mlClockID = lClockID: End Property
Public Property Get ClockID() As Long: ClockID = mlClockID: End Property
Public Property Let Lawyer(ByVal sLawyer As String): msLawyer = sLawyer: End Property
Public Property Get Lawyer() As String: Lawyer = msLawyer: End Property
Public Property Get Parent() As CClocks: Set Parent = ObjFromPtr(mlParentPtr): End Property
Public Property Set Parent(obj As CClocks): mlParentPtr = ObjPtr(obj): End Property
Private Function ObjFromPtr(ByVal pObj As Long) As Object
Dim obj As Object
CopyMemory obj, pObj, 4
Set ObjFromPtr = obj
' manually destroy the temporary object variable
' (if you omit this step you'll get a GPF!)
CopyMemory obj, 0&, 4
End Function
Private Sub Class_Initialize()
Set mclsContacts = New CContacts
End Sub
Private Sub Class_Terminate()
Set mclsContacts = Nothing
End Sub
CContacts (parent class to CContact and a child to each CClock class)
Private mcolContacts As Collection
Private Sub Class_Initialize()
Set mcolContacts = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolContacts = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = mcolContacts.[_NewEnum]
End Property
Public Sub Add(clsContact As CContact)
If clsContact.ContactID = 0 Then
clsContact.ContactID = Me.Count + 1
End If
Set clsContact.Parent = Me
mcolContacts.Add clsContact, CStr(clsContact.ContactID)
End Sub
Public Property Get Contact(vItem As Variant) As CContact
Set Contact = mcolContacts.Item(vItem)
End Property
Public Property Get Count() As Long
Count = mcolContacts.Count
End Property
CContact
Private mlContactID As Long
Private msContactName As String
Private mlParentPtr As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(dest As Any, Source As Any, ByVal bytes As Long)
Public Property Let ContactID(ByVal lContactID As Long): mlContactID = lContactID: End Property
Public Property Get ContactID() As Long: ContactID = mlContactID: End Property
Public Property Let ContactName(ByVal sContactName As String): msContactName = sContactName: End Property
Public Property Get ContactName() As String: ContactName = msContactName: End Property
Public Property Get Parent() As CContacts: Set Parent = ObjFromPtr(mlParentPtr): End Property
Public Property Set Parent(obj As CContacts): mlParentPtr = ObjPtr(obj): End Property
Private Function ObjFromPtr(ByVal pObj As Long) As Object
Dim obj As Object
CopyMemory obj, pObj, 4
Set ObjFromPtr = obj
' manually destroy the temporary object variable
' (if you omit this step you'll get a GPF!)
CopyMemory obj, 0&, 4
End Function
And the test procedure
Sub test()
Dim i As Long, j As Long
Dim clsClocks As CClocks
Dim clsClock As CClock
Dim clsContact As CContact
Set clsClocks = New CClocks
For i = 1 To 3
Set clsClock = New CClock
clsClock.Lawyer = "lawyer " & i
For j = 1 To 3
Set clsContact = New CContact
clsContact.ContactName = "Business Contact " & i & "-" & j
clsClock.Contacts.Add clsContact
Next j
clsClocks.Add clsClock
Next i
'write the data backout again
For Each clsClock In clsClocks
Debug.Print clsClock.Lawyer
For Each clsContact In clsClock.Contacts
Debug.Print , clsContact.ContactName
Next clsContact
Next clsClock
End Sub
Instead of having Contacts as an integral part of CClock, I make it its own class/collection class. Then I can access like
clsClock.Contacts.Item(1).ContactName
And I can use CContacts somewhere else in my code if it comes up.
You can ignore the NewEnum and CopyMemory stuff or read about it here http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/ and here http://www.dailydoseofexcel.com/archives/2007/12/28/terminating-dependent-classes/#comment-29661 Those two parts are so I can have a Parent property without worrying about garbage collection (CopyMemory and ObjPtr) and so I can For.Each through the class (NewEnum).
I haven't done VBA for a while, but I noticed this line:
Public Property Set BusinessContactAdd(ByRef strName() As Collection)
I think putting parentheses on a parameter name indicates that it's an array, which yours is not: it's a single instance of a collection.

Programmatically adding a commandbutton to a userform

In excel vba I have added a commandbutton to userform... like below
Set ctrl = Me.Controls.Add( _
bstrProgID:="Forms.CommandButton.1", _
Name:="CommandButton1", Visible:=True)
Now I wanted to know how would I tell it what to do when it is clicked?
This is one of those techniques that vba will let you do, but you probably shouldn't. For all the same reasons you shouldn't use code that alters your code.
That said, here is how to do what you want. First insert a class module and name it DynBtn, then paste this code into it:
Private WithEvents mobjBtn As MSForms.CommandButton
Private msOnAction As String
''// This has to be generic or call by name won't be able to find the methods
''// in your form.
Private mobjParent As Object
Public Property Get Object() As MSForms.CommandButton
Set Object = mobjBtn
End Property
Public Function Load(ByVal parentFormName As Object, ByVal btn As MSForms.CommandButton, ByVal procedure As String) As DynBtn
Set mobjParent = parentFormName
Set mobjBtn = btn
msOnAction = procedure
Set Load = Me
End Function
Private Sub Class_Terminate()
Set mobjParent = Nothing
Set mobjBtn = Nothing
End Sub
Private Sub mobjBtn_Click()
CallByName mobjParent, msOnAction, VbMethod
End Sub
Now to use this in your form, create a blank user form and paste this code into it:
Private Const mcsCmdBtn As String = "Forms.CommandButton.1"
Private mBtn() As DynBtn
Private Sub UserForm_Initialize()
Dim i As Long
ReDim mBtn(1) As DynBtn
For i = 0 To UBound(mBtn)
Set mBtn(i) = New DynBtn
Next
''// One Liner
mBtn(0).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn1", True), "DoSomething").Object.Caption = "Test 1"
''// Or using with block.
With mBtn(1).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn2", True), "DoSomethingElse").Object
.Caption = "Test 2"
.Top = .Height + 10
End With
End Sub
Public Sub DoSomething()
MsgBox "It Worked!"
End Sub
Public Sub DoSomethingElse()
MsgBox "Yay!"
End Sub
Private Sub UserForm_Terminate()
Erase mBtn
End Sub