Need some advice on a simple Class (1 Viewer)

KitaYama

Well-known member
Local time
Tomorrow, 08:34
Joined
Jan 6, 2022
Messages
1,877
Pretty new to classes.
Here's a simple class I have.

SQL:
Private WithEvents m_Label As Label
Private LabelCollection As Collection
'************************************************************************'
Public Sub init(frm As Access.Form)
    Dim Ctrl As Control
    Dim srtCtrl As clsMyClass
    Set LabelCollection = New Collection
    For Each Ctrl In frm.Section(acHeader).Controls
        Select Case Ctrl.ControlType
            Case acLabel
                Set srtCtrl = New clsMyClass
                Set srtCtrl.Ctrl = Ctrl
                LabelCollection.Add srtCtrl
        End Select
    Next
End Sub
'************************************************************************'
Public Property Set Ctrl(ctl As Control)
    Set m_Label = ctl
    m_Label.OnClick = "[Event Procedure]"
End Property
'************************************************************************'
Private Sub m_Label_Click()
    testFunction m_Label
End Sub
'************************************************************************'
Private Function testFunction(lbl As Access.Label)
    MsgBox lbl.Name
End Function
'************************************************************************'

Above, I've created a collection of all labels in the header of the form, and have set a click event for each of them.
Works fine.

My Question:
How can I save the last clicked label name and show it in a msgbox?
When the form opens, the first click on any label should show a blank msgbox.
From there, if I click any label, I want the msgbox tell me what was the previous label that was clicked.

To gain this, I added the following :
SQL:
Private m_LastClickedLabel As String

Private Property Let LastClickedLabel(ByVal NewValue As String)
    m_LastClickedLabel = NewValue
End Property
Private Property Get LastClickedLabel() As String
    LastClickedLabel = m_LastClickedLabel
End Property

and changed the onClick function to :
SQL:
Private Function testFunction(lbl As Access.Label)
    MsgBox LastClickedLabel
    LastClickedLabel = lbl.Name
End Function

Even though I set the clicked label name in LastClickedLabel property, but the msgbox doesn't show the previously clicked label.

Any kind of advice is much appreciated.
A database that shows what I have is attached.
Thanks again.
 

Attachments

Last edited:
Forgot to say:
I can add the two last Let/Get properties to the form, and read it from there. This solution works.
But for now, If possible, I don't want to go that rout.

Thanks.
 
Last edited:
see this demo but it is not simple as you can see.
open Form1 and click on the labels.
view the 3 classes on the class module.
 

Attachments

see this demo but it is not simple as you can see.
open Form1 and click on the labels.
view the 3 classes on the class module.
Million thanks. But 3 classes for a simple task like this is too much.
I think I'll stick with this method. Thanks for your time and help.
 

Attachments

on second thought, having a class is also an overkill. see form frm_agp
 

Attachments

on second thought, having a class is also an overkill. see form frm_agp
My final goal is much more complicated than that.
This help request was just a mock up class to show my problem.
I couldn't (and still can't) understand why when I use a collection to set a class to several control, the property of the class is reset and returns empty.
 
My final goal is much more complicated than that.
This help request was just a mock up class to show my problem.
I couldn't (and still can't) understand why when I use a collection to set a class to several control, the property of the class is reset and returns empty.
I don't do classes much, but here is my guess at what may be happening.
  1. You have a class and added a variable to hold the name of the "previous" control
  2. This variable is initially empty in the class
  3. When you loop through the controls and hit the first label, you instantiate this class which carries that empty variable
  4. When you hit the next label, you again instantiate a separate copy of the class with the empty variable
  5. When you click on any of the labels, it activates the class associated with that label and assign the name of the label you clicked
  6. When you click on the other label, it also activates its own copy of the class where the variable is still empty and assign the name of the label on that class
  7. When you click on the first label again, my guess is it will now show the name you previously assigned to it when you first clicked it, which, unfortunately, happens to be the name of the label you just clicked
So, my guess is that it's not that the variable is "reset"; but rather, the variables may not be reachable across multiple instances of the same class.

Again, that's just my guess...

PS. Of course, if you knew the index of the class instance you want to invoke from the collection, you can return the values from its variables, but then the trick would be to still store that information on the form. Just thinking out loud...
 
