The situation: I have an Access 2010 db that is meant to be deployed on a Windows 8 tablet. The main form of the app contains a Treeview control. Selecting a node on the Treeview sets the visibility of one of several subforms that are used for viewing/editing details of the selected node item. I have a yes/no message box and some basic code on the BeforeUpdate event for each of the subforms. So when the record on the subform is dirty and the user clicks anywhere on the main form (including anywhere in the Treeview control) this code is triggered.
The problem: When the subform record is dirty and the user taps anywhere on the Treeview control, the message box pops up but cannot be interacted with because the app is busy. Doing what, I don't know, but it stays that way until Access is shut down via Task Manager. There is no code attached to the Treeview for anything but the Click event. This happens even when they touch white space in the Treeview below the existing nodes.
If the record is not dirty, everything works fine.
If the record is dirty and the user hits the "Save" button on the subform to trigger the BeforeUpdate event, everything works fine.
If the user taps a different control or in the empty space on the main form, the BeforeUpdate event is triggered and everything works fine.
If you plug a mouse into the tablet and do the same series of steps by clicking instead of tapping, everything works fine.
I've done a ton of searching and haven't been able to find anything relevant to this, so any suggestions or guidance to new places to look for suggestions would be deeply appreciated.
I've attached a sample of the BeforeUpdate code that exists on each of these subforms. It's pretty basic, but maybe there's something in it that tapping and Treeviews just don't like.
Private Sub Form_BeforeUpdate(Cancel As Integer)
'If the form data has changed a message is shown asking if
'the changes should be saved. If the answer is no then
'the changes are undone
On Error GoTo BeforeUpdate_Error
If Me.Dirty Then
'Add PropertyID, LPParentNodeID and TreeNodeID if Record is new
If Me.NewRecord Then
Me.PropertyID = Me.Parent!PropertyID
Me.LPParentNodeID = Me.Parent!txtCurrKey
Me.TreeNodeID = DateDiff("s", Date, Now())
End If
'Display prompt to save the record
If MsgBox("The record has changed - do you want to save it?", _
vbYesNo + vbQuestion, "Save Changes") = vbNo Then
Me.Undo
End If
End If
'If the record is still dirty, then record the change in the Audit table
If Me.Dirty Then
Call AuditTrail(Me, InstanceID, PropertyID)
End If
BeforeUpdate_Exit:
Exit Sub
BeforeUpdate_Error:
MsgBox Err.Description
Resume BeforeUpdate_Exit
End Sub
08/30/2013 Addition: I forgot to mention the debugging behavior in the original question. When I set a breakpoint on the BeforeUpdate Sub of the subform on any line from the actual Sub entry point down to the If statement with the message box, the code window comes up but the app again becomes busy, and I can't interact with either window. Just like before, this behavior is unique to tapping that accursed Treeview control.
What you could do is put a kind of edit/save structure into each of the subforms, whereby controls in the subform are locked until edit is clicked, and re-locked after save is clicked. So:
private sub bEdit()
editMode true
end sub
private sub bSave()
...save logic
editMode false
end sub
private sub editMode(isEdit as boolean)
dim ctl as control
for each ctl in me.controls
if ctl.controltype is actextbox or ctl.controltype is accombobox then
ctl.locked = (not isEdit)
end if
next
end sub
With this approach, it's then a small task to add editmode control for the parent form by adding
me.parent.editmode isEdit
to the end of of the editmode procedure.
In the parent form, editMode will need to be a PUBLIC sub.
In this sub, control whether the tree will do anything when clicked on:
public sub editMode(isEdit as boolean)
tree.enabled = (not isEdit)
end sub
Related
I have a query shown in a form as a table. Additionally I have a button which opens another form where you can manipulate saved data. As soon as this form closes I would like to have the query in the other form to be updated by a macro. I tried couple of macro commands. Nothing worked. I thought I could use the requery macro with my subform as parameter but even that didn't work. What can I do?
Data gets only updated when I hit 'refresh all' but this should happen automatically.
I have ran in to all sorts of strange edge cases with forms containing subforms. What happens when the current record is removed?
This may seem overkill but if you want to be sure that all forms are up to date then you can use this on the parent form:
'#Description("Refresh the data on the form and all subforms")
Public Sub RefreshForm(ByVal theForm As Form)
On Error GoTo ErrorHandling
Echo False
RequeryInPlace theForm
theForm.Refresh
MoveToValidRecord theForm
Dim childForm As Control
For Each childForm In theForm.Controls
If TypeOf childForm Is SubForm Then
RequeryInPlace childForm.Form
childForm.Form.Refresh
MoveToValidRecord childForm.Form
End If
Next
ErrorHandling:
' Fail fast, ensure echo is always left on!
Echo True
End Sub
'#Description("Requery an entire form without losing the currently selected record")
Public Sub RequeryInPlace(ByVal theForm As Form)
Dim whereIam As Variant
With theForm
whereIam = .Form.Bookmark
.Form.Requery
.Form.Bookmark = whereIam
End With
End Sub
'#Description("Move cursor to a valid record")
Public Sub MoveToValidRecord(ByVal theForm As Form)
With theForm.Recordset
If .EOF And .BOF Then Exit Sub
If .EOF Then
.MoveLast
ElseIf .BOF Then
.MoveFirst
Else
.MoveNext
.MovePrevious
End If
End With
End Sub
Ok, the way to do this?
If you just editing data? (not adding new rows) to that grid?
Your code to launch the 2nd form will look like this:
me.Refresh - optional but REQUIRED if you do allow editing in the gride.
docmd.OpenForm "frmDetailsEdit",,,"ID = " & Me!ID,,acDialog
me.refresh - this will show any data changes you made in the 2nd dialog form
Note carefull:
You don't need the first me.Refresh UNLESS you allow edits in the data/grid display.
The acDialog will cause the code to WAIT/HALT until the user is done editing in the 2nd form.
The final me.refresh will update any data changes, and it will also keep the reocrd pointer on the same current row.
However, if your 2nd dialog form launched ALSO allows adding of records, then a me.refresh will NOT show the newly added records.
If you allow adding? Then you need to do a me.Requery on that last line in place of me.refresh. This will re-load the form based on its query, but you will ALSO lose the current position. You can if you wish re-position the record pointer/location if you need to, but we don't know if you need this ability as of yet.
the query is not being updated in background unless you are opening the form which calls the Query to run , an alternative is to request the query to run again to update the form as follows
Form.Requery
if your form is having a sub form then you need to address the sub form as well
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.
I need to prepare a screen that will work as a comments screen as used on Facebook. I have to prepare it on access form. Where User will enter the comments and the same should get stored in a table with PartNumber and ItemNumber. Also the recent Comments should be displayed on the same form.
I have prepared a form having Recordsource as my table where the comments needs to be saved. And written a click event where as user clicks the button the comments will get saved in table. But the prob is that without clicking that button if i type values in textbox and close the form still the table gets updated with value before click. Below is the code
Private Sub Post_cmnt_Click()
Me.RecordSource = "Part_GeneralPartComment"
Call FromForm_Add
End Sub
Access automatically commits pending changes when a bound form is closed. If you want to prevent that from happening you can add the following code as the On Close event handler for the form:
Private Sub Form_Close()
On Error GoTo Form_Close_error
DoCmd.RunCommand acCmdUndo
Exit Sub
Form_Close_error:
If Err.Number <> 2046 Then
' error was something other than "The command or action 'Undo' isn't available now."
Err.Raise Err.Number
End If
End Sub
In a Microsoft Access form, whenever the current record changes, any changes in bound controls are silently saved to the database tables. This is fine, but I don't want it to happen when a user closes a form, because it is the direct opposite of what many people would expect.
The best example is when you try to close an excel file with unsaved changes, it asks whether the changes should be discarded. This is exactly what I'm trying to achieve in Access, but can't find any way to trap the close button's event in VBA.
The form's Unload event is the first event that is triggered when someone clicks the close button, but by then the changes are already written to the database.
Is this at all possible, or do I have to create my own close buttons? I'm comfortable with writing large amounts of code for trivial things like this but I hate having to clutter the GUI.
You have to work with Form_BeforeUpdate event. Below is an example; however it does create a typical warning message: "You can't save this record at this time. Microsoft Access may have encountered an error while trying to save a record. ..." - depending on your database settings. You can use simple workaround below to avoid displaying of that message.
Private Sub Form_BeforeUpdate(Cancel As Integer)
Cancel = True
'Or even better you can check certain fields here (If Then...)
End Sub
Private Sub Form_Error(DataErr As Integer, Response As Integer)
If DataErr = 2169 Then
Response = True
End If
End Sub
Sean gave an almost correct answer but it leaves gaps.
In general, the FORM's BeforeUpdate is the most important form event. It is your LAST line of defense and ALWAYS runs prior to a record being saved regardless of what prompted the save (form close, new record, your own save button, clicking into a subform, etc.) Although I occasionally use the control's BeforeUpdate event just so the user gets the error message sooner, the bulk of the validation code I write runs in the Form_BeforeUpdate event. This is the event you MUST use if you want to ensure that certain controls are not empty. No control level event will do this reliably for all situations. Primarily because if the control never gets focus, no control level event ever fires. Form_BeforeUpdate is also the event you would use if your validation involves multiple fields. If you are using any other control or event level event, you are wasting your time. There is always away around your "trap" and your table almost certainly contains invalid data.
Regarding the OP's question. If you want to force people to use your own save button and prompt them if they don't then you need a form level variable as Sean's suggestion implied. The only difference, is that you need to set it to False, in the form's Current event NOT the Open event. You want the flag to be reset for EVERY new record, not just when the form opens. Then you set it to True in your save button click event, just before you force the record to save with DoCmd.RunCommand acCmdSaveRecord.
Then finally, in the Form_BeforeUpdate event, you check the value of the variable.
If bClose = False Then
If MsgBox("Do you want to save the changes?", vbYesNo) = vbNo Then
Cancel = True
If MsgBox("Do you want to discard the Changes?", vbYesNo) = vbYes Then
Me.Undo
End If
Exit Sub
End If
End If
this is code I have that checks to see if the form is being closed or saved.
Private Sub Form_BeforeUpdate(Cancel As Integer)
If Not UsingSaveButton Then
If MsgBox("Abandon Data?", vbInformation + vbYesNo) = vbNo Then
Cancel = True
Else
DoCmd.RunCommand acCmdUndo
End If
End If
End Sub
I have a Boolean Flag that is set to False on loading, and then when my Save button is used, I set it to true to allow the update to run through.
If the flag is not set, then they are leaving the record (either through going to a different record, or closing the form) so I ask if they actually want to save the changes.
The Cancel = True aborts the exit of the form or the move to a different record if any changes have been made.
The DoCmd.RunCommand acCmdUndo undoes any changes so they are not saved.
Actually, you cannot trap this, Access is working directly upon the table, every change is already being saved when the field looses the focus by moving to another field, record or a button.
Actually imho this is a great advantage compared to Excel
If you really want a behaviour similar to Excel you'd need to work on a copy of the table and some code for updating.
In the Form in the 'On Unload' event add the below code.
DoCmd.RunCommand acCmdUndo
DoCmd.Quit
Records no longer being saved when users close a form in any way.
** you can use exit button with this code**
Private Sub Button_Click()
If Me.Dirty = True Then
If MsgBox(" Save Change ", vbYesNo) = vbYes Then
Me.Dirty = False
Else
Me.Undo
End If
End If
DoCmd.Close acForm, "FormName"
End Sub
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