Cloning a Collection - vba

I have two collections - collection1 and collection2
collection1 has a number of class objects in it and I am trying to fill collection2 with copies of the same objects using the following command:
Set collection2 = collection1
This doesn't give me the desired result though, because when I use
collection2.Remove 1
It removes the object at index 1 from both collections.
Below is the full code, which I hoped would output 10 objects in collection1 after removing one from collection2
Sub test()
Dim collection1 As Collection
Dim collection2 As Collection
Dim testObj As Worksheet
Dim i As Integer
Set collection1 = New Collection
Set collection2 = New Collection
For i = 1 To 10
collection1.Add testObj
Next i
Set collection2 = collection1
collection2.Remove 1
Debug.Print collection1.Count
End Sub
I tried the code below and it works but I'm looking to avoid filling both collections one by one if possible:
...
For i = 1 To 10
collection1.Add testObj
collection2.Add testObj
Next i
...
The reason I'm not so keen on this option is because ultimately I intend to use multiple collections, manipulating them and taking copies at various points so I would end up with lots of for loops in my code, rather than just one.

I am trying to fill collection2 with copies of the same objects using the following command:
Set collection2 = collection1
Yet that's not what that command does. The Set keyword doesn't "copy objects", and doesn't automagically know (or even care) that it's dealing with a Collection object that contains items.
The Set keyword assigns a reference. Nothing more, nothing less.
So what Set collection2 = collection1 does, is, literally:
Take the pointer for the collection1 object, and replace whatever collection2 is referring to with that.
That Set instruction is effectively discarding the object you created in the first place, when you did this:
Set collection2 = New Collection
You're not "copying objects" or "filling collection2" - you're overwriting its object reference, making collection2 point to the same object as collection1.
And since both pointers point to the same object... removing an item using either Collection is indeed going to be removing it from "both" collections, ...because there's only one collection object involved.
The (undocumented?) ObjPtr keyword can help shed some light on what's happening, too:
Set collection1 = New Collection
Debug.Print "collection1: " & ObjPtr(collection1)
Set collection2 = New Collection
Debug.Print "collection2: " & ObjPtr(collection2)
'the debug output contains 2 different addresses at this point
Set collection2 = collection1
Debug.Print "collection1: " & ObjPtr(collection1)
Debug.Print "collection2: " & ObjPtr(collection2)
'now the debug output clearly shows that
'both collection1 and collection2 are pointing to the same object
If you need copies of a collection, you need a function that takes a collection and returns a brand new object:
Public Function CopyCollection(ByVal source As Collection) As Collection
Dim result As New Collection
Dim item As Variant
For Each item In source
result.Add item
Next
Set CopyCollection = result
End Function
As already noted in comments though, this will only work if your collections aren't keyed. Because of how limited a Collection is (you can't iterate its keys), you'll have to use a Scripting.Dictionary instead, if you need to clone a keyed collection.

First of all, you dont need to Dim collection1 As Collection and Set collection1 = New Collection. This can be replaced by Dim collection1 As New Collection
Now, what you are doing is making the collection2 to contain the collection1. Its like putting lots of caps on a box (collection1) and then putting this box inside another box (collection2).
The alternative is to either fill both of them at the same time or do a code to copy the contents like
for i = 1 to collection1.count
collection2.add collection1.item(i)
next i

Related

VBA Class with Collection of itself

