Looping through dictionary of objects in vba - vba

I'm writing code to instantiate a form that shows one record in each instance. I have functions to open and close instances using a dictionary but now I need to check whether a record is already opened.
Dictionaries and collections only allow you to store pairs of data (key/item) so created a class with two properties: form object and the opened record id. I store key and this object in a dictionary.
Now I want to check if a record id is already opened so I have to loop trough the dictionary checking the record id (servicioid in code below) property of the item.
Class module:
Private propFormulario As Form
Private propServicioId As Long
Public Property Let FormObj(frmFormObj As Form)
Set propFormulario = frmFormObj
End Property
Public Property Get FormObj() As Form
Set FormObj = propFormulario
End Property
Public Property Let servicioid(lngServicioId As Long)
propServicioId = lngServicioId
End Property
Public Property Get servicioid() As Long
servicioid = propServicioId
End Property
Open and close instances:
Public dicFormServicios As New Dictionary
Public Sub AbrirServicio(lngServicioId As Long)
Dim ServicioAbierto As clsServiciosAbiertos
Set ServicioAbierto = New clsServiciosAbiertos
ServicioAbierto.FormObj = New Form_servicios2
ServicioAbierto.servicioid = lngServicioId
dicFormServicios.Add CStr(ServicioAbierto.FormObj.hwnd), ServicioAbierto
ServicioAbierto.FormObj.visible = True
End Sub
Public Sub CerrarServicio(InstanciaHwnd As Long)
If dicFormServicios.Exists(CStr(InstanciaHwnd)) Then
dicFormServicios.Remove CStr(InstanciaHwnd)
End If
End Sub
My question is how do I loop trough the dictionary and how do I check an ID is in the servicioid property of any item.

