I'm creating a user form (with SAVE button that writes to Excel sheet and RETURN button that goes back to a splash page) and want to set/check a flag if there are unsaved changes on the form. I'm getting different functionality of the code depending on whether a breakpoint is set or not.
Here is the relevant portion of code:
Public saveFlag as Boolean
Private Sub UserForm_Initialize()
initializeCMAValues Me
initializeTeamValues Me, getRowNum("CMA", "Team", False)
' getRowNum is a function that returns the row to read from
' in this case, the row with name=CMA, type=Team, createRow=False
saveFlag = False
End Sub
Private Sub btn_Return_Click()
' Check for unsaved data
If saveFlag Then
If MsgBox("You have unsaved information on the form. Do you wish to save it?", vbYesNo, "Unsaved Data") = vbYes Then
btn_Save_Click
End If
End If
' Return to splash form
Unload Me
Form_Splash.Show
End Sub
If I enter the form and select RETURN, I get the msgbox prompt. If I set a breakpoint at the saveFlag = False line, enter form, select RETURN (invoking breakpoint), continue running, then it runs as I would expect (i.e., no msgbox).
Instead of a public saveFlag variable, I've tried writing the flag to a non-visible label on the form; I've also tried writing to a cell on the Excel sheet. Both gave the same results of this different behavior with and without the breakpoint.
I've also run the code with the initializeTeamValues line commented out and then the code runs fine. I've triple checked that this subroutine only fills values into the textboxes on the form, nothing else.
Some research turned up a vague answer related to OLE pushing values onto the stack. I then tried putting the "saveFlag = False" line at the beginning of the subroutine. Same results.
Ideas of why the varying behavior? Is there a better/alternate way to check for unsaved data?
Related
I have data in a textbox (call it info) on a userform that I would like the user to be able to change so I set up a info_change() subroutine. Problems are:
(1) as I load initial data into info.text from VBA, the info_change() subroutine is called.
(2) when I go into the info field to change the value INSIDE the info field, it call the info_change() subroutine again and continues to call the routine until the last entry I put in info field = value in info field before changing in (seems recursive)
Any thoughts? Maybe instead of calling it info_change(), call it another procedural event?
Thanks, Marty
Easy example using _AfterUpdate event
Instead of reacting each time a single part of the info textbox got changed by the control's _Change event, it's more efficient to use the _AfterUpdate event pausing till you changed to another control (btw could be easily augmented by a CommandButton procedure).
In order to prevent unnecessary calls a comparison is made between old and current info text using a type definition in the code module's declaration head. Furthermore I added a boolean IsTest variable to this definition to allow a test display of changes made triggering some other stuff in code (see b).
Additional hint: there's no recursion, the Change event or AfterUpdate event are only firing as reaction to each single change or update, i.e. making no difference if triggered via Userform_Initialize settings or an eventual user input, however the condition check prevents the Init from getting active.
Declaration head of Userform code module
Option Explicit ' declaration head of code module
'User defined type
Private Type TThis
cnt As Long
oldTxt As String
IsTest As Boolean
End Type
Dim this As TThis
Private Sub UserForm_Initialize()
'[1]assign text to Info-Control
Me.TextBox1.Text = "bla bla"
'[2]remember text
this.oldTxt = Me.TextBox1.Text
'[3]optional choice of test modus
this.IsTest = True
If this.IsTest Then Debug.Print " #", "Change history", "(new) text" & vbNewLine; 0, Now, this.oldTxt
End Sub
Private Sub TextBox1_AfterUpdate()
'~~~~~~~~~~~~~~~
'~> Example call
'~~~~~~~~~~~~~~~
check Me.TextBox1
End Sub
Sub check(myTextbox As MSForms.TextBox)
' Purp.: compare current text with old text
If this.oldTxt <> myTextbox.Text Then
'a) optional test display
If this.IsTest Then this.cnt = this.cnt + 1: Debug.Print this.cnt, Now, myTextbox.Text
'b) do other stuff
' ...
'c) remember old text
this.oldTxt = myTextbox.Text
End If
End Sub
Hey, I have an UserForm into which an user inputs varius profile
fields.
Once it's filled in, there is verification - if anything goes astray,
the CommandButton, named save_button is disabled
What I want to achieve is: If user clicks on the button, whilst it is in the disabled state, to display a MsgBox saying he needs to correct the incorrectly filled in field
For Demonstration purposes, I'm not gonna paste here the validation procedures, so let's just pretend that the save_button.Enabled = False is set from the getgo. Produces the same result.
save_button.Enabled = False ' already ran before, pretend this executes it
Private Sub save_button_Click()
If save_button.Enabled = False Then
MsgBox "Clicked disabled button"
End If
End Sub
Issue is, once a CommandButton is set to .Enabled = False then it can no longer be officially clicked (hence it can't even trigger the Click() procedure)
My next thought was to use the MouseUp as a substitute. Issue is,
this triggers on any miniscule movement over the button and I don't want to bombard the user with MsgBoxes
Can you think of any alternatives, as to how to detect if user clicked the disabled button?
When a control is disabled, the click event bubbles up the tree. In your case, I guess the user form will get the click instead. If you put your save button inside a frame, that will get the click if the button is disabled. It is fairly easy to make the frame invisible, by setting
Caption to ""
BorderStyle to fmBorderStyleNone
SpecialEffect to fmSpecialEffectFlat
And then size the frame so that is the same size as the button.
The code is easy:
Private Sub YourNewFrame_Click()
MsgBox "Save button disabled!"
End Sub
Tip: If you draw the frame, cut your button and paste it inside your new frame, it will be placed properly. Properly, as in in the right part of the hierarchy. Visually, you will have to do manually.
I want to offer another approach based on my comment to your question.
In the below gif, you will see that the 'Create Form' does not become enabled until all the necessary information is provided. You don't see it in the gif so much, but each entry that requires validation also has it behind the scenes in code.
The essential code behind that action is this:
Sub checkFields()
Select Case True
Case Len(Me.formTitleLine1) = 0, Len(Me.formPrefix) = 0, Len(Me.formNumber) = 0, Len(Me.formProduct) = 0, Len(Me.formEditionMonth) = 0, Len(Me.formEditionYear) = 0
Me.createForm.Enabled = False
Case Else
Me.createForm.Enabled = True
End Select
End Sub
And it's called on the afterUpdate event of each relevant box:
Private Sub formEditionYear_AfterUpdate()
checkFields
End Sub
With one small change to the checkFields sub, you can only show the save button once everything has been filled in correctly. That change would be:
Me.createForm.Visible
instead of
Me.createForm.Enabled
Hello wonderful VBA community,
I'm still really new to vba and am trying to learn a lot. Thank you in advance for looking through my code and my description of the issue I'm facing.
I have a button on a page that calls a new Userform.
CODE SNIPPET 1:
Sub btnShowDetails_Click()
Call frmShowDeets.ShowDeets
End Sub
... which calls the next bit of code in the 'frmShowDeets' UserForm:
CODE SNIPPET 2:
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
... triggering:
CODE SNIPPET 3:
Private Sub UserForm_Initialize()
Dim comboBoxItem As Range
For Each comboBoxItem In ContactList.Range("tblContactList[CompanyName]")
'^refers to unique values in a named range
With Me.boxCompanySelection
.AddItem comboBoxItem.Value
End With
Next comboBoxItem
End Sub
So at this point, the form I want to display has values loaded in its one combobox for user selection. The user selects a company and the Combobox_Change event triggers other routines that pull information for that company.
CODE SNIPPET 4:
Public Sub boxCompanySelection_Change()
Call frmShowDeets.PullData
End Sub
Sub PullData()
Dim numCompanies As Long
numCompanies = ContactList.Range("B6").Value 'this holds a count of the rows in the named range
Dim FoundCell As Range
Set FoundCell = ContactList.Range("tblContactList[Company Name]").Find(What:=boxCompanySelection.Text, LookIn:=xlValues, LookAt:=xlWhole)
Dim CompanyRow As Long
CompanyRow = FoundCell.Row
With ContactList
'pull a bunch of the company's details
End With
End Sub
Here is where it gets weird... Once the form is shown and the user selects one of the combo box items, triggering the Combobox_Change event the code breaks because the 'What:=boxCompanySelection.Text' part of the Range().Find method reads as "" empty (even though Code Snippet 3 is meant to load in company names and Code Snippet 4 is only triggered when the user selects one of those company names from the combobox) and I shouldn't need to build something to handle 'not found' exceptions since the only possible values should be the ones pulled in from my named range.
From stepping through the code, I have determined that for some reason, Code Snippets 2 and 3 run TWICE before Snippet 4 is run. Does anyone know what about my code is causing this to happen? I'm thinking there's a disconnect between the form that is shown and loaded with combobox values and whatever Code Snippet 4 is reading data from.
What is weirder is that if I run the code starting from Code Snippet 2 (ignoring the button call in Code Snippet 1), the form works as intended and from what I can tell 2 and 3 are only run once.
The problem is probably something simple I'm overlooking but I just cannot figure out what it is. Thanks again!
You have to understand that a form is an object - exactly as any other class module, except a form happens to have a designer and a base class, so UserForm1 inherits the members of the UserForm class.
A form also has a default instance, and a lot of tutorials just happily skip over that very important but rather technical bit, which takes us exactly here on Stack Overflow, with a bug involving global state accidentally stored on the default instance.
Call frmShowDeets.ShowDeets
Assuming frmShowDeets is the name of the form class, and assuming this is the first reference to that form that gets to run, then the UserForm_Initialize handler of the default instance runs when the . dot operator executes and dereferences the object. Then the ShowDeets method runs.
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
That line triggers UserForm_Initialize on the local instance named frm - which is an entirely separate object, of the same class. The Initialize handler runs whenever an instance of a class is, well, initialized, i.e. created. The Terminate handler runs when that instance is destroyed.
So ShowDeets is acting as some kind of "factory method" that creates & shows a new instance of the frmShowDeets class/form - in other words whatever happened on the default instance is irrelevant beyond that point: the object you're working with exists in the ShowDeets scope, is named frm, and gets destroyed as soon as it goes out of scope.
Remove the ShowDeets method altogether. Replace this:
Call frmShowDeets.ShowDeets
With this:
With New frmShowDeets
.Show
End With
Now the Initialize handler no longer runs on the default instance.
What you want, is to avoid using the default instance at all. Replace all frmShowDeets in the form's code-behind, with Me (see Understanding 'Me' (no flowers, no bees)), so that no state ever accidentally gets stored in the default instance.
Call frmShowDeets.PullData
Becomes simply:
Call Me.PullData
Or even:
PullData
Since Call is never required anywhere, and the Me qualifier is always implicit when you make a member call in a class module's code.
I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.
I'm trying to figure out how to disable a button within my userForm if a certain cell within my spreadsheet equals a certain number. I tried the code stated below, but it isn't working.
Private Sub UserForm_Initialize()
Label2 = Sheets("DATA").Range("AM2").Value
Label4 = Sheets("DATA").Range("AO2").Value
Label7 = Format(Sheets("DATA").Range("R8").Value, "Currency")
If Sheets("DATA").Range("AL10").Value = 10 Then
ActiveSheet.Shapes("CommandButton1").Select
UserFormact_Upgrade.CommandButton1.Enabled = False
Else
End If
End Sub
Your code should be working, as you're on the right path.
To test it, simply create a new form and add this code, you'll see it should work. Maybe you're having problems within the IF clause?
Besides, you don't need to select the shape prior to disabling it; just disable it right away.
Private Sub UserForm_Initialize()
CommandButton1.Enabled = False
End Sub
I know this is old, but got to this thread trying to solve my problem, and found a solution that wasn't mentioned here. So in case someone gets here like I did, and this didn't quite get them where they needed to go, I thought this might help.
I had a userform with a drop down box called cmdADAMFields, and I didn't want my submit button called FieldsSubmitButton to be enabled until I selected something from the dropdown box.
I had to break up my argument into two different private subs vs one larger If-Then-Else statement.
First, I put:
Private Sub UserForm_Activate()
If cmbADAMFields.ListIndex = -1 Then FieldsSubmitButton.Enabled = False
End Sub
Then when for my pulldown's private sub when it's value changed I wrote:
Private Sub cmbADAMFields_Change()
FieldsSubmitButton.Enabled = True
End Sub
The proper place for setting Enabled property is in Activate event (associated with Show method) and not Initialize event (associated with Load instruction).
The below code disable the button CommandButton1 when AL10 cell >= 10.
Private Sub UserForm_Activate()
CommandButton1.Enabled = ( Sheets("DATA").Range("AL10") < 10 )
End Sub
For buttons you can choose between normal buttons (property Enabled=False and property Visible=true), disabled buttons (property Enabled=False and property Visible=true) and invisible buttons (property Enabled=False and property Visible=False), that it is a cleaner interface, in most cases.
Concerning text boxes, besides normal, disabled and invisible status, there is a locked status, that is enabled and visible, but cannot be user edited. (property Locked = True)
A locked control only can be changed by VBA code. For instance, someone can includes date text boxes, that it's filled using a secondary popup date form with Calendar control.