but rather, the variables may not be reachable across multiple instances of the same class.
That was exactly what I had imagined. My loop creates an instance of the class for each label and adds it to the collection.
I was looking for a way to access or write a property for each instance, or find a way to make each instance being able to communicate with each other.
 
That was exactly what I had imagined. My loop creates an instance of the class for each label and adds it to the collection.
I was looking for a way to access or write a property for each instance, or find a way to make each instance being able to communicate with each other.
I think it's possible for each instance to communicate with each other if you knew the index in the collection of the instance you want to communicate to. Does that make sense?
 
But 3 classes for a simple task like this is too much.
If you create the separate class:- clsSharedState containing:-

Code:
Option Compare Database
Option Explicit

Private m_LastClickedLabel As String

' Property to get the last clicked label
Public Property Get LastClickedLabel() As String
    LastClickedLabel = m_LastClickedLabel
End Property

' Property to set the last clicked label
Public Property Let LastClickedLabel(ByVal NewValue As String)
    m_LastClickedLabel = NewValue
End Property

Then it works okay but I'm not sure if you want an extra class?
 
In order to have be able to capture all events but only instantiate a single variable. I do it this way.
1. Create your custom class
2. Create your custom collection class to manage your objects
3. Make the object raise an event in the custom collection
4. Trap the single event.

The form instantiates an instant of the custom collection class and add the objects to it.
Code:
Private WithEvents LA As LabelArray
'------------------------------------------------------- Load Control Array ------------------------------------------------------------------
Private Sub Form_Load()
  Dim ctl As Access.Control
  Set LA = New LabelArray
  For Each ctl In Me.Controls
    If ctl.Tag = "LA" Then
      LA.Add ctl
    End If
  Next ctl
End Sub
'-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'---------------------------------------------------------------------------- Events ---------------------------------------------------------------------------------
Private Sub LA_Click(TheLabel As Label)
 Dim strOut As String
 strOut = "Clicked: " & TheLabel.Name
 If Not LA.PreviousClicked Is Nothing Then
   strOut = strOut & " Previous label clicked: " & LA.PreviousClicked.Name
 Else
   strOut = strOut & " no previous clicked"
 End If
 MsgBox strOut
End Sub

Custom collection class which raises the single event

Code:
Option Compare Database
Option Explicit

Private m_LabelArray As Collection
Private M_PreviousClicked As Label

Public Event Click(TheLabel As Access.Label)


Public Function Add(TheLabel As Access.Label) As LabelArrayItem
  'create a new Pet and add to collection
  Dim NewLabelArrayItem As New LabelArrayItem
  NewLabelArrayItem.Initialize TheLabel, Me
  m_LabelArray.Add NewLabelArrayItem, TheLabel.Name
  Set Add = NewLabelArrayItem
End Function

Public Sub Add_LabelArrayItem(TheLabelArrayItem As LabelArrayItem)
  'I also add a second Add to allow you to build the object and then assign it
   m_LabelArray.Add TheLabelArrayItem, TheLabelArrayItem.Label.Name
End Sub

Public Property Get Count() As Long
  Count = m_LabelArray.Count
End Property


Public Property Get Item(Name_Or_Index As Variant) As LabelArrayItem
  Set Item = m_LabelArray.Item(Name_Or_Index)
End Property

Sub Remove(Name_Or_Index As Variant)
  'remove this item from collection
  'The name is the key of the collection
  m_LabelArray.Remove Name_Or_Index
End Sub

Public Property Get ToString() As String
  Dim strOut As String
  Dim i As Integer
  For i = 1 To Me.Count
    strOut = strOut & Me.Item(i).Label.Name & vbCrLf
  Next i
  ToString = strOut
End Property
'-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'---------------------------------------------------------------------------- Class Methods to Raise Common Events ---------------------------------------------------------------------------------

Public Sub LA_Click(TheLabel As Label)
 RaiseEvent Click(TheLabel)
End Sub
'----------------------------------------------- All Classes Have 2 Events Initialize and Terminate --------
Private Sub Class_Initialize()
'Happens when the class is instantiated not related to the fake Initialize method
'Do things here that you want to run on opening
  Set m_LabelArray = New Collection
End Sub

Private Sub Class_Terminate()
  'Should set the object class properties to nothing
  Set m_LabelArray = Nothing
