I have a Word userform with 60+ controls of varying types. I would like to evaluate the form every time a control_change event is triggered and change the enabled state of the form's submit button. However, I really don't want to write and maintain 60 on change event handlers.
You can create an event-sink class that will contain the event-handling code for all of your controls of a particular type.
For example, create the a class called TextBoxEventHandler as follows:
Private WithEvents m_oTextBox as MSForms.TextBox
Public Property Set TextBox(ByVal oTextBox as MSForms.TextBox)
Set m_oTextBox = oTextBox
End Property
Private Sub m_oTextBox_Change()
' Do something
End Sub
Now you need to create & hook up an instance of that class for each control of the appropriate type on your form:
Private m_oCollectionOfEventHandlers As Collection
Private Sub UserForm_Initialise()
Set m_oCollectionOfEventHandlers = New Collection
Dim oControl As Control
For Each oControl In Me.Controls
If TypeName(oControl) = "TextBox" Then
Dim oEventHandler As TextBoxEventHandler
Set oEventHandler = New TextBoxEventHandler
Set oEventHandler.TextBox = oControl
m_oCollectionOfEventHandlers.Add oEventHandler
End If
Next oControl
End Sub
Note that the reason you need to add the event handler instances to a collection is simply to ensure that they remain referenced and thus don't get discarded by the garbage collector before you're finished with them.
Clearly this technique can be extended to deal with other types of control. You could either have separate event handler classes for each type, or you could use a single class that has a member variable (and associated property & event handler) for each of the control types you need to handle.
In that case you have few options, because event handlers cannot be shared in VBA/VB6
Option 1: Use a central handling function which is called from every event handler.
Sub Control1_ChangeEvent()
CommonChangeEvent // Just call the common handler, parameters as needed
End Sub
Sub Control2_ChangeEvent()
CommonChangeEvent
End Sub
...
Sub CommonChangeEvent(/* Add necessary parameters */)
//Do the heavy lifting here
End Sub
Option 2: Organize your controls in control arrays.
Sub TextBox_ChangeEvent(Index As Integer)
CommonChangeEvent
End Sub
Sub OtherControlType_ChangeEvent(Index As Integer)
CommonChangeEvent
End Sub
Combining both options your total event handler count will shrink considerably and the remaining handlers are just brainless stubs for the one true event handler.
Related
I have a form in Access 2007, which has an "update" routine, that enables or disables certain textboxes based on values in other fields (textboxes, checkboxes, comboboxes). The regular operation of that routine works well.
Now I found that pressing ESC calls the undo function, that restores the original values in all fields. But this undo does not call the events on those fields, so the form is in a wrong state, where textboxes are disabled/enabled although they shouldn't.
I also found that there is an undo-event, but that is useless for me because it is called before undo. I need an event after undo. What can I do here to update the fields when ESC is pressed?
I like this solution more, because it works not only on the "ESC"-Key:
private Sub form_Undo(cancel as integer)
afterUndo = true
TimerInterval = 1
end Sub
private Sub Form_Timer()
if afterUndo then
'do something after the Undo-Event
end if
TimerInterval = 0
end Sub
Well, like many times before I have an idea for a solution after postion the question.
The solution here is enabling KeyPreview on the form and using the KeyUp event. The undo is called on KeyDown, so when KeyUp is raised, the form already has the restored values again and the update routine works.
Another solution to this problem is to use each control's OldValue property. Through experimentation, I've found that the three different value properties of controls come into play in different situations:
Control.Value or simply Control
The control's current value most of the time
When the control has focus, it is the value the control had before gaining focus
During a Form.Undo event, it is the value the control prior to the Undo
Relevant during the Control.AfterUpdate and Control.Undo events
Control.Text
The control's value while it has focus
Relevant during events that occur as the user types, such as Control.Change, Control.KeyUp, Control.KeyDown
Control.OldValue
The value the control had when the current record was first opened
Also the value that a form-level Undo will reset the control to
Relevant during the Form.Undo event
The timer-based answer to this question is my go-to solution, but if you're already handling events as the user types (e.g., for live validation), then code like this can be sensible:
Private Sub LastName_Change()
ValidateLastName SourceProperty:="Text"
End Sub
Private Sub LastName_Undo(Cancel As Integer)
ValidateLastName SourceProperty:="Value"
End Sub
Private Sub Form_Undo(Cancel As Integer)
ValidateLastName SourceProperty:="OldValue"
End Sub
Private Sub ValidateLastName(SourceProperty As Variant)
Dim LastName As String
Select Case SourceProperty
Case "LastName"
LastName = Nz(Me.LastName.Text, "")
Case "Value"
LastName = Nz(Me.LastName.Value, "")
Case "OldValue"
LastName = Nz(Me.LastName.OldValue, "")
Case Else
Debug.Print "Invalid case in ValidateLastName"
Exit Sub
End Select
' <Do something to validate LastName>
End Sub
Note that this method does not get you access to the post-Form-Undo value of Control.Column(x) of combo/list boxes. You can get it by using DLOOKUP on Control.OldValue, but you're probably better off just using the timer-based solution instead.
I've browsed the various questions already asked with this issue other users have faced and none of the solutions seem to fix the error code coming up.
I have a form which prompts the user for a reference number - they input this into the text field and then press OK
'OK button from form1
Public Sub CommandButton1_Click()
refInput = refTextBox.Value
InputRef.Hide
ExportShipForm.Show
End Sub
Once this has been pressed, the next form appears which I would like to be populated with data based on the reference number input on the first form. I have an update button which will update the "labels" on the form to show the data - this is where I am getting an error.
The first label to update is through a Vlookup:
Below the users clicks the update button the 2nd form:
Public Sub btnUpdate_Click()
Call ICS_Caption
lbl_ICS.Caption = Label_ICS
End Sub
This calls a function below:
Public Sub ICS_Caption()
Dim ws1 As Worksheet
refInput = InputRef.refTextBox.Value
Set ws1 = Worksheets("MACRO")
dataRef = Worksheets("Shipping Data").Range("A:K")
Label_ICS = WorksheetFunction.VLookup(refInput, dataRef, 7, False)
End Sub
The error continues to come up each time - I have ran the vlookup manually in a cell outside of VBA and it works fine.
I have typed the range in the Vlookup whilst also using named ranges but each variation shows the same error.
Eventually, I would want the label on form 2 to update with the result of the Vlookup.
Any ideas?
You need to Dim dataRef as Range and then Set it.
See code Below:
Dim DataRef as Range
Set dataRef = Worksheets("Shipping Data").Range("A:K")
Just like a Workbook or Worksheet you need to Set the Range
Just as Grade 'Eh' Bacon suggest in comments its always best to Dim every reference.
The best way to do so is to put Option Explicit all the way at the top of your code. This forces you to define everything which helps it preventing mistakes/typo's etc.
Update edit:
The problem was you are looking for a Reference number in your Sheet (thus an Integer) but refInput is set as a String this conflicts and .VLookup throws an error because it can't find a match.
I have reworked your code:
Your sub is now a function which returns the .Caption String
Function ICS_Caption(refInput As Integer)
Dim dataRef As Range
Set dataRef = Worksheets("Shipping Data").Range("A:K")
ICS_Caption = WorksheetFunction.VLookup(refInput, dataRef, 7, False)
End Function
The update Button calls your Function and provides the data:
Public Sub btnUpdate_Click()
lbl_ICS.Caption = ICS_Caption(InputRef.refTextBox.Value)
End Sub
By using a Function you can provide the Integer value and get a return value back without the need of having Global Strings or Integers.
Which would have been your next obstacle as you can only transfer Variables between Modules/Userforms by using a Global Variable.
I would even advice to directly use the function in the Initialize Event of your 2nd Userform to load the data before the Userform shows this is more user friendly then needing to provide data and then still needing to push an update button.
Verify that you have no missing libraries in VBA IDE > Tools > References
Try using a worksheet cell as the place to store and retrieve refTextBox.Value, rather than refInput (which I assume is a global variable):
Public Sub CommandButton1_Click()
...
Worksheets("Shipping Data").Range($M$1).Value=refTextBox.Value
End Sub
Public Sub ICS_Caption()
Dim refInput as Long'My assumption
...
refInput=Worksheets("Shipping Data").Range($M$1).Value
...
End Sub
Make sure you have Option Explicit at the top of all of your code windows.
OK, I've been out of Access programming for a couple of versions, but I could swear I used to be able to point controls at form global variables. Sample code as follows:
Option Compare Database
Option Explicit
Dim Testvar As String
Private Sub Form_Load()
Testvar = "Load"
End Sub
Private Sub Form_Open(Cancel As Integer)
Testvar = "open"
End Sub
Private Sub Text0_Click()
Testvar = "settest"
End Sub
I should be able to put a text box on the control that can see the TestVar variable, but the controls don't do it. Also, I used to be able to do that with the form's record source.
So, the questions -
Am I crazy - that was never possible?
Or have I forgotten how to address the form?
And then the most important question - what is the best way to get around this?
The most common way this is used is to pass in OpenArgs (record keys in this case) which is then parsed in to global vars and then a couple of controls display the open args and/or look up values to display from the keys.
I really hate to have to build routines that rebuild and load the record sources for the controls. Hope someone knows a better approach
In addition to your existing event procedures, you can add a function in the form module which retrieves the value of the Testvar module variable.
Function GetTestvar() As String
GetTestvar = Testvar
End Function
Then use =GetTestvar() as the Control Source for Text0.
You have to actually set the value of the text box. There's no way (to the best of my knowledge) to bind a text box to a variable.
Option Compare Database
Option Explicit
Private Sub Form_Load()
Text0.Value = "Load"
End Sub
Private Sub Form_Open(Cancel As Integer)
Text0.Value = "open"
End Sub
Private Sub Text0_Click()
Text0.Value = "settest"
End Sub
Of course, you could store the value in a variable and use it to set the value instead, but it makes little sense to do so in this simple example.
The TempVars collection is a feature introduced in Access 2007. So, if your Access version is >= 2007, you could use a TempVar to hold the string value. Then you can use the TempVar as the control source for your text box.
With =[TempVars]![Testvar] as the Control Source for Text0, the following event procedures do what you requested.
Private Sub Form_Open(Cancel As Integer)
TempVars.Add "Testvar", "Open"
End Sub
Private Sub Form_Load()
TempVars("Testvar") = "Load"
End Sub
Private Sub Text0_Click()
TempVars("Testvar") = "settest"
Me.Text0.Requery
End Sub
Note: [TempVars]![Testvar] will then be available throughout the application for the remainder of the session. If that is a problem in your situation, you could remove the TempVar at Form Close: TempVars.Remove "Testvar"
Requirement was: To show the login Id of the application user on all forms in the application.
Here is how I implemented this:
Create a module: module_init_globals
with the following code:
Option Compare Database
'Define global variables
Global GBL_LogonID as string
Option Explicit
Public Sub init_globals ()
'Initialize the global variables
'Get_Logon_Name is a function defined in another VBA module that determines the logon ID of the user
GBL_LogonID = Get_Logon_Name()
End Sub
On the main/first form - we need to call the module that will initialize the global variables:
In the code for "on open" event I have:
Private Sub Form_Open (Cancel as Integer)
call init_globals
End Sub
then on each of the forms in the app, I have a text control - txtUserID to display the logon id of the user
and I can set it's value in the "on open" event of the form.
txtUserID.value = GBL_LogonID
Is there a way to run a function in VBA the moment data in any control element changes? I've tried Form_AfterUpdate and Form_DataChange but they seem not to do anything
You do not have to code After Update/Change event of the controls, check out Key Preview
You can use the KeyPreview property to specify whether the form-level
keyboard event procedures are invoked before a control's keyboard
event procedures. Read/write Boolean.
Use it carefully.
For example, with KeyPreview on:
Private Sub Form_KeyPress(KeyAscii As Integer)
MsgBox "You pressed a key"
End Sub
Step 1: Create a function
Function DoStuff()
Call RunMySub
End Function
Step 2: Create a Macro (Named RunMyCode)
RunCode
Function Name DoStuff()
Step 3: Modify the Form_Load() sub
Private Sub Form_Load()
Dim cControl As Control
On Error Resume Next
For Each cControl In Me.Controls
if cControl.ControlType = 109 'this is for text boxes
'Depending on what your code does you can use all or some of these:
cControl.OnExit = "RunMyCode"
cControl.OnEnter = "RunMyCode"
cControl.OnLostFocus = "RunMyCode"
cControl.OnGotFocus = "RunMyCode"
If cControl.OnClick = "" Then cControl.OnClick = "RunMyCode"
end if
Next cControl
On Error GoTo 0
You can use any of the attributes from the control I find the pairs of 'OnExit/OnEnter' and 'OnLostFocus/OnGotFocus' to be the most effective. I also like 'OnClick' but I use the if statement to not overwrite actions (for buttons and stuff). There are a dozen other methods you can assign the control action to -- I'm sure you'll be able to find one/several that meet your goal.
Note -- I use the on error enclosure because I wrap this code around multiple different types of controls and not all have all of the methods.
I would like to poll a class and have it return all available subclasses in a way I can then address them. It can return an array, a dictionary, or something else. As long as I can then loop through the set of them and read properties or call functions from each.
Scenario:
I want to create a form where the user inputs the details of a series of events. Then I read the form and output a report. Each kind of event has a ".Name", but a different set of inputs (FormOptions) and methods (FormatOutput) to create an output. Right now this is implemented with a complex form and a script that runs after the user submits the form.
The trouble is that every time I add an option to the form, I have to change code in several different places. In the interest of making my code easier to maintain, I would like to contain all the code for each event type in a Class, then build the form based on the available Classes.
So as an example, I'm building an itinerary from a collection of Meeting and Travel objects:
Class Itinerary
Class Event
Public Property Get Name()
Name = "Meeting"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Meeting options</div>"
End Function
Public Sub FormatOutput(Time, Title, Location)
'Make a fancy meeting entry
End Sub
End Class
Class Travel
Public Property Get Name()
Name = "Travel"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Travel options</div>"
End Function
Public Sub FormatOutput(StartTime, EndTime, Origin, Destination)
'Make a fancy travel entry
End Sub
End Class
End Class
When the script runs it creates a form where the user can add a series of events. Each time the user chooses between "Meeting" and "Travel" then fills out the options for that event type. At the end, they push a button and the script makes a pretty document listing all the user's inputs.
At some point in the future, I will want to add a new kind of event: lodging.
Class Lodging
Public Property Get Name()
Name = "Lodging"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Lodging options</div>"
End Function
Public Sub FormatOutput(Date, Location)
'Make a fancy Lodging entry
End Sub
End Class
How do I setup my Itinerary class so that it automatically recognizes the new class and can return it as an available event type? I can think of several ways of doing this, but they all involve keeping an index of the available classes separate from the actual classes, and I'm trying to minimize the number of places I have to change code when I add new event types.
I am very purposely not including any details on how I build the form since at this point I'm open to anything. Also, please be gentle with references to "inheritance", "extensibility", or "polymorphism". I'm a just a scripter and these OOP concepts are still a bit foreign to me.
I don't think this is possible, but one way to come close to this would be to have a unique list of class names -- the simplest would be to have a global dictionary.
You can get a list of classes by reading the Keys collection of the dictionary.
I don't think VBScript supports references to classes, so when the user chooses one of the types use Eval to create an instance of the appropriate class.
Dim ClassList
Set ClassList = CreateObject("Scripting.Dictionary")
'on type selection:
Function GetItineraryInstance(className)
Set GetItineraryInstance = Eval("New " & className)
End Function
ClassList("Travel") = 1
Class Travel
'methods
End Class
At least the class registration can be kept together with the class definition.