Access VBA listing collection items in a class module - vba

Although I'm reasonable experienced VBA developer, I have not had the need to use class modules or collections but thought this may be my opportunity to extend my knowledge.
In an application I have a number of forms which all have the same functionality and I now need to increase that functionality. Towards that, I am trying to reorder a collection in a class module, but get an error 91 - object variable or with block not set. The collection is created when I assign events to controls. The original code I obtained from here (Many thanks mwolfe) VBA - getting name of the label in mousemove event
and has been adapted to Access. The assignments of events works well and all the events work providing I am only doing something with that control such as change a background color, change size or location on the form.
The problem comes when I want to reorder it in the collection - with a view to having an impact on location in the form. However I am unable to access the collection itself in the first place.
The below is my latest attempt and the error occurs in the collcount Get indicated by asterisks (right at the bottom of the code block). I am using Count as a test. Once I understand what I am doing wrong I should be able to manipulate it as required.
mLabelColl returns a correct count before leaving the LabelsToTrack function, but is then not found in any other function.
As you will see from the commented out debug statements, I have tried making mLabelColl Private and Dim in the top declaration, using 'Debug.Print mLabelColl.Count' in the mousedown event and trying to create a different class to store the list of labels.
I feel I am missing something pretty simple but I'm at a loss as to what - can someone please put me out of my misery
Option Compare Database
Option Explicit
'weMouseMove class module:
Private WithEvents mLbl As Access.Label
Public mLabelColl As Collection
'Dim LblList as clLabels
Function LabelsToTrack(ParamArray labels() As Variant)
Set mLabelColl = New Collection 'assign a pointer
Dim i As Integer
For i = LBound(labels) To UBound(labels)
'Set mLabelColl = New Collection events not assigned if set here
Dim LblToTrack As weMouseMove 'needs to be declared here - why?
Set LblToTrack = New weMouseMove 'assign a pointer
Dim lbl As Access.Label
Set lbl = labels(i)
LblToTrack.TrackLabel lbl
mLabelColl.Add LblToTrack 'add to mlabelcoll collection
'Set LblList as New clLabels
'LblList.addLabel lbl
Next i
Debug.Print mLabelColl.Count 'returns correct number
Debug.Print dsform.countcoll '1 - incorrect
End Function
Sub TrackLabel(lbl As Access.Label)
Set mLbl = lbl
End Sub
Private Sub mLbl_MouseDown(Button As Integer, Shift As Integer, x As Single, Y As Single)
Dim tLbl As Access.Label
'Debug.Print LblList.Count 'Compile error - Expected function or variable (Despite Count being an option
'Debug.Print mLabelColl.Count 'error 91
'Debug.Print LblList.CountLbls 'error 91
Debug.Print collCount
End Sub
Property Get collCount() As Integer
*collCount = mLabelColl.Count* 'error 91
End Property

In order to have all the weMouseMove objects reference the same collection in their mLabelColl pointer, a single line can achieve it:
LblToTrack.TrackLabel lbl
mLabelColl.Add LblToTrack
Set LblToTrack.mLabelColl = mLabelColl ' <-- Add this line.
But please be aware that this leads to a circular reference between the collection and its contained objects, a problem that is known to be a source of memory leaks, but this should not be an important issue in this case.

Related

VB.Net 2010 "Object reference not set to an instance of an object." when declaring a variable from a Dictionary with a list as the value

This is a school project for Programming (not an assessment or assignment, so I'm not cheating) where I have to make a 7-segment displaySource 1. I decided instead of going the traditional way and manually setting each RectangeShape to visible on each button pressed to display a number; store the corresponding number and which RectangleShape(s) to turn on as a key-value pair in a dictionary. I have some knowledge of Python, so this is where I got the idea from. My formSource 2 has 7 RectangleShape(s) and 10 Button(s). Just as an experiment since it's my first time working with Dictionaries and Lists in VB.net, I decided to only try it out for the number 1 for now (shp4 and shp5 should be visible). Here is the dictionary I made:
Dim Numbers As New Dictionary(Of Integer, List(Of PowerPacks.Shape)) From {{1, New List(Of PowerPacks.Shape) From {shp4, shp5}}, {2, New List(Of PowerPacks.Shape) From {shp2, shp3}}}
Here is the code for the button (btn1):
Private Sub btn1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn1.Click
Dim Thing As PowerPacks.Shape = Numbers(1)(0)
Thing.Visible = True
End Sub
When the program gets to the line that says Thing.Visible = True, it throws an error. It's a NullReferenceException that states Object reference not set to an instance of an object. Any ideas on how to fix this?
Sources
Source 1:
Source 2:
Programming is not magic. It works pretty much as you'd expect. If you are getting Nothing out of your List within a Dictionary then you must be putting Nothing in. Did you use the debugger before posting? Did you actually look at the value of shp2, etc, when this line is executed:
Dim Numbers As New Dictionary(Of Integer, List(Of PowerPacks.Shape)) From {{1, New List(Of PowerPacks.Shape) From {shp4, shp5}}, {2, New List(Of PowerPacks.Shape) From {shp2, shp3}}}
Based on your use of that Numbers variable in the second code snippet, it must be a member variable, which means that that first code snippet is outside any method, which means that it is executed before the class constructor, which means that no controls have been created at that time, which means that any fields that refer to controls can't be anything but Nothing. If you had put a breakpoint on that line and used the debugger, as you should have, then you'd have seen that.
The solution is not actually add the Shapes to the collection until they are created. That means after the call to InitializeComponent. That means that you can create your own constructor and do it there if you want, but that you should probably just do it in the Load event handler. Just declare the Dictionary variable without creating an object:
Private numbers As Dictionary(Of Integer, PowerPacks.Shape())
Note that I have provide an explicit access modifier, which you should ALWAYS do for all members, and also started the name with a lower-case letter, which you probably ought to do for all private fields. You then create the object in the load event handler:
numbers As New Dictionary(Of Integer, PowerPacks.Shape()) From {{1, {shp4, shp5}},
{2, {shp2, shp3}}}
I've taken the liberty of simplifying that by using arrays rather than Lists that add no value.
On your 7-Segment Display Form, I would use a List (of String) containing which rectangles are turned on for each dictionary key (number). This would require first programatically adding the 7 shapes onto the form (looks like you already coded that), and then in each button (for numbers) just pick off which rectangles are turned on and change their background color. "Front loading" all the Shapes (rectangles) into the Dictionary is probably unnecessary, since they are already manually fixed on the Form. If they are not, then pull out the code that places them on the form and run that first, one time, when the form loads. Using this approach, you don't have to worry about Shapes any more.
In Form 1, add:
Dim dicRSBackColor As New Dictionary(Of Integer, List(Of String))
Dim SC As New ShapeContainer
In Form1_Load, add the following:
'define each of the 7 rectangles using, e.g.:
Dim myrec4 As New RectangleShape
myrec4.Name = "shp4"
myrec4.Visible = True
myrec4.BringToFront()
'add locations and size to myrec4
myrec4.Top=100
myrec4.Left=100
myrec4.Width=10
myrec4.Height=100
'then add myrec4 to Shape container
SC.Shapes.Add(myrec4)
'add myrec4 to Form1
Me.Controls.Add(myrec4)
'do the above for all 7 rectangleshapes
'For the dictionary, add number 1's rectangleshape (turned on)
Dim mylist As List(Of String)
mylist.Add("shp4")
mylist.Add("shp5")
dicRSBackColor.Add(1, mylist)
'add number 2 rectangles (turned on)
mylist.Clear()
mylist.Add("shp1")
mylist.Add("shp3")
mylist.Add("shp4")
mylist.Add("shp6")
mylist.Add("shp7")
dicRSBackColor.Add(2, mylist)
'continue until all 7 rec's added
Last, in the button _Click for number 1, use the following:
'First change background color of all rectangleshapes to Form1's back color
For Each rs As RectangleShape In Me.SC.Shapes
rs.BackColor = Me.BackColor
Next
'Now pull the list of shapes that are turned on for number 1 using the dictionary
Dim mylist1 As List(Of String)
mylist1 = dicRSBackColor(1) 'you're pulling the value for key=1, which is a list of strings
'use a double loop and find out when the list value is the same as the name of the rectangleshape, and then set the back color to orange
For Each item In mylist1
For Each rs As RectangleShape In Me.SC.Shapes
If item = rs.Name Then rs.BackColor = Color.Orange
Next
Next

Access VBA - How to get the properties of a parent subform, or, get the user-given name for a subform (not the object reference name)

In MS Access 2016, let's say I have 2 forms: frmMain and frmBaby.
I have embedded frmBaby as a subform on frmMain. I have embedded on frmBaby a control (let's say it's a textbox, but it could be any control) named tbxInput.
On frmMain, since frmBaby is a "control" on frmMain, I have given that control the traditional name of subfrmBaby.
Now, in VBA, an event on subfrmBaby passes the tbxInput control ByRef (as Me.tbxInput) to a function that is meant to return the .Left property of the parent of the control passed ByRef. That is, I need the function to determine the .Left property for the location of subfrmBaby on frmMain. (The function is more complicated than this, but for the sake of keeping this question let's just say the function is returning the .Left property value because the .Left value is what I need to perform the function.)
Let's say the function is: Public Function fncLocation(ByRef whtControl As Variant) as Long
(I use Variant so that null values can be passed.)
Here is the code that I expected to return the .Left value of the parent (i.e., subfrmBaby) of whtControl: lngLeft = whtControl.Parent.Left
However, that gives me an error of: "Application or object-defined error"
When I use the immediate window to check things out I find that whtControl.Parent.Name is "frmBaby" and not "subfrmBaby" which makes it problematic to reference the subform on frmMain since I cannot figure out how to get the actual name given to the control on frmMain from the object passed to the function and so I cannot reference the subform by name either.
Questions:
How can I get the .Left value for the parent of the control passed to this function?
How can I get the actual name assigned to the subform control on frmMain? In this case, I need the name of "subfrmBaby" rather than "frmBaby."
Thanks in advance for ideas.
You can do this by iterating the controls on the main form, assuming whtControl is the form object of the subform (if it's a textbox, it's whtControl.Parent.Parent and If c.Form Is whtControl.Parent Then)
Dim mainForm As Form
Set mainForm = whtControl.Parent
Dim c As Access.Control
Dim subformControl As Access.Control
For Each c In mainForm.Controls
If TypeOf c Is SubForm Then
If c.Form Is whtControl Then
Set subformControl = c
Exit For
End If
End If
Next
If Not subformControl Is Nothing Then
Debug.Print subformControl.Left
End If
Note that iterating controls comes at a performance penalty, but this code should still take milliseconds, not seconds. Also, since we test reference equality, it works even if the same subform is present multiple times on the parent form.
I just had this issue, and I think I solved it! Thanks to Eric A's answer above to get me started. I tweaked it and built on it for my use. In my case, I needed to save the "full" address of a control to build and facilitate a control log (used to log both user actions for auditing and to allow for users to "undo" an action). I have several duplicated subforms in several sub-form controls, and a few sub-sub forms (each displaying differently filtered and sorted data), so I couldn't rely on simply knowing the subform's name, I also needed the subform control name. This also leverages others' work (as noted in the code notes with some tweaks to allow easier re-use for us. I've posted it here, hopefully it will help someone else. I know I've used SO a lot.
How we use it:
On a form, after logging an action, we record the control's ID info, which calls a function to get the toppost form (this is used in conjunction with afterUpdate event so we refresh the main form and subform). We also use the HWND to validate some other items elsewhere, and to grab a form if we don't have the initial form reference. If you use this and modify it, please point back to here and give comments.
Specific Function Code to get Control "address" and get control from address
' Posted on StackOverflow 2022 February 18 in response to Question:
' https://stackoverflow.com/q/66425195/16107370
' Link to specific answer: https://stackoverflow.com/a/71176443/16107370
' Use is granted for reuse, modification, and sharing with others
' so long as reference to the original source is maintained and you
' help lift others up as others have done those who helped with this concept
' and code.
Private Function GetControlAddress(ByRef ControlTarget As Object, _
ByRef ParentForm As Access.Form) As String
' Used in concert with building a form ID, this allows reflection back to the specific
' subform control and containing subform.
Dim ControlSeek As Access.Control
If TypeOf ControlTarget Is Form Then
' You need to dig through the whole list to get the specific controls for proper reflection down.
For Each ControlSeek In ParentForm.Controls
If ControlSeek Is ControlTarget Then
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlTarget.Name & FormIDFormSep
Exit For
ElseIf TypeOf ControlSeek Is SubForm Then
If ControlSeek.Form Is ControlTarget Then
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlSeek.Name & FormIDFormSep
End If
End If
Next ControlSeek
Else
' If you're not looking for a form, then you can skip the slow step of running through all controls.
GetControlAddress = ParentForm.Name & FormIDHWNDSep & ParentForm.Hwnd & FormIDHWNDSep & ControlTarget.Name & FormIDFormSep
End If
End Function
Public Function GetControlByAddress(ByRef StartingForm As Access.Form, ByRef strControlAddress As String) As Access.Control
' Given a control address and a starting form, this will return that control's form.
Dim ControlTarget As Access.Control
Dim TargetForm As Access.Form ' This is a reference to the hosting control
'Dim ControlSeek As
Dim FormIDArr() As String
Dim FormInfo() As String
Dim ControlDepth As Long
Dim CurrentDepth As Long
If strControlAddress = vbNullString Then GoTo Exit_Here
FormIDArr = Split(strControlAddress, FormIDFormSep)
' Because there's always a trailing closing mark (easier to handle buidling address), we skip the last array
' value, as it's always (or supposed to be...) empty.
ControlDepth = UBound(FormIDArr) - LBound(FormIDArr)
' Split out the form's Specific Information to use the details.
FormInfo = Split(FormIDArr(CurrentDepth), FormIDHWNDSep)
' The specific control is located in the 3rd element, zero referenced, so 2.
Set ControlTarget = StartingForm.Controls(FormInfo(2))
' If ControlDepth is 1 (control is on passed form) you can skip the hard and slow work of digging.
If ControlDepth > 1 Then
For CurrentDepth = 1 To ControlDepth - 1
' Note: you start at 1 because you already did the first one above.
' Split out the form's Specific Information to use the details.
FormInfo = Split(FormIDArr(CurrentDepth), FormIDHWNDSep)
Set TargetForm = ControlTarget.Form
Set ControlTarget = TargetForm.Controls(FormInfo(2))
Next CurrentDepth
End If
Exit_Here:
Set GetControlByAddress = ControlTarget
End Function
Required Helper Functions
Note, I use a property for the separators as there is some user locale handling (no included), and it also ensures that if we do change the separator it remains consistent. In this example, I simply set them to a character which is unlikely to be used in a form name. You will need to ensure your forms don't use the separator characters.
Public Function hasParent(ByRef p_form As Form) As Boolean
' Borrowed concept from https://nolongerset.com/get-top-form-by-control/
' and modified for our uses.
On Error Resume Next
hasParent = (Not p_form.Parent Is Nothing)
Err.Clear ' The last line of this will cause an error. Clear it so it goes away.
End Function
Private Function GetFormObjectByCtl(ByRef ctl As Object, _
ByRef ReturnTopForm As Boolean, Optional ByRef strControlAddress As String) As Form
strControlAddress = GetControlAddress(ctl, ctl.Parent) & strControlAddress
If TypeOf ctl.Parent Is Form Then
If ReturnTopForm Then
If hasParent(ctl.Parent) Then
'Recursively call the function if this is a subform
' and we need the top form
Set GetFormObjectByCtl = GetFormObjectByCtl( _
ctl.Parent, ReturnTopForm, strControlAddress)
Exit Function
End If
End If
Set GetFormObjectByCtl = ctl.Parent
Else
'Recursively call the function until we reach the form
Set GetFormObjectByCtl = GetFormObjectByCtl( _
ctl.Parent, ReturnTopForm, strControlAddress)
End If
End Function
Public Function GetFormByCtl(ctl As Object, Optional ByRef strControlAddress As String) As Form
Set GetFormByCtl = GetFormObjectByCtl(ctl, False, strControlAddress)
End Function
Public Function GetTopFormByCtl(ctl As Object, Optional ByRef strControlAddress As String) As Form
Set GetTopFormByCtl = GetFormObjectByCtl(ctl, True, strControlAddress)
End Function
Public Property Get FormIDHWNDSep() As String
FormIDHWNDSep = "|"
End Property
Public Property Get FormIDFormSep() As String
FormIDFormSep = ";"
End Property
Interesting. I don't think you can.
As you have seen, the parent of whtControl is its form, frmBaby.
The parent of that one is frmMain. The subform control is not part of the object chain when "going up", only when going down.
If you always use the naming scheme as in the question, you could do something like this (air code):
strSubform = whtControl.Parent.Name
strSubformCtrl = "sub" & strSubform
Set ctlSubform = whtControl.Parent.Parent(strSubformCtrl)

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.

Modify Chart properties in Access report via VBA (error 2771)

I am building an Access report (2010 version), and would like to be able to customize it based on the user's selections on a form. When I run it, I get error 2771: The bound or unbound object frame you tried to edit does not contain an OLE object.
This is the code to pass the parameter:
Private Sub Command120_Click()
DoCmd.OpenReport ReportName:="rpt_EODGraph", View:=acViewPreview, _
OpenArgs:=Me!Text0.Value
End Sub
This is the code to open the report.
Private Sub Report_Open(Cancel As Integer)
Dim ch As Chart
Set ch = Me.Graph3.Object.Application.Chart 'This line generates the error
ch.ChartTitle.text = OpenArgs
End Sub
I've found at least one person saying that this is not actually possible to do on a report. (http://www.access-programmers.co.uk/forums/showthread.php?t=177778&page=2 Note that this is page 2 of a 2 page forum discussion...) Can anyone corroborate or refute?
Apparently the Report has to have some kind of focus before OLE objects are accessible.
It is enough if you click on it or set the focus to something:
Private Sub Report_Open(Cancel As Integer)
Dim ch As Object
Me.RandomButton.SetFocus
Set ch = Me.Diagramm11.Object
ch.ChartTitle.Text = "Hello"
End Sub
This works. I just set a button on the report that gets the focus. Perhaps you find something more elegant ;)

Pass Global Variable to custom UserForm

Have the following code that ends up with a .Count variable. I would like to take that integer value and output it to a sentence in a custom UserForm. How do I do this within the Visual Studio 2012 designer. This is really stumping me. Thanks!
Public Shared Property mailItem As Object
Public Shared Property BodyMatchResults As MatchCollection
Public Shared Property SubjectMatchResults As MatchCollection
Public Sub Application_ItemSend(ByVal Item As Object, _
ByRef Cancel As Boolean) Handles Application.ItemSend
Dim mailItem As Outlook.MailItem = TryCast(Item, Outlook.MailItem)
If mailItem IsNot Nothing Then
Dim attachments = mailItem.Attachments
For Each attachment As Outlook.Attachment In attachments
AttachmentQuery(attachment, mailItem, Cancel)
Next attachment
End If
Dim BodyMatchResults As MatchCollection
Dim SubjectMatchResults As MatchCollection
Dim RegexObj As New Regex("\b(?!000)(?!666)(?!9)[0-9]{3}[ .-]?(?!00)[0-9]{2}[ .-]?(?!0000)[0-9]{4}\b")
BodyMatchResults = RegexObj.Matches(mailItem.Body)
SubjectMatchResults = RegexObj.Matches(mailItem.Subject)
If BodyMatchResults.Count > 0 Or SubjectMatchResults.Count > 0 Then
Cancel = True
MessageBox.Show((BodyMatchResults.Count + SubjectMatchResults.Count))
' Access individual matches using AllMatchResults.Item[]
Else
Cancel = False
End If
The UserForm is pretty basic with three buttons and a warning text above it. I would like to have in that text "There were either "BodyMatchResults.count" or "subjectmatchresults.count" in your email.
Well you obviously beging with your .count variable (which for arguments sake I'll assume is of type Integer). Hopefully you also have your Custom UserForm which we'll assume you have called UserForm.
In that UserForm add the following code:
Private _count As Integer
Public Sub New (ByVal count AS Integer)
InitializeCompponent()
_count = count
End Sub
You can then use the _count variable to dipaly the informationyou want displayed.
Now when you call your UserForm you can pass your .count variable to it so that it can be used there like this:
dim frm as New UserForm(NumberOfCountsIWantToDisplay)
This basic principle will work for most situations.
Edit
Clearly I'm not reading things properly. Your question had specifically asked about passing a Global variable. You should simply be able to refer to a publically defined global variable from anywhere within your application (so long as that variable has application scope). However global variables can be more trouble that they are worth and if in reality you simply want to pass one value from form a to form b then I would use the approach that I had originally outlined having misread the subject title, for which I apologise.