End Sub
'-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'---------------------------------------------------------------------------- Previous Clicked ---------------------------------------------------------------------------------

Public Property Get PreviousClicked() As Label
  Set PreviousClicked = M_PreviousClicked
End Property

Public Property Set PreviousClicked(ByVal ThePreviousClicked As Label)
  Set M_PreviousClicked = ThePreviousClicked
End Property

And the simple class
Code:
Private WithEvents m_Label As Access.Label
Private m_ParentLabelArray As LabelArray


Public Sub Initialize(TheLabel As Access.Label, ParentLabelArray As LabelArray)
 ' On Error GoTo ErrHandler
  Set Me.Label = TheLabel
  Set Me.ParentLabelArray = ParentLabelArray
  m_Label.OnClick = "[Event Procedure]"
  Exit Sub
ErrHandler:
   If Not (Err.Number = 459 Or Err.Number = 91) Then
      MsgBox ("Error: " & Err.Number _
            & " " & Err.Description _
            & " " & Err.Source)
   End If
   Resume Next
End Sub
Public Property Get Label() As Access.Label
    Set Label = m_Label
End Property
Public Property Set Label(ByVal objNewValue As Access.Label)
    Set m_Label = objNewValue
End Property

Public Property Get ParentLabelArray() As LabelArray
    Set ParentLabelArray = m_ParentLabelArray
End Property

Public Property Set ParentLabelArray(ByVal objNewValue As LabelArray)
    Set m_ParentLabelArray = objNewValue
End Property

'-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'---------------------------------------------------------------------------- Event Handler if Label ---------------------------------------------------------------------------------
Private Sub m_Label_Click()
  Me.ParentLabelArray.LA_Click Me.Label
  Set Me.ParentLabelArray.PreviousClicked = Me.Label
End Sub
 
You may say you do not want a second Custom Collection Class. I say you probably do. Almost every class that you are holding multiple instances requires a collection class to easily hold the objects. The good thing is you write it once and reuse it every time just change the name.
Now I agree that this approach violates some principles of OOP. Since a labelarrayitem is a class property of the label array class it should be able to be instantiated without a reference to its parent collection. But IMO that is a limitation of VBA.
However this is still far better encapsulating the objects than the OP's idea. This instantiates specific controls and does not tightly couple it to the form.

It would be better if the true purpose was known. I would take a look at this discussion.
MarkK's solution is probably more flexible and academically correct, but it is a lot of extra work.

But bottom line this approach allows you to trap multiple object events and then raise a single event. Now the forms simply trap a single event from the collection.
 
@MajP I really appreciate the time you spent to explain in details.
Today, I have a pile to clear at work, so am out of free time. Give me a day to test and understand what you suggested please.

By the way, is the file attached to #13 correct. A fast glance into it? I didn't see any changes.

It would be better if the true purpose was known.
The main purpose is learning.
In most of the classes I see, for example your FAYT, you have one control to work with. In your case, a textbox.
You pass it to the class and from there, you have certain options to use and manipulate your object.

I've always wondered how I can pass an unknown number of the same type of objects to a class and make them behave the same.
After reading some tutorials and watching numerous youtubes , I ended up with the method I used in #1, to loop through the objects and insatiate a class for each control.
To test what I've learned and how it actually works, I took a piece of one of my databases and tried to change it to a class.

The objective was to write a class that when any label in a continuous form is clicked, the field associated with that label is sorted, the ForeColor changes and an arrow is added. Clicking another label, would turn previous label to its default mode and sort the new field.
A right click on any label, takes the form back to the order it was opened with.

The attached db is what my test ended with. It works.
On a later attempt, I tried to find a way not to rely on form's public properties and do everything in the class level. And this was where I failed.

As I explained, it was just a matter of testing and learning. So my question above was a general question with no true purpose. So I thought a mockup generic question would be OK.

Sincere thanks again.
 

Attachments

Last edited:
Sorry that was the wrong file. Try this one. However, you could simply copy those classes and tag the labels and it would work on any form.
Strongly recommend reading this

I've always wondered how I can pass an unknown number of the same type of objects to a class and make them behave the same.
From doing this a lot, I think the design I propose is the most flexible and full proof. Again, I will agree it violates some principles of good OOP, but it is what you can do with the limitations of vba.
1. Build the class with events to a control
2. Build your custom collection class. For the most part this is just renaming it since the properties and methods are generic.
3. Add a method to the custom class that raises a custom event when called by the class items. This allows you then to have the collection raise an event triggered by the items trapping an event.
4. Add items and instantiate them only through the custom class add method which then sets a reference back to the collection class.

