Triggering an event in subform control in Access using VBA - vba

I wanted to trigger an after update event in the subform using vba from the parent form.
In the subform I have:
Private Sub USER_AfterUpdate()
'After update code
End sub
The subforms name in my parent form is subForm2
So from my main form I am doing:
Call subForm2.Form.USER_AfterUpdate
However, I get
Application-defined or object defined error
I wanted to target the last user field in my subform but I do not mind running an after update event on all of the user field in the sub form.

Either make the function Public:
Public Sub USER_AfterUpdate()
'After update code
End Sub
or create a separate function to call:
Private Sub USER_AfterUpdate()
UserAfterUpdate
End sub
Public Sub UserAfterUpdate()
'After update code
End sub
and then call this (UserAfterUpdate) from the main form.
You may have to use the extended syntax:
Call procedures in a subform or subreport

You may want to use strong typing to reach to your subforms methods. I find this approach much more readable than the extended syntax proposed by Gustav.
Say your subform is named "Form_Subform", and included in a subform control called "subForm2". You could write:
Dim subform as Form_Subform
Set subform = Me.subForm2.Form
Call subform.USER_AfterUpdate
You will find that, after assigning to a strongly typed variable, intellisense will show you all the public subs and functions of your subform (which is not the case with subform.Form, as my best guess there is that it's seen as the parent class Form and not your Form_Subform implementation).

Related

Split Form creates a separate collections for each of its parts

I have a split Form in MS Access where users can select records in the datasheet and add it to a listbox in the Form thereby creating custom selection of records.
The procedure is based on a collection. A selected record gets transformed into a custom Object which gets added to the collection which in turn is used to populate the listbox. I have buttons to Add a Record, Remove a Record or Clear All which work fine.
However I thought, that pressing all these buttons is a bit tedious if you create a Selection of more then a dozen records, so i reckoned it should be simple to bind the actions of the Add and Remove buttons to the doubleclick event of a datasheet field and the listbox respectively.
Unfortunately i was mistaken as this broke the form. While a doubleclick on a field in the datasheet part of the form added the record to the listbox it was now unable to remove an item from the underlying collection giving me Run-time error 5: "Invalid procedure call or argument". Furthermore the clear all Button doesn't properly reset the collection anymore. When trying to Clear and adding a record of the previous selection the code returns my custom error that the record is already part of the selection and the listbox gets populated with the whole previous selection, which should be deleted. This leaves me to believe, that for some Reason the collection gets duplicated or something along these lines. Any hints into the underlying problem is apprecciated. Code as Follows:
Option Compare Database
Dim PersColl As New Collection
Private Sub AddPerson_Click()
AddPersToColl Me!ID
FillListbox
End Sub
Private Sub btnClear_Click()
Set PersColl = Nothing
lBoxSelection.RowSource = vbaNullString
End Sub
Private Sub btnRemovePers_Click()
PersColl.Remove CStr(lBoxSelection.Value)
FillListbox
End Sub
Private Sub FillListbox()
Dim Pers As Person
lBoxSelection.RowSource = vbaNullString
For Each Pers In PersColl
lBoxSelection.AddItem Pers.ID & ";" & Pers.FullName
Next Pers
lBoxSelection.Requery
End Sub
Private Function HasKey(coll As Collection, strKey As String) As Boolean
Dim var As Variant
On Error Resume Next
var = IsObject(coll(strKey))
HasKey = Not IsEmpty(var)
Err.Clear
End Function
Private Sub AddPersToColl(PersonId As Long)
Dim Pers As Person
Set Pers = New Person
Pers.ID = PersonId
If HasKey(PersColl, CStr(PersonId)) = False Then
PersColl.Add Item:=Pers, Key:=CStr(PersonId)
Else: MsgBox "Person Bereits ausgewählt"
End If
End Sub
This works alone, but Simply Adding this breaks it as described above.
Private Sub Nachname_DblClick(Cancel As Integer)
AddPersToColl Me!ID
FillListbox
End Sub
Further testing showed that its not working if i simply remove the Private Sub AddPerson_Click()
Edit1:
Clarification: I suspected that having 2 different events calling the same subs would somehow duplicate the collection in memory, therefore removing one event should work. This is however not the case. Having the subs called by a button_Click event works fine but having the same subs called by a double_click event prompts the behaviour described above. The issue seems therefore not in having the subs bound to more than one event, but rather by having them bound to the Double_Click event.
Edit2: I located the issue but I Haven't found a solution yet. Looks like Split-Forms are not really connected when it comes to the underlying vba code. DoubleClicking on the record in the datasheet view creates a Collection while using the buttons on the form part creates another one. When trying to remove a collection item by clicking a button on the form, it prompts an error because this collection is empty. However clicking the clear all button on the form part doesn't clear the collection associated with the datasheet part.
Putting the collection outside into a separate module might be a workaround but i would appreciate any suggestions which would let me keep the Code in the form module.
The behavior is caused by the split form which creates two separate collections for each one of its parts. Depending from where the event which manipulates the collection gets fired one or the other is affected. I suspect that the split form is in essence not a single form, but rather 2 instances of the same form-class.
A Solution is to Declare a collection in a separate module "Coll":
Option Compare Database
Dim mColl as new Collection
Public Function GetColl() as Collection
Set GetColl= mColl
End Function
And then remove the Declaration of the Collection in the SplitFormclass and Declare the Collection in every Function or Sub by referencing the collection in the separate Module like in the following example:
Option Compare Database
Private Sub AddPersToColl(PersonId As Long)
Dim Pers As Person
Dim PersColl as Collection
Set PersColl = Coll.GetColl
Set Pers = New Person
Pers.ID = PersonId
If HasKey(PersColl, CStr(PersonId)) = False Then
PersColl.Add Item:=Pers, Key:=CStr(PersonId)
Else: MsgBox "Person Bereits ausgewählt"
End If
End Sub
This forces the form to use the same Collection regardless if the event is fired from the form or datasheet part of the split-form. Any Further information is appreciated but the matter is solved for now.
Thanks everyone for their time

How to stop Access from prompting "Do you want to save changes to the layout of query"

How can I stop Access from prompting "Do you want to save changes to the layout of query" when I try to close a form that has a subform in datasheet view?
I have a form with a subform in Datasheet view with the .SourceObject set to a temporary pivot / crosstab query (no actual form object). If the user changes the width of a column and closes the window with the built-in Access close button, the user (and annoyed developer) is always presented with the "Do you want to save changes to the layout of query" prompt.
I am able to avoid this prompt by setting MySubform.SourceObject = "" in the click of my Close button but anyone clicking the [x] button or pressing CTRL+F4 or CTRL+W gets the prompt.
I have put breakpoints in the Form_Close and Form_Unload events but this prompt appears before they fire.
I want to clarify further that this subform object is Unbound and not based on a form object. I am building a dynamic Crosstab SQL statement, creating a QueryDef with the SQL, and then setting MySubform.SourceObject = "query.qry_tmp_Pivot" This is a neat technique for a crosstab/pivot query because the columns can vary and I don't think a form object would support that.
I am able to use the Watch window on the MySubform object and I can see that a MySubform.Form object exists. It has a Controls collection containing a control for all of the columns in my query. I have an optional routine that I can run that will loop through all of the controls and set their .ColumnWidth = -2, which will auto-size the column width based on the widest data of the visible rows in the datasheet. When this routine was running I was noticing that every time I closed the form (not using my Close button) I was getting the save prompt. I have disabled this routine for debugging but a user will still get the prompt if they manually adjust any column width.
I feel like I need to explain this extra detail so you realize this is not an Access 101 issue. You probably know that if you've read this far. Here's another thought I had: Maybe I could trap the Unload event in the subform control before the prompt happens. Because there is no true Form object to put test code in, I created a class object and passed MySubform to it. The class uses WithEvents and creates events like OnClose and OnCurrent on the class module's mForm object. Sample class code is below.
Private WithEvents mForm As Access.Form ' This is the form object of the Subform control
Public Sub InitalizeSubform(Subform As Access.Subform)
Set mForm = Subform.Form
Debug.Print Subform.Name, mForm.Name, mForm.Controls.count
' Create some events to intercept the default events.
mForm.OnClick = "[Event Procedure]"
mForm.OnClose = "[Event Procedure]"
mForm.OnUnload = "[Event Procedure]"
mForm.OnCurrent = "[Event Procedure]"
End Sub
Private Sub mForm_Click()
Debug.Print "Clicking " & mForm.Name
End Sub
Private Sub mForm_Current()
Debug.Print "On Current " & mForm.Name, "Record " & mForm.CurrentRecord & " of " & mForm.RecordsetClone.RecordCount
End Sub
Private Sub mForm_Unload(Cancel As Integer)
Debug.Print "Unloading " & mForm.Name
End Sub
Private Sub mForm_Close()
Debug.Print "Closing " & mForm.Name
End Sub
The VBE Watch window shows my new events on the mForm object but unfortunately they never fire. I know the class works because I used it with a bound subform and all of the events are intercepted by the class. I'm not sure what else to try.
Events on the subform never fire because it's a lightweight form (without a module). See this Q&A and the docs. Lightweight forms don't support event listeners, but do support calling public functions from an event, e.g. mForm.OnClick = "SomePublicFunction()"
Note that the workaround described in this answer also opens up the possibility of displaying a crosstab query in a form without saving it at all.
Alternatively, you could try capturing the event on your main form, and suppress saving there.
I gave answer credit to #ErikA for this question. He directed me to an answer to a somewhat related question here which is what I ended up implementing and it works. His instructions were very straightforward and easier to implement than I first anticipated.
The answer to my question seems to be that it may not be possible to avoid the save prompt when using a lightweight form object. If someone comes along with a solution I'd still like to hear it.
What I learned from this experience:
An unbound subform that has its .SourceObject set to a table or query will not have a code module. This considered to be a lightweight object.
Public functions can be added to event properties on lightweight forms but they don't seem to fire before the save prompt appears as the parent form is closing.
Binding a pivot query or temp table to a real form object that has 254 controls on it is the only way I found that doesn't prompt to save design changes when the parent form closes. Mapping the query/table columns to the 254 datasheet controls is super fast.
A class is a nice way to intercept events if you're not using a lightweight object. A class also lets you open multiple instances of the same object.
I ended up not implementing my solution without using a class because there were no events I needed to capture. At this point I don't need my frmDynDS to be used for other purposes and I'd like to keep the object count down (this is a large app with 1400+ objects).
I put the following code in a function that gets called when the subform is loaded or refreshed.
With subCrosstab
' Set the subform source object to the special Dynamic Datasheet form. This could be set at design time on the Subform control.
.SourceObject = "Form.frmDynDS"
' Run the code to assign the form controls to the Recordset columns.
.Form.LoadTable "qry_tmp_Pivot" ' A query object works as well as a table.
' Optional code
.Form.OnCurrent = "=SomePublicFunction()"
.Form.AllowAdditions = False
.Form.AllowEdits = False
.Form.AllowDeletions = False
.Form.DatasheetFontHeight = 9
End With
I can now resize the columns and never get a prompt to save layout changes no matter how I close the parent form.

VBA Calling a private function inside of a userform

I have a userform defined as "SingleListForm".
I have a private sub named "loadSingleListForm(ID As Double)"
I am attempting to pass a variable into the form and I was implementing it in the fashion that loadSingleListForm would set the form up based on ID, basically re-using one form to show variable data from variable sources in the listbox.
But calling a standard Intersect from outside the form (Worksheet_SelectionChange) these two options compile but do not work.
Application.Run "SingleListForm.loadSingleListForm", ID 'ID already declared and assigned
This doesn't work either
Call ActiveWorkbook.UserForm("SingleListForm").loadSingleListForm(ID)
Where it says UserForm I have also tried SingleListForm.
Here the runtime error is:
I am trying hard not to use a Global Variable here to pass to the form.
Perhaps I should go to Initialize and try something there.
I am trying to pass the variable to the form and then of course set up the form based on this case and then show the form. you can't pass with show so you have to find another way to set up.
I just realized I have not called a userform private function from outside of the form before, but I do it with modules all the time. The first case works in that instance.
Cheers,
-WWC
The better way to do this is to declare a property to the form. In the form's module enter
Option Explicit
Private myID as double
Property Set ID(i as double)
myID = i
End Property
Then your function
Private Sub loadSingleListForm()
can refer to myID with in it's code
To Use this from outside modules you use
Load SingleListForm
SingleListForm.ID = ID 'ID variable already declared
Declare your sub in the form as Public Public Sub loadSingleListForm(ID As Double) and then call it like this SingleListForm.loadSingleListForm ID
Just to cover this. Empty workbook, one button.
The button calls to a private function in the form that does nothing but open a message box. Testing concept here.
This is all there is:
Doesn't work:
UserForm1.you_made_it
Error at compile, method or data member not found
Same if this:
With ThisWorkbook
UserForm1.you_made_it
End With
Then try this:
Application.Run "UserForm1.you_made_it"
Error: Cannot run macro . . . .
Try this from first comment:
ActiveWorkbook.UserForm("UserForm1").you_made_it
Error: Object doesn't support this property or method
So this is the winner from above. Not sure I wanted to go Public but it works.
Doesn't solve how to use a private member in the form but it gets the coding going forward.
Public Sub you_made_it()
MsgBox ("you made it")
End Sub
So far:
1) Move the private to a module and then call it
2) make the function Public and it works
Thank you,
-WWC

