Preventing close buttons from saving records in MS Access - vba

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

Related

MS-Access: How to refresh a form after data was updated?

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

Error catching a form in Access using VBA

So i have a form that's updating a table in Microsoft Access. I'm using a BeforeUpdate event call to stop the form from updating if the 'first name' hasn't been filled in.
I basically only want the form to be saved once everything has been filled in, and then switch to see the updated table (the bit under If not cancel).
Currently the popup box to say 'You must enter a First Name' correctly fires, the next pop up box asking if the user wants to undo any changes fires. But then a pop up box saying 'No current record' appears and then the view changes to the table. I'm not sure why this is all happening.
Option Compare Database
Private Sub Form_BeforeUpdate(Cancel As Integer)
Cancel = False
' perform data validation
If IsNull(Me.customer_f_name) Then
MsgBox "You must enter a First Name.", vbCritical, "Data entry error..."
Me.customer_f_name.BorderColor = vbRed
DoCmd.GoToControl "customer_f_name"
Cancel = True
End If
If Not Cancel Then
DoCmd.SelectObject acTable, "ticket_tracker_table"
DoCmd.Requery
DoCmd.GoToRecord acDataTable, "ticket_tracker_table", acLast
End If
' if the save has been canceled or did not pass the validation , then ask to Undo changes
If Cancel Then
If MsgBox("Do you want to undo any changes?", vbYesNo, "Confirm") = vbYes Then
Me.Undo
End If
End If
End Sub
Review https://learn.microsoft.com/en-us/office/vba/api/Access.Form.BeforeUpdate(even)
Instead of GoToControl, use SetFocus.
I did a very simple test with the Undo question and it does work:
Private Sub Form_BeforeUpdate(Cancel As Integer)
If IsNull(Me.customer_f_name) Then
MsgBox "Must enter customer name."
Cancel = True
If MsgBox("Do you want to undo edits?", vbYesNo) = vbYes Then Me.Undo
Me.customer_f_name.SetFocus
End If
End Sub
Suggest you put table code in AfterUpdate event.
so to supplement J7, with some big picture view: "basically only want the form to be saved once everything has been filled in" …. that is swimming against the tide; which is why you are having to write a lot of code. The product enters data as you go fundamentally. that is it's core design.
it is very normal to do field checks as you go - typically in the AfterUpdate event, the check code is applied and if invalid - generate a message box and invoke an Undo plus reset the focus back to the field they were attempting to leave.
A totally different design, in some cases it may justify a temp local duplicate table just for the purposes of holding form input data - - this approach allows a very elaborate check(s) against data in the production database; where upon if valid an Append Query then writes the data into the permanent table.

VBA in Access: How does DoCmd.RunCommand acCmdSaveRecord work?

I use
DoCmd.RunCommand acCmdSaveRecord
to save but Im not sure how this actually works. Does it save every unsaved change? Or does it save everything no matter if it was changed or not? Or does it only save the current form? Whats about releated unsaved changes in other forms? Or does it function in any other way? Is there any offical documentation to this function?
It saves the current record of the active form. It is the same as clicking on the record selector.
If the current record is not currently being edited (not "Dirty"), then nothing happens.
If you want better control, especially when dealing with subforms, I suggest to use the Form.Dirty property instead. With this you can explicitly address the form you want to save.
To save the current record in a form module (only save when needed):
If Me.Dirty Then
Me.Dirty = False
End If
To save the record in any form
With Forms!myForm
If .Dirty Then
.Dirty = False
End If
End With
Or a subform
Forms!mainForm!SubFormControl.Form.Dirty = False
This is much clearer and better IMO. I have stopped using DoCmd.RunCommand acCmdSaveRecord completely.

Row gets modified when form is opened in access vba

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

Treeview behaving differently when tapped than when clicked

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