How and why this even works, I cannot wrap my head around. It is like a weird recursive set up.
Code:
 For Each Ctrl In frm.Section(acHeader).Controls
        Select Case Ctrl.ControlType
            Case acLabel
                Set srtCtrl = New clsMyClass
                Set srtCtrl.Ctrl = Ctrl
                LabelCollection.Add srtCtrl
        End Select
    Next

You create an instance of your class and then initialize it. It holds a reference to the parent form and then adds new non initiated instances of the class to the collection. Each of those instance has no populated collection, and no reference to the form. In fact non of them even have a reference to the label.

The objective was to write a class that when any label in a continuous form is clicked, the field associated with that label is sorted, the ForeColor changes and an arrow is added. Clicking another label, would turn previous label to its default mode and sort the new field.
A right click on any label, takes the form back to the order it was opened with
Off the top of my head if I was doing this.
1. Build the Class SortableLabel
Needs properties
ParentForm: This can be set in the initialize since a control knows it parent (this property may go in the collection)
SortField: Text to sort on.
SelectedColor:
If the arrow is another control then need: ArrowControl reference to the control that show the arrow

2. Build the Class SortableLabels (I basically already did this)
May need event Reset: This turns all back to original settings by looping the controls.
May Need Event SortOn(index). Pass in a name of a control and it will reset the controls and then sort and select the passed in control
May not need to store reference to last selected.
3. In the SortLabel trap the label onclick event and raise the event in the collection Class SortOn(pass name of control).
 

Attachments

However, you could simply copy those classes and tag the labels and it would work on any form.
Since the code and the database were on two different posts, I thought you want to show me another way, something different from the code you shared.

Anyway, Million thanks. I very much appreciate your help.
 
Last edited:
Using My method of Developing a Class and Collection Class

Put the sort order in the tag property of the form.
You can add more than one field like "ProductCode, ProductName"
Code:
Private WithEvents SLS As SortableLabels
Private Sub Form_Load()
  Set SLS = New SortableLabels
  Dim ctrl As Access.Control
  Dim lbl As Access.Label
  For Each ctrl In Me.Controls
    If ctrl.Tag <> "" Then
      Set lbl = ctrl
      SLS.Add lbl, lbl.Tag
    End If
  Next ctrl
End Sub


SortableLabel
Code:
Option Compare Database
Option Explicit

Private WithEvents m_Label As Access.Label
Private m_SortField As String
Private m_ParentForm As Access.Form
Private m_ParentCollection As SortableLabels
Private m_SortFontColor As Long
Private m_DefaultFontColor As Long

'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------   Initialize   -------------------------------------------------------------
'*****************************************************************************************************************************************************************

Public Sub Initialize(TheLabel As Access.Label, TheSortField As String, TheParentCollection As SortableLabels, Optional TheSortFontColor = vbRed)
  Set Me.Label = TheLabel
  Me.sortfield = TheSortField
  Set Me.ParentForm = TheLabel.Parent
  Set Me.ParentCollection = TheParentCollection
  Me.SortFontColor = TheSortFontColor
  Me.DefaultFontColor = TheLabel.ForeColor
  Me.Label.OnClick = "[Event Procedure]"
End Sub

'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------   Properties   -------------------------------------------------------------
'*****************************************************************************************************************************************************************

Public Property Get Label() As Access.Label
    Set Label = m_Label
End Property
Public Property Set Label(ByVal objNewValue As Access.Label)
    Set m_Label = objNewValue
End Property
Public Property Get ParentCollection() As SortableLabels
    Set ParentCollection = m_ParentCollection
End Property

Public Property Set ParentCollection(ByVal objNewValue As SortableLabels)
    Set m_ParentCollection = objNewValue
End Property
Public Property Get sortfield() As String
    sortfield = m_SortField
End Property
Public Property Let sortfield(ByVal sNewValue As String)
    m_SortField = sNewValue
End Property
Public Property Get ParentForm() As Access.Form
    Set ParentForm = m_ParentForm
