I am trying to prevent users from saving without using the naming conventions established in a Userform. Below is the workbook code:
Public Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
UserForm1.Show
If UserForm1.Visible = True Then
Cancel = False
Exit Sub
End If
Cancel = True
MsgBox ("Please use the save form.")
End Sub
I then have a button on the userform that should save the file containing the code:
Sub SaveButton_Click()
Dim FileName As String
FileName = FileNameTextBox.Value
ActiveWorkbook.SaveAs FileName:=FileName
End Sub
Unfortunately this gives me a
400 error: "Can't show modally".
I am not sure what this means or how to resolve it. Please let me know if there is a better way to do this.
UserForm1.Show is showing the form modally - that means execution will only resume with the next instruction after the form has closed, and this implies If UserForm1.Visible = True will always be False... if you're lucky.
If the user closes the modal form with the red [x] button, then the object gets destroyed, and If UserForm1 re-spawns a new one, and that instance will not be visible either (because it was never shown in the first place).
You could try showing the form modeless:
UserForm1.Show vbModeless
Now execution will resume in this scope immediately after showing the form... making the condition UserForm1.Visible always True.
Remove UserForm1.Show from the BeforeSave handler: that handler's job is to cancel saving if the form isn't visible - not to unconditionally display that form! - you're getting this "can't show modally" error, because you're trying to show a modal form that's already modally displayed.
Read up on userforms and default instances on my Rubberduck blog.
Related
I have researched several questions and answers, but was unable to solve my problem.
I have a workbook that when it opens then a UserForm is shown, let's call it the main form.
On the main form there are several controls, one specific checkbox when clicked and it's value is changed to TRUE then it opens another UserForm, let's call this one the secondary form.
This secondary form is a simple password prompt form, and it's code name is pswdPrompt.
The main form code name is usf_TDM.
Now the code inside the pwsdPrompt is the following:
Option Explicit
Private Sub btn_input_Click()
Call passwordSubmit
Me.Hide
End Sub
Private Sub UserForm_Initialize()
With usf_TDM
Me.Top = .Top + .Height / 2
Me.Left = .Left + .Width / 2
End With
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
Call closePswdForm
End If
End Sub
The called subs passwordSubmit and closePswdForm are inside a module:
Public Sub passwordSubmit()
Application.enableEvents = False
With pswdPrompt
If .txt_password.Value = unblockPassword Then
usf_TDM.c_unblockForced.Value = True
Else
usf_TDM.c_unblockForced.Value = False
End If
.txt_password.Value = ""
End With
Application.enableEvents = True
End Sub
Public Sub closePswdForm()
pswdPrompt.Hide
End Sub
When I get to the pswdPrompt.Hide command, the secondary form dissapears BUT after inserting a breakpoint on the QueryClose event of the main form, I discovered that after the secondary form dissapears somehow the code continues to the QueryClose event of the main form even if no code called for the closure of the main form.
Furthermore, upon getting there the CloseMode variable of the event equals 1, which according to the documentation it means that "The Unload statement is invoked from code."
However at no point no Unload command was used.
So I'm unable to understand what is going on, and also this disrupt the purpose of the main form also.
Can anyone decipher what is going on here please?
UPDATE
After reading through the information, I have changed the code.
But before changing the code, I noticed that the problem I had dissapeared even though the default instances were being used, so I'm thinking it was some other context error.
I have however changed the code to use explicit instances of each user form.
I didn't use encapsulation nor composition as suggested on the rubberduck article because the software I wrote isn't that big and doesn't need to be scalable.
I find myself now with another problem, I have been searching for a way to identify each instance of each user form.
So far, and this is the wrong way to do it, I have been using the vba.Userforms with the index.
Is there another way to identify each instance?
I have an Excel utility that reads/writes tag values to and from a PLC. Some of the tag values are Boolean, therefore range checking is very important. I created a Userform_Termiate() event that first calls a range checking sub, and then updates the sheet that houses the actual data for the form. My question is, is there anyway I can cancel the termination of the user form if the input is not in range? I will post what I have now, which just tells the user the input is not in range, cancels the update for the sheet, but then still closes the form.
Private Sub Userform_Terminate()
Call CheckNumericRange
If Check = True Then
UpdateC1Sheet
Else
Exit Sub
End If
End Sub
UserForm_Terminate event occurs on closure of the userform. For example, if you close (Manually)/ unload(With procedure) the userform, the following procedure will give the message box.
Private Sub UserForm_Terminate()
MsgBox "You closed the useform"
End Sub
So, if you want to keep the userform open while UpdateC1Sheet is running, then first confirm the public boolean variable Check variable is set to True in some other macro. .. OR .. If it is set to True in CheckNumericRange procedure, you can also add in the same procedure after it is set to True -:-
If Check = True Then
UpdateC1Sheet
End If
Rather than the Terminate event, from my understanding of your intentions, you'd be better off using the QueryClose event.
Straight from the MS documentation online;
QueryClose event:
Occurs before a UserForm closes.
Terminate event:
Occurs when all references to an instance of an object are removed from memory by setting all variables that refer to the object to Nothing or when the last reference to the object goes out of scope.
So in layman's terms, say you click the red X close button or have some close my userform CommandButton on your form, the QueryClose event fires which has a Cancel part - allowing you to catch the close operation and decide if you want to do cancel the close event or not. If you don't cancel the close opearation in the QueryClose event, the form is unloaded from memory and thus the Terminate event is fired.
You could change your code to the following, to evaluate Check and either cancel closing the form or not.
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Call CheckNumericRange
If Check = True Then
UpdateC1Sheet
Else
Cancel = 1 '1 = True
End If
End Sub
Excel allows to start with a Modeless form and then display a Modal (but not the other way around)
I have an app with 4 Userforms : UF1 - Select a partner, UF2 - List existing transactions, UF21 - Display an existing transaction, UF22 - Make a new transaction. UF21 and UF22 both stem from UF2.
UF21 needs to be Modeless in order to display more than one transactions and compare side by side, therefore UF1, UF2 and UF21 are all Modeless. But I want UF22 to be Modal in order to issue one new transaction at a time.
My problem is that after I close UF22, even just ESCaping from the form right off the bat, all previous forms close. I should be able to return to UF2. If I make UF22 Modeless all is ok.
I have written a function to traverse the UserForms Collection and I am able to get a reference to the object of the Form I want to activate. So, I am able to return (in debug mode) to UF2 which is a listbox, activate the list box, but after the last pending statement both UF2 and UF1 close.
Is what I am trying to do impossible due to the nature of the Modal and Modeless forms or should I keep pushing for the correct code?
Since my original question is still open and my tested implementation of the proposed solution by #PeterT is not working properly, I include the code I have for the moment, based on #PeterT 's suggestion.
'===============
' Form UF1
'===============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'============
' Form UF2
'============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'----------------
Private Sub Cbn_OpenUF22_Click()
If ActivateUF22() = True Then
Exit Sub
Else
With New UF22
.Show vbModeless
End With
End If
End Sub
'================
' In a Module...
'================
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
Else
ActivateUF22 = False
End If
End Function
Well I finally managed to get the workaround to behave.
The remaining problem was the fact that clicking twice in a row on the same userform, besides the "Modal" one, would succeed and allow the user to break out.
I even tried the "AppActivate Application.caption" approach found in another SO thread but that didn't work either.
The only solution that works and does not bother me is to insert a MsgBox with a warning to the user, as such:
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
MsgBox("You cannot move away from this form until it is either completed or cancelled")
Else
ActivateUF22 = False
End If
End Function
Displaying the MsgBox does the trick internally, switches the focus to a different form from the one clicked and, upon return, the UserForm.Activate event fires normally and the ActivateUF22 function prevents the user from escaping the Pseudo-Modal form.
Thanks #PeterT for pointing me to a workaround. I managed to do what I set out to do, albeit in a different manner.
PS I still believe that there is a way to switch from a Modeless form to a Modal one. After all the MsgBox I use is obviously a Modal form and works just as I would like ;-)
I have code that incorporates business logic for saving documents (Excel 365) to ensure proper naming convention, file locations etc etc as a Sub Workbook_BeforeSave
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
Cancel = True ''Cancels the Save from the button push or Ctrl+S
Application.EnableEvents = False
'' code code code
Application.EnableEvents = True
End Sub
The problem is that if the file is opened as Read-Only (as most will be) Excel will prompt that "the file is Read-Only" (pic a) and go to the Save As screen in the File Ribbon (pic b). The Workbook_BeforeSave sub won't kick in until the SAVE button is pressed. It also won't move off this screen even after the sub has run.
Is there any way to either:
Get in front of the Read-Only prompt ... or
Write some code to move off the Save As screen?
MS Read-Only promt (pic a)
Save As Screen (pic b)
Huge thanks in advance!
This is not a perfect approach but try this.
This will cancel the save completely even after pressing the Save button, you can add your own save code.
Edit: Just realized that the code below does not stop the Read Only Alert, but when you click Save it cancels the Save and Closes the Save As Menu. However, when they use Ctrl+S the Send Keys {ESC} triggers a Ctrl+ESC which opens the Star Menu...
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
'Disable Read Only Alert
Application.DisplayAlerts = False
'Disable Events
Application.EnableEvents = False
'Cancel Save
Cancel = True
'Do whatever code you need in here
'
'Mark Workbook as Saved, allowing for the file to close without an alert even if not saved
ThisWorkbook.Saved = True
'Send Escape Key to leave Save As Menu
Application.SendKeys "{ESC}", 1
'Enable Events
Application.EnableEvents = True
'Enable Alerts
Application.DisplayAlerts = True
End Sub
Problem: I am building a Userform that has a 'Submit' and 'Cancel' button. I want the entire form to clear any entered data and close the form if the user hits the 'Cancel' button but I also am trying to build in the same functionality if the user hits the red 'X' in the top right corner. I'm unclear where I need to unload the form. I currently have it placed within the btnCancel_Click() method and I'm able to launch the form, enter some data and hit Cancel and it will close the form down.
But when I try to re-launch the form a 2nd time I get an error (I attached a picture of that message) that says
"Run-Time error '-2177418105 (80010007): Automation Error - The Callee (server [not server application]) is not available and disappeared; all connections are invalid. The call may have executed.
If I remove Unload Me from btnCancel_Click() then the form can close and re-open just fine, but any data I entered the first time will still be on the form and isn't cleared properly. I'm wondering if this is an Unload Me error or do I need to reset all form controls when I initialize the form?
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
' how was the form closed?
' vbFormControlMenu = X in corner of title bar
If CloseMode = vbFormControlMenu Then
' cancel normal X button behavior
Cancel = True
' run code for click of Cancel button
btnCancel_Click
End If
End Sub
'******************************************************************
Private Sub btnCancel_Click()
mbCancel = True
Me.Hide
Unload Me
End Sub
'*********************************************************************
Private Sub UserForm_Initialize()
'Populate values for 2 combo boxes
lastEmp = Sheets("Form_Ref").Cells(Rows.Count, 1).End(xlUp).Row
Me.cmbBoxEmpName.List = Sheets("Form_Ref").Range("A2:A" & lastEmp).Value
lastBld = Sheets("Form_Ref").Cells(Rows.Count, 2).End(xlUp).Row
Me.cmbBoxBuildingName.List = Sheets("Form_Ref").Range("B2:B" & lastBld).Value
End Sub
'******************************************************************
Public form As New CheckOutForm
Sub testFormOptions()
'Button pressed within Excel will start program and show the userform
form.Show
End Sub
This is the easiest quick and dirty solution:
Delete Public form As New CheckOutForm from the code. Then add it in the testFormOptions():
Sub testFormOptions()
Dim form As New CheckOutForm
form.Show
End Sub
Some not-that-good VBA books/tutorials would even go a bit like this, but this is brutal:
Sub testFormOptions()
CheckOutForm.Show
End Sub
Anyway, now the problem with the predefined values in the form is solved.
For the clean and not-so-easy solution, consider writing a MVC framework around the form:
https://codereview.stackexchange.com/questions/154401/handling-dialog-closure-in-a-vba-user-form
this blogpost (disclaimer - mine!), which pretty much says what the above link proposes, but it does not have the errors from the question.
the old StackOverflow tutorial for UserForms
If you execute Unload, you destroy the form object. With other words, your (global) variable form gets invalid and if you issue a new form.show, you get the runtime error.
When, on the other hand, you just unhide the form, the form-object stays valid (it's just currently not visible) and all controls keep their value.
Either you do some housekeeping by resetting all controls when a form is displayed (use the UserForm_Activate-event), or you have to create a new form-object every time you want to display it (I would strongly advice not to use the name form as variable name to avoid confusion).
Sub testFormOptions()
dim myForm as CheckOutForm
myForm.Show
End Sub