VBA: Adding class items to a collection within class - vba

I'm trying to make a tree (kind of a composite pattern actually), but I just can't add the created items of a class to a collection of items in the parent.
Inside the class
Private pChildList As Collection
Private Sub Class_Initialize()
Set pChildList = New Collection
End Sub
Public Property Set ChildList(Value As CProduct)
pChildList.Add Value
End Property
Public Property Get ChildList() As Collection
ChildList = pChildList
End Property
The main function calling
Set Pro = New CProduct
Set Child = New CProduct
Pro.ChildList.Add Child
So the result should be a parent (Pro) with a Child in its pChildList collection, but I only get the error that "Argument is not optional".
Many thanks in advance!

You are just missing a Set in your property Get definition. A Collection is an object, you need to use the Set keyword to affect it to a variable.
Public Property Get ChildList() As Collection
Set ChildList = pChildList
End Property
To complement my answer following you comment:
Property Set are for Objects, Property Let are for base types. Those two properties are usually used to change the value of a member variable (and are expected to do that), that is to access the variable for writing, but you can do whatever you want in the code.
Property Get are usually used to return the value of a member variable (but once again, you can do whatever you want in code), that is to access the variable for reading.
Since there is no reason to change the pChildList itself, I would drop totally the Property Set.
You can also decide to completely hide the member variable and use member functions to add and remove Childs, for example:
Public Sub AddChild(vValue as CProduct)
pChildList.Add vValue
End Sub

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

Is it possible to change the appearance of a custom class's object in the VBA editor's locals and watch windows? [duplicate]

Although an experienced VBA programmer it is the first time that I make my own classes (objects). I am surprised to see that all properties are 'duplicated' in the Locals Window. A small example (break at 'End Sub'):
' Class module:
Private pName As String
Public Property Let Name(inValue As String)
pName = inValue
End Property
Public Property Get Name() As String
Name = pName
End Property
' Normal module:
Sub Test()
Dim objTest As cTest
Set objTest = New cTest
objTest.Name = "John Doe"
End Sub
Why are both Name and pName shown in the Locals Window? Can I in some way get rid of pName?
As comments & answers already said, that's just the VBE being helpful.
However if you find it noisy to have the private fields and public members listed in the locals toolwindow, there's a way to nicely clean it up - here I put the Test procedure inside ThisWorkbook, and left the class named Class1:
So what's going on here? What's this?
Here's Class1:
Option Explicit
Private Type TClass1
Name As String
'...other members...
End Type
Private this As TClass1
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
The class only has 1 private field, a user-defined type value named this, which holds all the encapsulated data members.
As a result, the properties' underlying fields are effectively hidden, or rather, they're all regrouped under this, so you won't see the underlying field values unless you want to see them:
And as an additional benefit, you don't need any pseudo-Hungarian prefixes anymore, the properties' implementations are crystal-clear, and best of all the properties have the exact same identifier name as their backing field.
All the Inspection windows not only show the public interface of the objects to you, but also their private members. AFAIK there is nothing you can do about it.
Consider it a nice feature to get even more insights while debugging.
In my experience this is less of an issue in real world objects as they tend to have more fields and properties. Assuming a consistent naming (as your example shows), fields and properties are nicely grouped together.
If you really dont want to see even Mathieu's This you could wrap it into a function. This is a bit more involved, and can be achieved using
a second class that stores the data in public variables. This will be marginally slower then Mattieu's implementation
a collection object that accesses the data using keys. This does not require additional clutter in the project exporer's 'class module' list but will be a little slower if you call the This repeatedly in fast sucession
An example for each is given below. If you break in the Class's Initialisation function, you can add me to the watch window and only the Name property will be listed
Using 2 Objects example
insert a classmodule and name it: InvisibleObjData
Option Explicit
Public Name As String
Public plop
Private Sub Class_Initialize()
Name = "new"
plop = 0
End Sub
insert a classmodule and name it: InvisibleObj
Option Explicit
Private Function This() As InvisibleObjData
Static p As New InvisibleObjData 'static ensures the data object persists at successive calls
Set This = p
End Function
Private Sub Class_Initialize()
This.Name = "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
This.plop = 111: Debug.Print This.plop
End Sub
Property Let Name(aname As String): This.Name = aname: End Property
Property Get Name() As String: Name = This.Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleObj
If you dont like splitting the class over two objects, a similar behaviour can be generated using a 'wrapped' collection object:
insert a classmodule and name it: InvisibleCol
Option Explicit
Private Function This() As Collection
Static p As New Collection
'static ensures the collection object persists at successive calls
'different instances will have different collections
'note a better dictionary object may help
Set This = p
End Function
Private Function add2this(s, v)
'a better dictionary object instead of the collection would help...
On Error Resume Next
This.Remove s
This.Add v, s
End Function
Private Sub Class_Initialize()
add2this "name", "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
add2this "plop", 111
Debug.Print This("plop") ' use the key to access your data
Debug.Print This!plop * 2 ' use of the BANG operator to reduce the number of dbl quotes
' Note: This!plop is the same as This("plop")
End Sub
Property Let Name(aname As String): add2this "name", aname: End Property
Property Get Name() As String: Name = This!Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleCol

Looping through dictionary of objects in 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!)

VBA global class variable

My obstacle is trying to get multiple subs to recognize class variables. When I try to declare them globally, I get a compile error: "Invalid outside procedure". Then, when I run a public function or sub to declare the variables, they remain undefined in the other subs. I want multiple subs to recognize the variables because their values are supposed to be altered via UserForm, and then utilized in a different sub.
If it could work in this manner, great, but I understand that my design could fundamentally be flawed. Please advise!
This is my Class definition, inserted as a Class module named "cRSM":
Option Explicit
Private pName As String
Private pDesiredGrowth As Double
'Name of RSM
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
'Growth property
Public Property Get DesiredGrowth() As Double
DesiredGrowth = pDesiredGrowth
End Property
Public Property Let DesiredGrowth(Value As Double)
If Value > 0 And Value < 1 Then
pDesiredGrowth = Value
End If
End Property
This is invalid procedure error (which I put in the Global Declarations section):
'Bedoya
Dim Bedoya As cRSM
Set Bedoya = New cRSM
Bedoya.Name = "Bedoya"
And this is the "variable not defined error" (within a private sub):
Private Sub Add_Click()
**Bedoya.DesiredGrowth** = Txt2.Value
Thank you for your time
In a standard module (I name mine MGlobals), put
Public Bedoya As cRSM
Then in another standard module (I name mine MOpenClose), put
Sub Initialize()
If Not Bedoya Is Nothing Then
Set Bedoya = New cRSM
End If
End Sub
Any default properties you want set should be set in the Class_Initialize procedure. In any procedure that you want to use Bedoya, use
Initialize
and it will instantiate the global variable if necessary. The only difference between this and the New keyword is that you can't accidentally instantiate the variable with this method. You either call Initialize or you don't. A lot of VBA developers use New, but almost never do for that reason.
If I understood well You want a global object.
You can put the declaration in module like
public Bedoya As cRSM
then you have create the object ... you can use a global event inside the Workbook like
Private Sub Workbook_Open()
Set Bedoya = New cRSM
Bedoya.initialize("Bedoya") 'a method to initialize private variables
End Sub
Now you can use the global object. You have to restart the excel file or run this method manually.
Is not good style to use global variables, but sometimes is the more easy to do :P
What you want to do nowadays is done using singleton Software Pattern, but this is for other day hehehe

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.