Remove reference from List Of Objects in vb.net - vb.net

I want to copy the content of one List(Of Object) to another and modify a single value. Is there a way to remove the reference?
In the code sample I get the output 'test2 test2' when I expect 'test1 test2'.
Module Module1
Sub Main()
Dim ListOfSample As New List(Of sample)
Dim var1 As New sample
var1.Name = "test"
ListOfSample.Add(var1)
ListOfSample.Add(var1)
Dim NewListOfSample As New List(Of sample)
NewListOfSample.AddRange(ListOfSample)
NewListOfSample(1).Name = "test2"
Console.Write(NewListOfSample(0).Name & " " & NewListOfSample(1).Name)
End Sub
End Module
Public Class sample
Public Name As String
End Class

Since your list is a list of Objects, when you perform add range, you are not adding "copies", instead you are adding the pointers (references) to the same objects that are in your original list.
You will need to clone all of your objects in the first list, and then add those clones to your second list. When it comes to cloning there are several different ways in .NET. Here's a post on getting deep copies of objects that does a good job explaining your options: Deep Copy of an Object
You can either create a clone method on your "sample" object to return a newly initialized copy of itself, or you can use some of the serialization methods mentioned in the post I linked to.

In the line NewListOfSample.AddRange(ListOfSample) you're adding references to your new list. So whatever you change in your new list will update the reference in your original list (they're both pointing to the same objects). You need to add new instances of Sample to the second list for it to contain independent items.

Related

How to handle object declaration in VBA (Error 91)