VBA- UserForm.Lable.Caption gets Error 91 Object Not Set

System Description: I have a userform that takes input on an item that is being returned. A user clicks the row of the item that needs to be returned and then clicks a "Check-In button"
My Attempt: I created a button checkin_cmdbutton on the spreadsheet that measures which item is selected by which cell is selected Application.ActiveCell.Row, writes the info into a userform Checkin_Form, the user finishes the rest of the check-in info, and clicks submit.
This code is the event for the button checkin_cmdbutton on the spreadsheet:
Private Sub checkin_cmdbutton_Click()
Set ItemID = Cells(Application.ActiveCell.Row, 1)
Set ItemDescription = Cells(Application.ActiveCell.Row, 2)
If ItemID Is Nothing Then
MsgBox ("ID is null, ending...")
Exit Sub
End If
Checkin_Form.UserForm_Initialize
Checkin_Form.itemid_dynamiclabel.Caption = ItemID.Value
Checkin_Form.description_dynamiclabel.Caption = ItemDescription.Value
Checkin_Form.checkin_datepicker.Value = Date
Checkin_Form.Show
End Sub
Problem: The code throws an error 91 "Object variable or with block variable not set" at Checkin_Form.itemid_dynamiclabel.caption and the following 2 lines. Why is an object on a form throwing this error? I can't declare these, can I?
You shouldn't be explicitly calling UserForm_Initialize - that's an event handler, and there's a reason handlers are Private by default: they're invoked by the event provider, when the event provider deems it necessary - in this case, when the object instance is getting initialized.
The best way to ensure the form gets initialized properly, is to treat it like the object it is, instead of storing global state on its default instance.
A UserForm class is little more than a class module with a designer and a VB_PredeclaredId module attribute. This attribute makes VBA create a global-scope object variable named after the class, and that is how this code is legal:
UserForm1.Show
Except, it shouldn't be.
You DON'T want to store global state in the default instance: that's the very last thing you want, especially if your form involves dynamic controls.
New it up instead.
With New UserForm1
.Show
'what follows only executes when the form is closed:
'...
End With
For this to work, you must handle the form's QueryClose event, to prevent the object instance from self-destructing itself when the user clicks the [X] button.
For this to work, you must also avoid destroying the form yourself, e.g. with Unload Me (or worse, Unload UserForm1) calls - say, when the user clicks the [Ok] button. Instead, you Hide (or Me.Hide) the form, so that the caller (the code that New'd it up) can still access the object's state.
From the look of your code - i.e. with the .Show call being the very last thing your macro does, I can tell that you're having the form run the show: this is an anti-pattern that will keep creating problems every time you do that.
Forms don't implement application logic: forms present and collect data. Nothing more, nothing less. It's not the form's job to write to any spreadsheet, or even to know anything about worksheets.
Read this recent article of mine if you want more information about doing forms right.
Now, the actual problem.
Checkin_Form.itemid_dynamiclabel.Caption = ItemID.Value
If that label is dynamic (i.e. created at run-time), then I'm surprised accessing it like this even compiles. First, remove the underscore in the form's name: underscores have a special meaning in VBA - I'm sure you've noticed the pattern by now, of how VBA generates event handlers for a given object:
Private Sub ObjectName_EventName()
End Sub
If ObjectName or EventName has an underscore, you're asking for compile errors at one point or another - one day you'll want to use an Implements statement and discover that your code can't be compiled anymore, if you kept that underscore habit: better lose it now.
If the control is dynamic, you can't do what you're trying to do the way you're doing it.
Dynamic controls need to be accessed through the form's Controls collection:
Dim myLabel As MSForms.Label
Set myLabel = Me.Controls("NameOfTheLabelControl")
Otherwise, you need to keep a reference to the dynamic contols at module-level, in the form's code-behind - you could expose it via a property:
Option Explicit
Dim myLabel As MSForms.Label
Private Sub UserForm_Initialize()
Set myLabel = Me.Controls.Add(...)
End Sub
Public Property Get ThatLabel() As MSForms.Label
Set ThatLabel = myLabel
End Property
Or better, use an actual model class, and let the calling code not be bothered with controls at all - see the previously linked article for details.
TL;DR:
You're getting that error because your label object instance isn't initialized, i.e. it's Nothing. Since you aren't showing your form's code-behind, we can't really point out why that is the case, but my money is on the form's default instance making you yet another victim of the "hey look how easy it is!" VBA tutorials that teach things wrong.
Implement the worksheet-handling code outside the form, make the form collect data, make the calling code read this data after the form is hidden, and then make the calling code create and destroy the form instance.
Now, with all that said, I've no idea why you think you need a dynamic control for this.
Just shooting in the dark, as far as I really do not know the names of your variables and what they are (a few screenshots will be helpful). Try like this, if your code is in a form (as far as you have _Click I assume it is):
Private Sub checkin_cmdbutton_Click()
Set ItemID = Cells(Application.ActiveCell.Row, 1)
Set ItemDescription = Cells(Application.ActiveCell.Row, 2)
Me.itemid_dynamiclabel.Caption = ItemID.Value
Me.description_dynamiclabel.Caption = ItemDescription.Value
Me.checkin_datepicker.Value = Date
Me.Show
End Sub
And try at least declaring the variables (e.g. ItemID etc) and using Option Explicit on top.

VBA Listbox becomes unresponsive after first use

I have a VBA (Excel 2010) system which involves selecting an item from a listbox and then displaying it in another form. Here is a very simplified version of what happens.
' Part of frmForm1 code module
sub lstListbox_Click
dim MyEvent as string
dim i as integer
i=me.lstListbox.listindex
MyEvent=me.lstlistbox.list(i)
' Now show the item in the second form
Load frmForm2
me.hide
ThisWorkbook.LoadDataIntoForm2 (frmForm2, MyEvent)
frmForm2.show
unload frmForm2
me.show
end sub
The listbox accepts the click, and first the event (the event handler is giver above). Key parts of the event handler are:
Load the second form (to display the detail data)
Pass the second form as a UserForm parameter to a procedure (LoadDataIntoForm2)
Hide the host form (frmForm1) and show the second form (frmForm2)
When the second form processes an Exit click, the code looks like this:
' Part of frmForm2 code module
sub cmdExit_Click
me.hide
end sub
The first time round it works fine - but when I return to frmForm1 (in the tail end of the lstListBox_Click procedure), even though the rest of the form is operative, the listbox remains stubbornly unresponsive.
I've managed to abstract this down to a little demo system if that would help - the same behavior is seen there. (It's regular .xls file, but that seems not to be easily acceptable as an upload)
Has anyone seen this before? And does anyone have any ideas how I might get this to work the way I want it to?
Thanks,
Tony
The default for the .Show method is to make the form modal. Explicitly set it to modeless:
Sub lstListbox_Click
...
Me.Show vbModeless
End Sub