I'm trying to create a class with a Collection in it that will hold other CASN (kind of like a linked list), I'm not sure if my instantiation of the class is correct. But every time I try to run my code below, I get the error
Object variable or With block not set
CODE BEING RUN:
If (Numbers.count > 0) Then
Dim num As CASN
For Each num In Numbers
If (num.DuplicateOf.count > 0) Then 'ERROR HERE
Debug.Print "Added " & num.REF_PO & " to list"
ListBox1.AddItem num.REF_PO
End If
Next num
End If
CLASS - CASN:
Private pWeek As String
Private pVendorName As String
Private pVendorID As String
Private pError_NUM As String
Private pREF_PO As Variant
Private pASN_INV_NUM As Variant
Private pDOC_TYPE As String
Private pERROR_TEXT As String
Private pAddressxl As Range
Private pDuplicateOf As Collection
'''''''''''''''' Instantiation of String, Long, Range etc.
'''''''''''''''' Which I know is working fine
''''''''''''''''''''''
' DuplicateOf Property
''''''''''''''''''''''
Public Property Get DuplicateOf() As Collection
Set DuplicateOf = pDuplicateOf
End Property
Public Property Let DuplicateOf(value As Collection)
Set pDuplicateOf = value
End Property
''''' What I believe may be the cause
Basically what I've done is created two Collections of class CASN and I'm trying to compare the two and see if there are any matching values related to the variable .REF_PO and if there is a match I want to add it to the cthisWeek's collection of class CASN in the DuplicateOf collection of that class.
Hopefully this make sense... I know all my code is working great up to this point of comparing the two CASN Collection's. I've thoroughly tested everything and tried a few different approaches and can't seem to find the solution
EDIT:
I found the error to my first issue but now a new issue has appeared...
This would be a relatively simple fix to your Get method:
Public Property Get DuplicateOf() As Collection
If pDuplicateOf Is Nothing Then Set pDuplicateOf = New Collection
Set DuplicateOf = pDuplicateOf
End Property
EDIT: To address your question - "So when creating a class, do I want to initialize all values to either Nothing or Null? Should I have a Class_Terminate as well?"
The answer would be "it depends" - typically there's no need to set all your class properties to some specific value: most of the non-object ones will already have the default value for their specific variable type. You just have to be aware of the impact of having unset variables - mostly when these are object-types.
Whether you need a Class_Terminate would depend on whether your class instances need to perform any "cleanup" (eg. close any open file handles or DB connections) before they get destroyed.

Access VBA listing collection items in a class module

Although I'm reasonable experienced VBA developer, I have not had the need to use class modules or collections but thought this may be my opportunity to extend my knowledge.
In an application I have a number of forms which all have the same functionality and I now need to increase that functionality. Towards that, I am trying to reorder a collection in a class module, but get an error 91 - object variable or with block not set. The collection is created when I assign events to controls. The original code I obtained from here (Many thanks mwolfe) VBA - getting name of the label in mousemove event
and has been adapted to Access. The assignments of events works well and all the events work providing I am only doing something with that control such as change a background color, change size or location on the form.
The problem comes when I want to reorder it in the collection - with a view to having an impact on location in the form. However I am unable to access the collection itself in the first place.
The below is my latest attempt and the error occurs in the collcount Get indicated by asterisks (right at the bottom of the code block). I am using Count as a test. Once I understand what I am doing wrong I should be able to manipulate it as required.
mLabelColl returns a correct count before leaving the LabelsToTrack function, but is then not found in any other function.
As you will see from the commented out debug statements, I have tried making mLabelColl Private and Dim in the top declaration, using 'Debug.Print mLabelColl.Count' in the mousedown event and trying to create a different class to store the list of labels.
I feel I am missing something pretty simple but I'm at a loss as to what - can someone please put me out of my misery
Option Compare Database
Option Explicit
'weMouseMove class module:
Private WithEvents mLbl As Access.Label
Public mLabelColl As Collection
'Dim LblList as clLabels
Function LabelsToTrack(ParamArray labels() As Variant)
Set mLabelColl = New Collection 'assign a pointer
Dim i As Integer
For i = LBound(labels) To UBound(labels)
'Set mLabelColl = New Collection events not assigned if set here
Dim LblToTrack As weMouseMove 'needs to be declared here - why?
Set LblToTrack = New weMouseMove 'assign a pointer
Dim lbl As Access.Label
Set lbl = labels(i)
LblToTrack.TrackLabel lbl
mLabelColl.Add LblToTrack 'add to mlabelcoll collection
'Set LblList as New clLabels
'LblList.addLabel lbl
Next i
Debug.Print mLabelColl.Count 'returns correct number
Debug.Print dsform.countcoll '1 - incorrect
End Function
Sub TrackLabel(lbl As Access.Label)
Set mLbl = lbl
End Sub
Private Sub mLbl_MouseDown(Button As Integer, Shift As Integer, x As Single, Y As Single)
Dim tLbl As Access.Label
'Debug.Print LblList.Count 'Compile error - Expected function or variable (Despite Count being an option
'Debug.Print mLabelColl.Count 'error 91
'Debug.Print LblList.CountLbls 'error 91
Debug.Print collCount
End Sub
Property Get collCount() As Integer
*collCount = mLabelColl.Count* 'error 91
End Property
In order to have all the weMouseMove objects reference the same collection in their mLabelColl pointer, a single line can achieve it:
LblToTrack.TrackLabel lbl
mLabelColl.Add LblToTrack
Set LblToTrack.mLabelColl = mLabelColl ' <-- Add this line.
But please be aware that this leads to a circular reference between the collection and its contained objects, a problem that is known to be a source of memory leaks, but this should not be an important issue in this case.

Method or Data Member not found Error

I am trying to filter my combobox on Outlook Macro. But I am getting the error as mentioned on title. Could you please help me to solve this problem?
It works on Visual Studio but doesn't work on Outlook Macro.
Private Sub cmbProjects_Change()
Dim SelectedText As String
SelectedText = cmbProjects.text
Dim Items As Object
Items = CreateObject("System.Collections.ArrayList")
Items = cmbProjects.Items
Dim newList As Object
newList = CreateObject("System.Collections.ArrayList")
For Each Item In Items
If Item.Contains(SelectedText) Then
newList.Add (Item)
End If
Next
cmbProjects.DataSource = newList
End Sub
You need to add set since it's an object reference. And you are trying to access this object's (you assumed as an ArrayList) with its member methods/properties. Which fails. So change to,
Set Items = CreateObject("System.Collections.ArrayList")
Set newList = CreateObject("System.Collections.ArrayList")
Further check if you have the right reference added in your VBA editor for collection library objects.
CreateObject("System.Collections.ArrayList")
As per my comments, Items = cmbProjects.Items also fails with the same error. That's because cmbProjects doesn't have a method called Items. So you need to use List instead.. List returns a variant object.
Since you can't cast a variant into an ArrayList. You have to iterate through the variant and add items into the ArrayList. Note I am writing on the fly, the code is not tested. You may test it out and comment, should you need further assistance. I strongly recommend you to validate this pseudo-code against the accurate syntax.
Dim vr as Variant
Dim i as Integer
vr = cmbProject.List
'--add error handling to check if vr is empty or if cmbProject is empty.
For i = 0 to UpperBound(vr)
Items.Add (vr(i))
Next i

How to adapt a loop's structure to a user's preference?

I'm using a loop to fill a collection. There are a few properties for each item of the collection, but some of these properties are optional. The user is prompted to choose which properties will be copied to the collection. Is it possible to omit the code for the optional properties if the user has chosen to ignore them?
Sub fillcoll()
Dim coll as Collection
Set coll = New Collection
Dim NewItem as Class1
For each r in Selection.Rows
Set NewItem = New Class1
If Userform1.Checkbox1.Value = True then
NewItem.Property1 = somearray1(r.Row)
End If
If Userform1.Checkbox2.Value = True then
NewItem.Property2 = somearray2(r.Row)
End If
If Userform1.Checkbox3.Value = True then
NewItem.Property3 = somearray3(r.Row)
End If
Next r
End Sub
With this code, the Checkboxes' values are read at each iteration. I fear that this may slow down the program's execution unnecessarily. The checkboxes could be read once and the loop's contents would adapt to the checkboxes' values. Is this possible?
Thanks in advance.
Read the checkboxes at the beginning,out of the loop, and assign their values to three booleans or an array of booleans. then you just read from the boolean variables every time.
this will improve performance since you do not need to access any object variable, but just a boolean that lays inside your class/object.

Updating 1object appears to also update another. VB.Net

I having a problem in my website where I have 2 objects held in session, both object are structured differently and do not have any links to each other (not intentionally). Both objects however, have a collection of "Travellers". What I am trying to do is modify one collection by adding or removing "Travellers", when a finish button is clicked, the modified collection will then be saved to the other object.
The problem is that for some reason when I modify one collection in one object, the modifications are also made to the collection in the other object.
Here's a rough example:
Dim bookingContext As BookingContext = Session("BookingContext")
Dim personSearchContext As PersonSearchContext = Session("PersonSearchContext")
Dim personId As Integer = Request.Form("PersonID")
Dim mode As String = Request.Form("Mode")
Select Case mode
Case "Add"
For Each traveller As PersonProfile In personSearchContext.Travellers
If traveller.PersonID = personId Then
personSearchContext.SelectedTravellers.Add(traveller)
Exit For
End If
Next
context.Session("PersonSearchContext") = personSearchContext
Case "Remove"
For Each traveller As PersonProfile In personSearchContext.SelectedTravellers
If traveller.PersonID = personId Then
travellerSearchContext.SelectedTravellers.Remove(traveller)
Exit For
End If
Next
context.Session("PersonSearchContext") = personSearchContext
Case "Save"
bookingContext.Travellers = personSearchContext.SelectedTravellers
context.Session("BookingContext") = bookingContext
End Select
The issue is the Travellers collection within the object "BookingContext" is being updated when I add and remove from the collection within "PersonSearchContext". It's as though there is some sort of link or pointer between them.
Any ideas?
Cheers.
The problem is this line in your Save code:
bookingContext.Travellers = personSearchContext.SelectedTravellers
What you're doing there is taking the reference to the SelectedTravellers collection in the personSearchContext object and assigning the reference to the Travellers collection in the bookingContext object. This assignment means that bookingContext.Travellers is no longer a separate collection from personSearchContext.SelectedTravellers. They're now both references to the same object in memory.