My VBA is a bit rusty, so you're going to want to do something along the lines of...
dicFormServicios.Add myForm.FormId, myForm
Then to recover a value try...
myReturnForm = dicFormServicios.Item("SomeFormName")
Details here...
Dictionary Object
Whether a value exists
Recover an item from the dictionary
(The Dictionary reference above is very useful, but really...) All of the objects that you need are already there. You can reference an object directly from the Forms collection using either an index number...
Set myForm = Forms![0]
...or by using the form's name...
Set myForm = Forms!["myFormName"]
(Such a long time since I've done any of this stuff!)

Related

Create a class modules with parent and child classes in VBA

I am a novice user of class module.
I don't understand the concept of a class module well.
I want to configure a class module similar to the basic objects of Excel like Worksheet or Cells or ETC..
So I want to control it by creating a parent object and creating its child objects.
Child Class - Defect
Option Explicit
Private pDefectSymptom As String
Private pDefectLevel As Integer
Property Get DefectSymptom() As String
DefectSymptom = pDefectSymptom
End Property
Property Let DefectSymptom(ByVal vDefectSymptom As String)
pDefectSymptom = vDefectSymptom
End Property
Property Get DefectLevel() As Integer
DefectLevel = pDefectLevel
End Property
Property Let DefectLevel(ByVal vDefectLevel As Integer)
pDefectLevel = vDefectLevel
End Property
Function Delete()
'???
End Function
Property Get Parent() As Object
'???
End Property
Parent Class - Defects
Private Defects As New Collection
Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Defects.Add NewDefect
'Add = NewDefect 'Error! Like the open command of workbook, I want to return an object or just command
End Function
Property Get Count() As Long
Count = Defects.Count
End Property
Property Get Item(Index As Long) As Defect
Item = Defects(Index) 'Error! I don't know what raise Error.
End Property
My question are.
How to add Command like Open Command of workbook. Return or just command.
Why Raise Error Item Property? how to fix that?
Hiding Private variable. Because office office objects seem to be hidden.
enter image description here
If you have time, please help with Delete command and Parent command.
How to add Command like Open Command of workbook. Return or just
command.
You just need to return the newly created object in the function. Keep in mind since we're dealing with objects, we need to Set the object's reference.
Public Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Defects.Add NewDefect
Set Add = NewDefect '<- here
End Function
Why Raise Error Item Property? how to fix that?
Same as the above, you need to Set the object's reference.
Property Get Item(Index As Long) As Defect
Set Item = Defects(Index)
End Property
To delete, simply supply the index to the function. However, this method must reside where the collection is (parent) since a Defect object cannot delete itself.
Function Delete(ByVal Index As Long)
Defects.Remove Index
End Function
Lastly, to hold a reference to the parent, each child must hold a reference to it in a private variable. Then you need to set the parent when creating a new item using the keyword Me.
So in the Defect class, create a private field.
Private mParent As Defects
Property Set Parent(ByVal objDefects As Defects)
Set mParent = objDefects
End Property
Property Get Parent() As Defects
Set Parent = mParent
End Property
With this done, amend the Add() method to store the reference.
Public Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Set NewDefect.Parent = Me '<- here
Defects.Add NewDefect
Set Add = NewDefect '<- here
End Function
Not sure this is a good idea though. I tend to avoid circular references altogether, since a child can hold the parent in memory by holding a reference to it. You will need to make sure to clear the reference to the Parent when deleting the item.
Lastly, you should avoid creating the Defects collection like this. Instead, you should make use of the class constructor and destructor.
This method is called automatically when a new class is created:
Private Sub Class_Initialize()
Set Defects = New VBA.Collection
End Sub
This method is called just before the class is destroyed from memory.
Private Sub Class_Terminate()
Set Defects = Nothing
End Sub

Getting a collection property of a class take a property of another class of another type?

I wanted to first thank you all for the help you've given me implicitly over the last few months! I've gone from not knowing how to access the VBA IDE in Excel to writing fully integrated analysis programs for work. I couldn't have done it without the community here.
I'm currently trying to overhaul the first iteration of a data analysis program I wrote while learning how to code in VBA. While purpose driven and only really legible to myself, the code worked; but was a mess. From folks on this site I picked up Martin's Clean Code and gave it a read on how to try and be a better programmer.
From Martin's Clean Code, it was impressed on me to prioritize abstraction and decoupling of my code to allow for higher degrees of maintenance and modularization. I found this out the hard way since very minor changes requested above my pay grade would require massive and confusing rewrites! I'm trying to eliminate that problem going forward.
I am attempting to rewrite my code in terms of single responsibility classes (at least, where it is possible) and I am a bit confused. I apologize if my question isn't clear or if I'm using the wrong terminology. I want to be able to generate a collection of specific strings (the names of our detectors to be specific) with no duplicates from raw instrument data files from my lab. The purpose of this function is to assemble a bunch of metadata in a class and use it to standardize our file system and prevent clerical errors from newbies and old hands when they use the analysis program.
The testing initialization sub is below. It pops open a userform asking for the user to select the filepaths of the three files in the rawdatafiles class; then it kills the userform to free memory. The metadata object is currently for testing and will be rewritten properly when I get the output I want:
Sub setup()
GrabFiles.Show
Set rawdatafiles = New cRawDataFiles
rawdatafiles.labjobFile = GrabFiles.tboxLabJobFile.value
rawdatafiles.rawdatafirstcount = GrabFiles.tboxOriginal.value
rawdatafiles.rawdatasecondcount = GrabFiles.tboxRecount.value
Set GrabFiles = Nothing
Dim temp As cMetaData
Set temp = New cMetaData
temp.labjobName = rawdatafiles.labjobFile
'this works fine!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
End Sub
The cMetadata class I have currently is as follows:
Private pLabjobName As String
Private pDetectorsOriginal As Collection
Private pDetectorsRecheck As Collection
Private Sub class_initialize()
Set pDetectorsOriginal = New Collection
Set pDetectorsRecheck = New Collection
End Sub
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
When I step through the code it starts reading the "public property get rawdatafirstcount() as string" and throws the error after "End Property" and points back to the "temp.detectorsOriginal = rawdatafiles.rawdatafirstcount" line in the initialization sub.
I think I'm at least close because the temp.labjobName = rawdatafiles.labjobFile code executes properly. I've tried playing around with the data types since this is a collection being assigned by a string but I unsurprisingly get data type errors and can't seem to figure out how to proceed.
If everything worked the way I want it to, the following function would take the filepath string from the rawdatafiles.rawdatafirstcount property and return for me a collection containing detector names as strings with no duplicates (I don't know if this function works exactly the way I want since I haven't been able to get the filepath I want to parse properly in the initial sub; but I can deal that later!):
Function getDetectors(filePath As String) As Collection
Dim i As Integer
Dim detectorsCollection As Collection
Dim OriginalRawData As Workbook
Set OriginalRawData = Workbooks.Open(fileName:=filePath, ReadOnly:=True)
Set detectorsCollection = New Collection
For i = 1 To OriginalRawData.Worksheets(1).Range("D" & Rows.Count).End(xlUp).Row
detectorsCollection.Add OriginalRawData.Worksheets(1).Cells(i, 4).value, CStr(OriginalRawData.Worksheets(1).Cells(i, 4).value)
On Error GoTo 0
Next i
getDetectors = detectorsCollection
Set detectorsCollection = Nothing
Set OriginalRawData = Nothing
End Function
Thanks again for reading and any help you can offer!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
It throws an error because, as others have already stated, the Set keyword is missing.
Now with that out of the way, a Set keyword is NOT what you want here. In fact, sticking a Set keyword in front of that assignment will only buy you another error.
Let's look at this property you're invoking:
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
You're trying to assign detectorsOriginal with what appears to be some String value that lives in some TextBox control on that form you're showing - but the property's type is Collection, which is an object type - and that's not a String!
Now look at the property that does work:
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
This one is a String property, with a Property Let mutator that uses the fileName parameter it's given.
The broken one:
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Is a Set mutator, takes a Collection parameter, and doesn't use the originalFilepath parameter it's given at all!
And this is where I'm confused about your intention: you're passing what has all the looks of a String except for its type (Collection) - the calling code wants to give it a String.
In other words the calling code is expecting this:
Public Property Let detectorsOriginal(ByVal originalFilepath As String)
See, I don't know what you meant to be doing here; it appears you're missing some pOriginalFilepath As String private field, and then detectorsOriginal would be some get-only property that returns some collection:
Private pOriginalFilePath As String
Public Property Get OriginalFilePath() As String
OriginalFilePath = pOriginalFilePath
End Property
Public Property Let OriginalFilePath(ByVal value As String)
pOriginalFilePath = value
End Property
I don't know what you're trying to achieve, but I can tell you this:
Don't make a Property Set member that ignores its parameter, it's terribly confusing code.
Don't make a Property (Get/Let/Set) member that does anything non-trivial. If it's not trivially simple and has a greater-than-zero chance of throwing an error, it probably shouldn't be a property. Make it a method (Sub, or Function if it needs to return a value) instead.
A word about this:
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
Whenever you Dim something As New, VBA will automatically instantiate the object whenever it's referred to. In other words, this wouldn't throw any errors:
Dim FSO As New FileSystemObject
Set FSO = Nothing
pLabjobName = FSO.GetBaseName(fileName)
Avoid As New if you can. In this case you don't even need a local variable - use a With block instead:
With New FileSystemObject
pLabjobName = .GetBaseName(fileName)
End With
May not be your issue but you're missing Set in your detectorsOriginal Set/Get methods:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
So the error is one I've made a time or two (or more). Whenever you assign an object to another object, you have to use the Set reserved word to assign the reference to the Object.
In your code do the following:
In Sub setup()
Set temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
And in the cMetadata class change the Public Property Set detectorsOriginal(originalFilepath As Collection) property to the following:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Also in your function Function getDetectors(filePath as String) as Collection change the statement afterNext i` to
Set getDetectors = detectorsCollection
Also, I'm very glad to hear that you've learned how to use VBA.
When you're ready to create your own Custom Collections, check out this post. Your own custom Collections.
I also book marked Paul Kelly's Excel Macro Mastery VBA Class Modules – The Ultimate Guide as well as his Excel VBA Dictionary – A Complete Guide.
If you haven't been to Chip Pearson's site you should do so. He has a ton of useful code that will help your delivery your projects more quickly.
Happy Coding.

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.

Passing Array to Another Form (vb.net)

I got a form which declared an array in public,
Public requestedqueue() As Integer
I would like to pass the array to form3 and perform other calculations there, but how?
I tried doing(at a new form):
public newrequest() As Integer
newrequest = form2.requestedqueue
I tried to show it at a new form by doing:
TextBox1.Text = = String.Join(",",form2.newrequest)
But whenever I run into form3 it would say the newrequest is null.
But it shows as an array in form2, Im so confused.
I'm not sure what you mean by
But it shows as an array in form2
but newrequest will be Nothing because you have set it equal to the value of requestedqueue which is Nothing until you populate the array with some values.
If you had Public requestedqueue() As Integer = {1, 2, 3} then you would not encounter the error.
One way to pass data to a form is to add a property to the second form.
Lets say you have a form called Form3 and this is your form's code. In the code you will need to declare an array of integer to hold the passed data, and also declare a public property so you have a way of passing the array
Public Class Form3
Dim requestedqueue() As Integer
Public Property ArrayParameter As Integer()
Get
Return arrayData
End Get
Set(value() As Integer)
arrayData = value
End Set
End Property
End Class
Then, to pass the data from Form1, in form1, you would simply use
Form3.ArrayParameter=requestedqueue()
to set the parameter.
and if you wish, you can show the form as normal, or if the form is already visible, you can process the code using button clicks etc.
If you want to process the data in an already open form immediately without any user interaction, you can write a Procedure that does the processing and include that in the Set portion of your property.
For example. If you want to add all the elements of the array to a ListBox called ListBox1 in Form3, you could write a procedure like this..
Private Sub AddDataToListbox()
ListBox1.Items.Clear()
For Each item As Integer In requestedqueue
ListBox1.Items.Add(item)
Next
End Sub
and change your Form3.ArrayParameter code to this
Public Property ArrayParameter As Integer()
Get
Return requestedqueue
End Get
Set(value() As Integer)
requestedqueue = value
AddDataToListbox()
End Set
End Property

Linking Object to UI

What is the best method to use to link a class to a user form?
Sorry to be hypothetical, but using actual code will lead to a question that is pages long.
Let's say I have a class that holds data about a person.
<<class Person>>
Public FirstName as String
Public LastName as String
Public PhoneNumber as String
<<end class>>
I put that data into a VBA UserForm listview.
Now, let's say I want to change the phone number to 555-555-1234 if the user clicks on that record in the listview.
I can read the interaction with the listview with the item click event.
Private Sub lvExample_ItemClick(ByVal Item As MSComctlLib.ListItem)
' Change the phone number
End Sub
What is the best way to get from Item in the above code to my actual object? Should I add an GUID to each object and put that in the tag of the listitem, then look it up? Should I add the listitem from the listview into the class so I can loop through all my people and then see if the Item from _ItemClick equals the Item from the object?
The easiest way is to use either the Index property, if you don't have any unique identifier, or the Key property if you do, of the ListItem.
If you choose to use the Index property, then you can't (or at least it will greatly complicate it) add any functionality to rearrange the order of list items.
You would have populated the ListView based on the objects in a collection/recordset via the ListView.ListItems.Add method. You can use the Index property to get back to that original object based on the order of items in ListItems corresponding to the order of items in your original collection of objects.
Alternatively, if you prefer the greater flexibility of using a unique key but don't wish to modify the underlying object, then you can trivially construct a unique key (the simplest being CStr(some incrementing number)) as you add each object to ListItems, storing the keys alongside the objects.
You can then use the .Key property of the ListItem. The benefit here is the user can be allowed to modify what items are in, delete stuff etc without you having to invalidate your control and re-add all objects in order to keep the linkage between Index in source and index in the list.
E.g.:
Private persons As Collection
Private Sub lvExample_ItemClick(ByVal Item As MSComctlLib.ListItem)
' Change the phone number:
'Method 1, using the index of listitem within listitems
'to tie back to index in persons collection
'Don't allow rearranging/sorting of items with this method for simplicity
With persons.Item(Item.Index)
.PhoneNumber = "555-555-1234"
'...some stuff
End With
'Method 2, using a unique key
'that you generate, as the underlying person object doesn't have a necessarily unique one
'Items would have been added in a method similar to AddItemsExample1() below
With persons.Item(Item.Key)
.PhoneNumber = "555-555-1234"
'...some stuff
End With
End Sub
Sub AddItemsExample1()
'Storage requirements vs. your existing recordset or whatever
'are minimal as all it is storing is the reference to the underlying
'person object
'Adapt per how you have your existing objects
'But basically get them into a keyed collection/dictionary
Dim i As Long
Set persons = New Collection
For Each p In MyPersonsSource
persons.Add p, CStr(i)
i = i + 1
Next p
'By keying them up, you can allow sorting / rearranging
'of the list items as you won't be working off of Index position
End Sub
Finally, another way if you have them in a recordset returned by a DB is to add a new field (I imagine you have an existing object field) and do run an UPDATE query on your records populating it with an incrementing number (this should only effect the local recordset (check the recordset settings first of course!)). Use this as the key.
You mention in a comment to your question that you get the data from SQL. For all normal purposes with a list box it is still probably easiest to just run them through a Collection object as detailed above, but if you have e.g. 4 fields from SQL for a record in a recordset then you don't hav an 'object' in the sense of being able to call properties on it. Please specify in your question or in a comment to this if you do so, as there may be a better treatment to answer your question or the actual update operation will likely require different syntax or semantics (particularly if you need to propagate any update back to the source) :)
I would use a event driven approach.
Person will have properties instead of variables and assigning those properties will raise a event.
User form will have WithEvents Person, so that changes in related person will trigger user form code.
Person class:
Public Event Changed()
Private pFirstName As String
Private pLastName As String
Private pPhoneNumber As String
Public Property Get FirstName() As String
FirstName = pFirstName
End Property
Public Property Let FirstName(ByVal v As String)
pFirstName = v
RaiseEvent Changed
End Property
Public Property Get LastName() As String
LastName = pLastName
End Property
Public Property Let LastName(ByVal v As String)
pLastName = v
RaiseEvent Changed
End Property
Public Property Get PhoneNumber() As String
PhoneNumber = pPhoneNumber
End Property
Public Property Let PhoneNumber(ByVal v As String)
pPhoneNumber = v
RaiseEvent Changed
End Property
Event catching in user form:
Public WithEvents ThisPerson As Person
Private Sub ThisPerson_Changed()
'Update user form
End Sub
So whenever you assign YourForm.RelatedObject = SomePerson, any changes done to SomePerson will trigger code in YourForm.
Since you can't change returned ListItem, I have another solution:
Add a related ListItem to your Person class:
Public FirstName as String
Public LastName as String
Public PhoneNumber as String
Public RelatedObject As Object
When populating TreeView, assign it:
Set SomeListItem = SomeTreeView.ListItems.Add(...)
Set SomePerson.RelatedObject = SomeListItems
So whenever you have a change in a ListItem:
Private Sub SomeListView_ItemClick(ByVal Item As MSComctlLib.ListItem)
Dim p As Person
For Each p In (...)
If p.RelatedObject Is Item Then
'Found, p is your person!
p.PhoneNumber = ...
End If
Next
End Sub
Alternatively, if you don't want to change your Person class, you can encapsulate it in another class, eg, PersonToObject:
Public Person As Person
Public RelatedObject As Object
And use PersonToObject as link objects.