Updating 1object appears to also update another. VB.Net - 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.

Related

How do I use class modules to set up parent and child variables?

First of all, excuse my explanation of my problem if I use incorrect terminology, I'm not experienced and self-taught.
I am learning to use class modules to set up "Objects" to easier reference variables and to run common functions. The issue I'm having is that I can't find information on how to set up a class module which can act as a collection to make use of the add function inherent in a collection.
For Example, I have my parent class module named clsSchool
in this class module, I have defined an object so that I can set a "child" class, clsTeacher
In my class module for clsTeacher, I have set a string variable Name. This is how my 2 class modules look.
clsSchool
Public Student As Object
clsTeacher
Public Name as String
In my module I have
modSchool
Set mySchool = New clsSchool
Set mySchool.Teacher = New clsTeacher
mySchool.Teacher.Name = "Jim"
At this point, my code is exactly what I want it is very easy to use the variable mySchool.Teacher.Name to recall "Jim", I can also use a while loop with mySchool.Teacher to recall various variables I have defined in my clsTeacher
I problem is that I want to add multiple teachers without having to set multiple classes at the top of my code as the number of teachers can vary. I.e. the following does work but has limitations.
modSchool
Set mySchool = New clsSchool
Set mySchool.Teacher1 = New clsTeacher
Set mySchool.Teacher2 = New clsTeacher
Set mySchool.Teacher3 = New clsTeacher
mySchool.Teacher1.Name = "Jim"
mySchool.Teacher2.Name = "Jack"
mySchool.Teacher3.Name = "John"
What I would like is something similar to how collections work. I.e. I want to find the total number of teachers (which varies) and create a for loop to create a new clsTeacher for each unique teacher. What I want is something like the following, but I'm stuck and I can't find any resources which help explain how to set up the following.
modSchool
Set mySchool.Teacher = New clsTeacher
n_teachers = 6
for i=1 to n_teachers
mySchool.Teacher(i).Name = Range("A1").Offset(i,0)
Next i
This way I can easily recall the name of teacher 1 or teacher 2 and use while/for loops to do so.
You can create a class (clsTeachers) which hides a collection of clsTeachers. Below is a sample of some of the things that can be done - effectively adding to what a Collection can do.
Private pTeachers as New Collection ' Debate: New in the declaration, or New in Class_Initialise?
Property Get Count() as Long
Count = pTeachers
End Property
Sub AddTeacher(NewTeacher as clsTeacher) ' enforces type.
pTeachers.Add(NewTeacher)
End Sub
Function SortTeachers1() as clsTeachers
Dim tNewTColl as clsTeachers
'some sort routine using the existing collection and adding in order to a new class
Set SortTeachers = tNewColl
End Function
Sub SortTeachers2()
Dim tNewColl as New Collection
'some sort routine using the existing collection and the new one
Set pTeachers = tNewColl
End Sub
Sub PrintTeachers()
Dim tTchr as clsTeacher
For each tTchr in pTeachers
'valid print command or add to string for later printing
Next 'tTchr
End Sub
The methods are only limited by your imagination. The one drawback I have found is no easy way to use this in a 'For Each' loop. The standard For I = 1 to MyTeachers.Count is a useful fallback - just not as neat. In the class you can have:
Property Get Teacher(Index as Long) as clsTeacher
'error check here to ensure Index exists
if tIndexValid then
Teacher = pTeachers(Index)
else
Teacher = Nothing 'or however you want to handle this
end if
End Property
I have found this way of doing collections useful because you can do validation and tailored outputs hidden from the main code view. As you can see by the signatures, you can also enforce types (i.e. your chosen collection cannot have anything but Teachers in it).
** some references for doing a for-each with custom collections:
https://www.mrexcel.com/forum/excel-questions/466141-use-custom-class-each-loop.html
How to implement custom iterable class in VBA
which includes other links.

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.

Cloning a Collection

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

should changing the parent of a listbox change the selected index?

You wouldn’t think so, but it does when the listbox is bound to a datasource (as far as I can see).
I’ve reduced the behaviour to the code below. The "if" line toggles between loading a list via data binding and loading a list “manually” (both use the same data table). In each case I set the selected index afterwards, and then change the parent form. With manual loading, the selected index is retained, with binding it is lost. I cannot see how this makes any sense – I don't see why changing the host form should alter any property of the list. Is this a bug?
Public Class Form1
Sub main() Handles Me.Load
Dim ListControl1 As ListBox = New ListBox
ListControl1.Parent = Me
Dim dt = New DataTable
dt.Columns.Add("intColourID")
dt.Columns.Add("strName")
dt.Rows.Add({1, "Red"})
dt.Rows.Add({2, "Green"})
dt.Rows.Add({3, "Blue"})
ListControl1.ValueMember = dt.Columns(0).ColumnName
ListControl1.DisplayMember = dt.Columns(1).ColumnName
If False Then
ListControl1.DataSource = dt
Else
For i = 0 To dt.Rows.Count - 1
ListControl1.Items.Add(dt.Rows(i)("strName").ToString)
Next
End If
ListControl1.SelectedIndex = 2
Dim z As Form = New Form
ListControl1.Parent = z
z.Show()
End Sub
End Class
The correct way of adding a control to a form is not to set its parent, but to add it to the Controls collection of the form. If I do it like this I do not get an exception (the three last lines commented out as you write in your comment).
Me.Controls.Add(ListControl1) ' Instead of ListControl1.Parent = Me

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.