I'm stuck in VBA and I couldn't find a good answer in the other questions related to error 91. I want to create an object and store variables and arrays inside that object. I tried an approach like I would do in js:
Dim block As Object
...
Set block = Nothing
block.Name = "Unbekannter Prüfblock"
block.Awf = "Unbekannter Anwendungsfall"
block.Conditions = Array()
block.Checks = Array()
I use the "Set block = Nothing" because I will use it multiple times in a loop.
But all I get is error 91 - Object variable not set
How can I set the object?
Do I really have to declare everything in vba?
Isn't there a "stop annoying me with declaration notices" toggle? ;-)
Update
Thank you all so much for the detailed answers!
As suggested I created a class for "block" and also a class for "condition" and "check". Block for example:
Option Explicit
Public name As String
Public awf As String
Public conditions As Collection
Public checks As Collection
Then I use it inside my code like this:
Dim bl As Block
Dim co As Condition
Dim ce As Check
Set bl = New Block
bl.name = ws.Range("B" & i).value
bl.awf = ws.Range("B" & i).value
Set co = New Condition
co.attr = ws.Range("B" & i).value
co.value = ws.Range("C" & i).value
bl.conditions.Add co
VBA isn't Javascript; objects and their members cannot be created inline, they need a class definition.
When you make a member call against an object, you refer to it by name, and whenever that name refers to a null reference (Nothing) you'll get error 91.
To fix it, you need to ensure every member call is made against a valid object reference. Using the Set keyword you can assign such a reference, and to create a new instance of an object you can use the New keyword followed by the name of the class that defines the type you want a new instance of:
Dim Block As Object
Block.Something = 42 ' Error 91
Set Block = New SomeClass ' set reference
Block.Something = 42 ' OK
Note that because the object is declared As Object, every member call is late-bound (resolved at run-time); if the member doesn't exist (or if there's a typo), you'll get error 438 at run-time.
You can move this error to compile-time with early binding by using a more specific data type for the declaration:
Dim Block As SomeClass
Because the members of SomeClass are known at compile-time, the IDE will now provide you with a member completion list when you type up a member call, and typos will no longer be valid at compile-time: strive to remain in the early-bound realm whenever possible! Note: As Variant (explicit or not) is also similarly late-bound.
So we add a new class module and call it SomeClass and we add a couple of public fields:
Option Explicit
Public Name As String
Public Case As String
Public Condition As Variant
Public Check As Variant
And now you can create and consume a new instance of that class, and add instances of it to a collection to process later (note: you can't do that with a UDT/Type).
The VBIDE settings have an annoying option ("automatic syntax check", IIRC) that immediately pops a message box whenever there's a compilation error on the current line; uncheck it (invalid lines will appear in red, without a message box), but do have the "require variable declaration" setting checked: it will add Option Explicit to every module, and that will spare you from a number of easily avoidable run-time errors, moving them to compile-time.
In JS, you can add properties (together with values) on the fly to an object. That's not possible in VBA (and most other languages).
Your declaration Dim block As Object is defining a variable that is supposed to point to an Object. But it isn't pointing to anything yet, per default it is initialized with Nothing, which is, literally, nothing, and has neither properties nor methods, it's just a placeholder to signal "I don't point to anything yet". Furthermore, Object cannot be instantiated.
in VBA, you assign something to an object variable with Set (this is different to most other languages). If you want to create a new object, you use the keyword New.
However, before you do that, you need to know what kind of object (which class) you need. This can be an existing class, eg a Worksheet or a Range in Excel, or it can be an own defined class (by creating a new class module in your code). In any case, you need to define the properties and the methods of that class. Considering the most simple class module Class1 (of course you should think about a more usefull name):
Option Explicit
Public name as String
Public case as String
(I will not start to talk about private members and getter and setter).
You then write
Dim block As Class1
Set block = New Class1
block.name = "Unbekannter Prüfblock"
(But block.data = "Hello world" will not be possible as data is not a member of your class.)
Big advantage of this attempt is that the compiler can show you when you for example mistyped a property name before you even start your code (Debug->Compile). In JS, you will get either a runtime error or a new property on the fly - hard to find nasty bugs.
If you later need a new (empty) object, just create a new object using New. If the old object is not referenced anywhere else, the VBA runtime will take care that the memory is freed (so no need to write Set block = Nothing).
In case you really don't know the properties in advance (eg when you read XML or JSON files or a key-value list from an Excel sheet...), you can consider to use a Collection or a Dictionary. Plenty of examples on SO and elsewhere.
One remark to block.Condition = Array(). Arrays in VBA are not really dynamic, you cannot add or remove entries easily during runtime. In VBA, you have static and dynamic arrays:
Dim a(1 to 10) as String ' Static array with 10 members (at compile time)
Dim b() as String ' Dynamic array.
However, for dynamic members you need to define how many members you need before you write something into it, you use the ReDim statement for that. Useful if you can calculate the number of members in advance:
Redim b(1 to maxNames) ' Dynamic array, now with maxNames members (whatever maxNames is)
You can change the array size of a dynamic array with Redim Preserve, but that should be an exception (bad programming style, inperformant). Without Preserve, you will get a new array, but the former data is lost.
Redim Preserve b(1 to maxNames+10) ' Now with 10 more members.
If you really don't know the number of members and it can change often during runtime, again a Collection or a Dictionary can be the better alternative. Note that for example a Dictionary can itself a Dictionary as value, which allows to define Tree structures.
Regarding your issue adding to the collection:
You need to add this code to your class module "Block" - only then you can add objects to the collections
Private Sub Class_Initialize()
Set conditions = New Collection
set checks = new Collection
End Sub

How to Build a List of Form Controls Before the Form is Loaded

I’m new to OOP and VB.NET, so please bear with me.
In VB.Net I currently have working code that defines, for each form, significant information about selected controls on the form.
I have defined in a public class:
Public Class FormFld
Public Property ScrField As Control ' A control on the form
Public Property DbField As String ' Its corresponding field name in the database
… ' Other info about the control or its database field
End Class
When each form loads, I create a list of FormFlds for the form’s selected controls:
At the start of each form
ReadOnly FormFlds As New List(Of FormFld)()
and in the form’s Load routine
FormFlds.Add(New FormFld With {.ScrField = Control1Name, .DbField = "Field1Name", …})
FormFlds.Add(New FormFld With {.ScrField = Control2Name, .DbField = "Field2Name", …})
…
This technique has worked well to easily loop through the selected fields and, on input, populate those fields from the database, or, on output, write those field values to the database.
With this implementation, however, the list must be built every single time the form is loaded. I’m wondering if the setup of the list can be done only once, during program initialization, before the forms are loaded.
Here’s the latest that I have tried.
In Class1:
Public Class indiv
Public FormFlds As List(Of FormFld)() ' The list for the frmIndividual form
End Class
In Module1, I attempt defining the FormFlds for the eventual form frmIndividual, to be saved as Indiv.FormFlds. I’d like the form name (f) and the list owner (owner) to be defineable so I can easily change those for each form.
Dim owner As New indiv
Dim f As FrmIndividual
owner.FormFlds.Add(New FormFld With {.ScrField = f.TxtKey, .DbField = "Sort_Key", …})
In Visual Studio, the third line shows error “BC30456: 'Add' is not a member of 'List(Of FormFld)()”.
Wondering if the problem might be due to not having an actual form FrmIndividual created yet, I tried changing the second line to
Dim f As New FrmIndividual
but it didn’t change anything.
I’m using VS 2022, v17.2.1. If there’s more info you need, please let me know.
The reason that you're told there's no Add method is because arrays have no Add method and you have an array. Here:
ReadOnly FormFlds As New List(Of FormFld)()
You are using the New keyword to invoke a constructor, so the parentheses at the end are for the argument list for that method call. Here:
Public FormFlds As List(Of FormFld)()
there's no New keyword so there's no constructor, so the parentheses at the end indicate that the field is an array type. That code is functionally equivalent to this:
Public FormFlds() As List(Of FormFld)
When you get that field you're getting a reference to an array of List(Of FormFld), not just a single List(Of FormFld) object. Of course, the field is initially Nothing anyway, so you'd have to assign something to it first to be able to use it.
By the way, the error message was already telling you what the issue was:
'Add' is not a member of 'List(Of FormFld)()`
It is telling you that Add is not a member of array of List(Of FormFld), which it's obviously not.

Collection getting cleared when clearing the Source collection

I have a collection, "entity.DestinationDetailscollection". I then copied this collection to an another collection, "DestinationCollection" as shown in the below code. But whenever i clear the source collection(i.e entity.DestinationDetailscollection), the collection "DestinationCollection " is also getting cleared. Please help.
Dim DestinationCollection As New Collection(Of StudentDTO)
DestinationCollection = New Collection(Of StudentDTO)(entity.DestinationDetailscollection)
entity.DestinationDetailscollection.Clear()
If you look at the documentation of Collection(Of T), you'll find that Collection(Of T) constructor does NOT copy the source collection supplied as the parameter. It just acts as a wrapper. So clearing one will clear the other.
This behavior is different from List(Of T) constructor, which actually copies the source collection elements (just object references are copied) into the new List object and thus creates a separate list.
Though you could always copy the entire list manually to the new collection object (a simple loop), you should prefere List(Of T) over Collection(Of T).
As a side note, you're instantiating your Collection twice. You should dod it in one line:
Dim DestinationCollection As New Collection(Of StudentDTO)(entity.DestinationDetailscollection)
You should replace your first two lines with this one line.

How to prevent VBA variables from being shared across Word documents?

I have a VBA template project that runs automatically when a Word document is opened. However, if I open multiple documents, they all share the variables values. How can declare these variables to be only associated with the active window or active document?
I tried declaring them in a Class Module, but that did not help. Switching between opened document I can see that these variables are shared.
Any input is appreciated...
This what I have in my Module:
Option Private Module
Dim CurrentCommand As String
Public Function SetCurrentCommand(command)
CurrentCommand = command
End Function
Public Function GetCurrentCommand()
GetCurrentCommand = CurrentCommand
End Function
More Info: The code/Macro start at AutoExec like this:
Public Sub Main()
Set oAppClass.oApp = Word.Application
If PollingRate <> "" Then Application.OnTime Now + TimeValue(PollingRate), "CaptureUserViewState"
End Sub
And the CaptureUserViewState is a Sub that resides in a different Module and does all teh checks (comparing new values to last recorded ones) and here how this Sub does the check:
If WL_GetterAndSetter.GetLastPageVerticalPercentage <> pageVerticalPercentScrolled Then
'Update the last value variable
WL_GetterAndSetter.SetLastPageVerticalPercentage (pageVerticalPercentScrolled)
'log change
End If
You don't give us much information, but I assume you declared public variables at module level like this:
Public myString As String
Public myDouble As Double
From VBA documentation:
Variables declared using the Public statement are available to all procedures in all modules in all applications unless Option Private Module is in effect; in which case, the variables are public only within the project in which they reside.
The answer is to use Option Private Module.
When used in host applications that allow references across multiple projects, Option Private Module prevents a module’s contents from being referenced outside its project.
[...] If used, the Option Private statement must appear at module level, before any procedures.
EDIT You have now clarified that you declare your variables using Dim at module level. In this case, Option Private Module is irrelevant.
Variables declared with Dim at the module level are available to all procedures within the module.
i.e. regardless of whether you're using Option Private Module or not.
If you're finding that the values are retained between runs, then that must be because you are running a procedure from the same module from the same workbook. You may think you're doing something else, but in reality this is what you're doing.
EDIT
In your class module, instead of Dim CurrentCommand As String try Private CurrentCommand As String. Without more information it's hard to debug your program. I'm just taking random potshots here.
What you need to do is store multiple versions of the variables, one set per document.
So I would suggest that you create a simple class to hold the different values.
You then store them in a collection mapping the data-set with the document name or similar as the key.
In classmodule (MyData), marked as public:
Public data1 as String
Public data2 as Integer
In module with the event-handlers:
Dim c as new Collection 'module global declaration
Sub AddData()
Dim d as new MyData 'Your data set
d.data1 = "Some value"
d.data2 = 42
c.add Value:=d, Key:=ActiveDocument.name
End Sub
Then when you enter the event-handler you retrieve the data and use the specific set for the currently active document.
Sub EventHandler()
Dim d as MyData
set d = c.item(ActiveDocument.name)
'use data
'd.data1...
End Sub
Please not that this code is just on conceptual level. It is not working, You have to apply it to your problem but it should give you some idea on what you need to do. You will need to add alot of error handling, checking if the item is already in the collection and so on, but I hope you understand the concept to continue trying on your own.
The reason for this is because, as I understand the situation from your question, you only have one version of your script running, but multiple documents. Hence the script have to know about all the different documents.
On the other hand, If each document would have their own code/eventhandlers, hence having multiple versions of the script running, then you don't need the solution provided above. Instead you need to be careful what document instance you reference in your script. By always using "ThisDocument" instead of "ActiveDocument" you could achieve isolation if the code is placed in each open document.
However, as I understood it, you only have one version of the script running, separate from the open documents, hence the first solution applies.
Best of luck!
You might want to store the Document Specific details using
The Document.CustomDocumentProperties Property
http://msdn.microsoft.com/en-us/library/office/aa212718(v=office.11).aspx
This returns a
DocumentProperties Collection
Which you can add new Properties to Using
Document.CustomDocumentProperties.Add(PropertyName, LinkToContent, Value, Type)
And then Read From using
Document.CustomDocumentProperties.Item(PropertyName)
A downside, or bonus, here is that the properties will remain stored in the document unless you delete them.
This may be a good thing or a bad thing

Adding items to list results in duplicates. What is a better way?

I have this code to return a list of fund sources for our organization.
Dim FundSourceList As New List(Of FundSource)
Dim fs As New FundSource
If results.Count > 0 Then
For Each result In results
fs.FundID = result.Item("strFundID")
fs.FundDescription = result.Item("txtFundIDDescr")
fs.ShortFundDescription = result.Item("txtFundIDDescrShort")
FundSourceList.Add(fs)
Next
End If
Return FundSourceList
The problem is that when I loop through the resulting FundSourceList all it shows is the last value. For example, if I have three fund sources (state, federal, athletic), then when I use this code to loop through all I get listed is athletic, athletic, athletic.
For Each FundSource In FundSources
Debug.Print(FundSource.FundDescription)
Next
So I change the code to this. I moved the creation of the fs variable inside the loop.
Dim results = From result In dsResult.Tables(0) Select result
Dim FundSourceList As New List(Of FundSource)
If results.Count > 0 Then
For Each result In results
Dim fs As New FundSource
fs.FundID = result.Item("strFundID")
fs.FundDescription = result.Item("txtFundIDDescr")
fs.ShortFundDescription = result.Item("txtFundIDDescrShort")
FundSourceList.Add(fs)
Next
End If
Return FundSourceList
This works fine but now I'm creating a new class over and over again. It seems a little inefficient to me. Can I not create the class outside the loop and use it over and over again? Thanks.
If you have 3 fund sources, you need three FundSource objects. It's as simple as that. I don't know what's inefficient about it...
How can you add 3 fund sources to your list but just create one?
You're not actually creating a class - the class is the code definition for the methods and properties. When you use the New operation, you're creating an instance of that class, which results in an object. When you have a list of objects, like FundSourceList, you want the items in it to be individual objects. So yes, the solution you have at the bottom is correct. You mention efficiency concerns - when you instantiate the object, basically all that is happening (in this case) is some memory is being allocated to store the variables (and some references for the managed memory, but you don't need to worry about that here). This is necessary and is optimized under-the-hood, so you shouldn't need to worry about that either.
You can't instantiate the object outside of the loop to achieve the result you're after.
This is because your object would be a reference type.
By instantiating outside of the loop, you would create one reference to your object.
When iterating through your results and setting the properties, you'll be using that same reference over and over.
All you're adding to the list on each iteration is the same reference, which by the end of the loop, will refer to an object containing the last values in your result set.
By creating new objects inside the loop, you create new references - each pointing to a new FundSource. Your loop now writes into a fresh object, and get your desired results.