End Property
Public Property Set ParentForm(ByVal objNewValue As Access.Form)
    Set m_ParentForm = objNewValue
End Property
Public Property Get SortFontColor() As Long
    SortFontColor = m_SortFontColor
End Property

Public Property Let SortFontColor(ByVal lNewValue As Long)
    m_SortFontColor = lNewValue
End Property

Public Property Get DefaultFontColor() As Long
    DefaultFontColor = m_DefaultFontColor
End Property

Public Property Let DefaultFontColor(ByVal lNewValue As Long)
    m_DefaultFontColor = lNewValue
End Property
'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------   Label_Event   -------------------------------------------------------------
'*****************************************************************************************************************************************************************

Private Sub m_Label_Click()
  Me.ParentCollection.SortForm Me
End Sub
The user only works with the Collection to add SortableLabels

SortableLabels
Code:
Option Compare Database
Option Explicit

Private m_Items As Collection
Public Event LabelClicked(ClickedLabel As SortableLabel)
Public Function Add(TheLabel As Access.Label, TheSortField As String) As SortableLabel
  'create a new Item and add to collection
  'Can only create a new item through the collection since the parent collection is a required property.
 
  Dim NewItem As New SortableLabel
  NewItem.Initialize TheLabel, TheSortField, Me
  m_Items.Add NewItem, NewItem.Label.Name
  Set Add = NewItem
End Function

Public Sub Add_Item(ByVal TheItem As SortableLabel)
  'I also add a second Add to allow you to build the object and then assign it
   m_Items.Add TheItem, TheItem.Label.Name
End Sub

Public Property Get Count() As Long
  Count = m_Items.Count
End Property

Public Property Get NewEnum() As IUnknown
    'Attribute NewEnum.VB_UserMemId = -4
    'Attribute NewEnum.VB_MemberFlags = "40"
    'This is allows you to iterate the collection "For Each Item in Items"
   Set NewEnum = m_Items.[_NewEnum]
End Property

Public Property Get Item(Name_Or_Index As Variant) As SortableLabel
  'Attribute Item.VB_UserMemId = 0
  'Export the class and uncomment the below in a text editer to allow this to be the default property
  'Then reimport
  Set Item = m_Items.Item(Name_Or_Index)
End Property

Sub Remove(Name_Or_Index As Variant)
  'remove this person from collection
  'The name is the key of the collection
  m_Items.Remove Name_Or_Index
End Sub

Public Property Get ToString() As String
  Dim strOut As String
  Dim i As Integer
  For i = 1 To Me.Count
    strOut = strOut & Me.Item(i).Label.Name & vbCrLf
  Next i
  ToString = strOut
End Property


'----------------------------------------------- All Classes Have 2 Events Initialize and Terminate --------
Private Sub Class_Initialize()
'Happens when the class is instantiated not related to the fake Initialize method
'Do things here that you want to run on opening
  Set m_Items = New Collection
End Sub

Private Sub Class_Terminate()
  'Should set the object class properties to nothing
  Set m_Items = Nothing
End Sub
'****************************************************************************************************************************************************************
'----------------------------------------------------------------------------------- Sort the Form,  Raise Custom Event from Called Procedure   -------------------------------------------------------------
'*****************************************************************************************************************************************************************

Public Sub SortForm(TheSortLabel As SortableLabel)
  Dim frm As Access.Form
  Dim sortfield As String
  sortfield = TheSortLabel.sortfield
  Set frm = TheSortLabel.ParentForm
  frm.OrderBy = TheSortLabel.sortfield
  frm.OrderByOn = True
  'Set default colors and fonts then set the selected label
  SetDefaults
   TheSortLabel.Label.FontBold = True
   TheSortLabel.Label.ForeColor = TheSortLabel.SortFontColor
 
  'I do not use this but could be used in the form that instantiates
  RaiseEvent LabelClicked(TheSortLabel)
End Sub
Public Function SetDefaults()
  Dim i As Integer
  For i = 1 To m_Items.Count
    m_Items(i).Label.ForeColor = m_Items(i).DefaultFontColor
  Next i
End Function

With a little more work you can add arrows and I like to change the sort order on each click switch between ascending to descending .
Also I left a lot of properties and methods Public. Many should be locked down to Private.
 

Attachments

Users who are viewing this thread

Back
Top Bottom