UDF - Identify if it's a recalculation or first execution - excel

I want to program a UDF to query a access db. I'm wondering if it's possible to have 2 different Workflows, depending whether it's a first execution or only a recalculation of the UDF.
Ideally I would have a UDF that you can feed the primary key of the db and the UDF presents an overview of possible values of the access db table. If it's a recalculation I don't want to have a userform popup again. Is this possible at all? Can somebody point me in the right direction?
Thx!
Edit
An attempt to show some (dummy) code:
public function key_from_table(primarykey as string) as string
' Read-out column names from Access table for userform
' Trigger userform with possible column names and let user choose
' readout Chosen column names
key_from_table = Call get_from_db(Primary_key, column_names)
end function
Function get_from_db(Primarykey as string, column_names as string) as string
'call Access db and readout result
end Function
If a recalculation is triggered the userform popup comes up again
I'm still new to Excel vba - pls tell me if this is rather stupid :)

Declares a global dictionary variable. Just before you trigger the form, check if dictionary already has the column name. If it does, don't trigger the form. If it doesn't, trigger the form and add the column name to dictionary once form is closed. You can clear the variable in Workbook_BeforeClose just to be clean

Something like this should work for you:
Public pub_sRecalcCheck As String
Public Function MyTest() As Boolean
Dim bReCalc As Boolean
If InStr(1, " " & pub_sRecalcCheck & " ", " " & Application.Caller.Address(External:=True) & " ", vbTextCompare) = 0 Then
'This is a brand new calculation
'Add this cell to the public variable storing where calculations for this UDF have occurred
bReCalc = False
pub_sRecalcCheck = WorksheetFunction.Trim(Replace(pub_sRecalcCheck, " " & rCell.Address(External:=True) & " ", " "))
''''''''''''''''''''''''''''''''''''''''''''
' '
' Your code here for new calculation '
' '
''''''''''''''''''''''''''''''''''''''''''''
Else
'This is a recalculation
bReCalc = True
''''''''''''''''''''''''''''''''''''''''''
' '
' Your code here for recalcuations '
' '
''''''''''''''''''''''''''''''''''''''''''
End If
MyTest = bReCalc
End Function
EDIT: And just in case the formula gets deleted from the cell, use this in the ThisWorkbook module to clear that cell's address from the RecalcCheck public string variable so that if a new formula is put there it is treated as a new calculation:
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
Dim rCell As Range
For Each rCell In Target.Cells
'Check if cell has been deleted/cleared
If IsEmpty(rCell) Then
'Found deleted cell, check if it is stored for the recalc checks
If InStr(1, " " & pub_sRecalcCheck & " ", " " & rCell.Address(External:=True) & " ", vbTextCompare) > 0 Then
'It is stored, remove it so that if formula is put back it is treated as a new calculation
pub_sRecalcCheck = WorksheetFunction.Trim(Replace(pub_sRecalcCheck, " " & rCell.Address(External:=True) & " ", " "))
End If
End If
Next rCell
End Sub

Related

Cannot access range item of For Each loop in UserForm VBA

So I have a UserForm that is triggered with the entry of a string in an InputBox. The user the selected from the list of populated ListBox items and presses a command button. This is supposed to redirect to the corresponding sheet in my workbook that contains the selection. To achieve this, I have to loop through the cells in a range defined in the For Each loop. I have done something similar many times before, with nearly identical loops, but for whatever reason, cell in the code below is Empty when I run the code, so I get error when I get to here: rosterSh.Rows(stuAddr & ":" & stuAddr).Select What's going on?
Private Sub OK_Click()
With Application
.ScreenUpdating = False
.DisplayAlerts = False
End With
' Set sheet to current sheet
Dim rosterSh As Worksheet, rosterShName As String
Dim stuCount As Integer, lastStuRow As Integer
Dim selectedStu As String, stuAddr As String
If ListBox1.ListIndex < 0 Then
MsgBox "You did not make a selection. Please make a selection or press " & Chr(34) & "Cancel" & Chr(34) & " to continue.", vbExclamation, "Alert"
Exit Sub
Else
selectedStu = ListBox1.List(ListBox1.ListIndex)
rosterShName = Replace(Split(selectedStu, "[")(1), "]", "") & " Roster"
Set rosterSh = ThisWorkbook.Sheets(rosterShName)
rosterSh.Activate
' Find selection on sheet, activate sheet and highlight student
stuCount = rosterSh.Cells(Rows.Count, "A").End(xlUp).row - 1
lastStuRow = stuCount + 1
Dim cell As Variant
For Each cell In rosterSh.Range("A2:A" & lastStuRow)
If InStr(cell.Value, Trim(Left(selectedStu, InStr(selectedStu, "[")))) > 0 Then
Debug.Print "cell is: " & cell
stuAddr = Split(cell.Address, "$")(1)
End If
Next cell
rosterSh.Rows(stuAddr & ":" & stuAddr).Select
End If
Call unloadUserForm9
UserForm9.Hide
With Application
.ScreenUpdating = True
.DisplayAlerts = True
End With
End Sub
EDIT: apologies for not being more clear about the error. I get a Run-time error '13': Type mismatch, which doesn't happen with nearly identical loops I have used.

Update cell with a value when form field is selected

I have an excel workbook with modeless form. The way it's setup is that: each sheet in the workbook has a tab in the form. Each field in these tabs is Linked to a cell in corresponding sheet. So when a value is changed/updated in the form, it is automatically updated in the relevant cell. The way I am doing this is by using the onChange event for each filed which call's a UDF that does the updating. My question, there are a lot of fields in the form and lots more to be added. Is there a way to update relevant cell when a field in the form is selected without having to add the call to a UDF in onChange event for each field?
I have tried using things like ControlSource but that only one way where it just updates the value in the form but doesn't update the value in the cell when form is updated.
As a side note, unfortunately I cannot share the form or the sheet but am willing to answer any questions
EDIT
Below is the function that updates the field:
Sub UpdateWorksheetValue(ByVal oObj As Object)
Dim oWS As Worksheet
Dim sCurrentValue As String
Dim iC As Long
' Lets check if tag is set
If Len(Trim(oObj.Tag)) = 0 Then
MsgBox "Empty tag found for '" & oObj.Name & "' field. Failed to update field value" & vbCrLf & vbCrLf & "Please contact system administrator with this information", vbCritical + vbOKOnly, "Update Failed"
Exit Sub
ElseIf Len(Trim(Mid(oObj.Tag, InStr(1, oObj.Tag, "¬") + 1))) = 0 Then
MsgBox "Tag for '" & oObj.Name & "' field does not include page title. Failed to update field value" & vbCrLf & vbCrLf & "Please contact system administrator with this information", vbCritical + vbOKOnly, "Update Failed"
Exit Sub
End If
' Set worksheet
Select Case LCase(Trim(Mid(oObj.Tag, InStr(1, oObj.Tag, "¬") + 1)))
Case "client identification"
Set oWS = oWB.Worksheets("Client Identification - Output")
Case "request details"
Set oWS = oWB.Worksheets("Request Details - Output")
Case "db responsible individuals"
Set oWS = oWB.Worksheets("DB Responsible Ind - Output")
Case "additional details"
Set oWS = oWB.Worksheets("Additional Details - Output")
End Select
' Set value
With oWS
' Lets check if tag is set
If Len(Trim(Mid(oObj.Tag, 1, InStr(1, oObj.Tag, "¬") - 1))) = 0 Then
MsgBox "Tag for '" & oObj.Name & "' field does not include corresponding cell information. Failed to update field value in '" & oWS.Name & "' worksheet" & vbCrLf & vbCrLf & "Please contact system administrator with this information", vbCritical + vbOKOnly, "Update Failed"
Exit Sub
End If
' Set the search value
.Range("Z1").Value = Mid(oObj.Tag, 1, InStr(1, oObj.Tag, "¬") - 1)
DoEvents
' If a row with tag text is not found, throw a message and exit sub
If Len(Trim(.Range("Z2").Value)) = 0 Then
MsgBox "Unable to find corresponding cell for '" & oObj.Name & "' field in '" & .Name & "' worksheet. Failed to update field value" & vbCrLf & vbCrLf & "Please ensure that the field's 'Tag' matches a cell in the sheet or contact system administrator", vbCritical + vbOKOnly, "Update Failed"
Exit Sub
End If
' Set field value
Select Case LCase(TypeName(oObj))
Case "textbox", "combobox"
.Range("B" & .Range("Z2").Value).Value = oObj.Value
Case "optionbutton"
If oObj.Value = True Then
.Range("B" & .Range("Z2").Value).Value = oObj.Caption
Else
.Range("B" & .Range("Z2").Value).Value = ""
End If
Case "listbox"
' First lets the current cell value
sCurrentValue = .Range("B" & .Range("Z2").Value).Value
' Now lets build the string for the cell
For iC = 0 To oObj.ListCount - 1
If oObj.Selected(iC) And InStr(1, sCurrentValue, oObj.List(iC)) = 0 Then
sCurrentValue = sCurrentValue & "/" & oObj.List(iC)
ElseIf Not oObj.Selected(iC) And InStr(1, sCurrentValue, oObj.List(iC)) > 0 Then
sCurrentValue = Replace(sCurrentValue, "/" & oObj.List(iC), "")
End If
Next
' And finally, set the value
.Range("B" & .Range("Z2").Value).Value = sCurrentValue
End Select
End With
' Clear object
Set oWS = Nothing
End Sub
EDIT 2
I now have a class called formEventClass as suggested by David. Contents of the class are:
Option Explicit
Public WithEvents tb As MSForms.TextBox
Private Sub tb_Change()
UpdateWorksheetValue (tb)
End Sub
But when I make a change in any given text box, cells are not updated (as per David's suggestion, I've removed the call to UpdateWorksheetValue in text box onChange event. Cells are not updated even when I tab out of the field. As this is working for David, I suspect I am missing something here
If you want to get fancy using WithEvents...
Create a Class Module and name it tbEventClass. Put the following code in this module.
Option Explicit
Public WithEvents tb As MSForms.TextBox
Private Sub tb_Change()
Call UpdateWorksheetValue(tb)
End Sub
This defines a custom class (tbEventClass) which is responsive to the events of it's tb property which is a TextBox. You'll need to map your textboxes to instances of this class during the form's Initialize event:
Public textbox_handler As New Collection
Private Sub UserForm_Initialize()
Dim ctrl As Control, tbEvent As tbEventClass
For Each ctrl In Me.Controls
If TypeName(ctrl) = "TextBox" Then
Set tbEvent = New tbEventClass
Set tbEvent.tb = ctrl
textbox_handler.Add tb
End If
Next
End Sub
Important: You will either need to remove or modify the Change event handlers in the UserForm module to avoid duplicate calls to the "update" procedure. If the only thing going on in those event handlers is the call to your update macro, just get remove the event handlers entirely, they're fully represented by the tbClass. If those events contain other code that does other stuff, just remove or comment out the line(s) that call on your update function.
Update:
This is working for me with the controls within a MultiPage and required ZERO changes to the implemented code above.

Create a VBA version of dictionaries with 2 values per key

I am trying to make my excel macro dynamic. The excel macro essentially looks at only 2 columns, one which contains the name and the other contains the numeric part. I have my macro working perfectly, the only problem is that it is hard coded when I created the program. In my code, I hard coded the name in column 2 and the numeric part in column 3. However, that is not the case in real life. The name and numeric data could appear in column 1 and 5, for example. I've been manually rearranging the data in the columns so that it fits into what hard coded. However, I want to make this process dynamic and less manual work for the user.
There are 5 different versions of spreadsheets this macro will be used on and in each spreadsheet, the name and number columns are different. I am looking to make a user form box of some sort, where the user selects "Vendor XYZ" and since Vendor XYZ always sends their data sheets the same way I know that Vendor XYZ's name column is 2 and number is 4. So I was thinking that the dictionary would be something in the form of {Vendor XYZ: 2,4} (where the first number is the name column and the second number is the numeric columnnumber...I know the syntax is wrong)
I think my work around this would be to hard code the different vendors and then use if statements ( I haven't tried it yet)
I will have a user input/dropdown box of 5 different vendors. Then something like
If userinput="A"
then namecol=2 and numcol=1
If userinput="B"
then namecol="3" and numcol="4"
I don't know if that would even work. The problem with that is that the number of vendors is small now, but will be scaling up and I can't do that if we have 100 or 1000 vendors.
Any ideas?
Depending on how your initial dataset is retrieved, you can use something like this:
Public Function GetHeaderIndices(ByVal InputData As Variant) As Scripting.Dictionary
If IsEmpty(InputData) Then Exit Function
Dim HeaderIndices As Scripting.Dictionary
Set HeaderIndices = New Scripting.Dictionary
HeaderIndices.CompareMode = TextCompare
Dim i As Long
For i = LBound(InputData, 2) To UBound(InputData, 2)
If Not HeaderIndices.Exists(Trim(InputData(LBound(InputData, 1), i))) Then _
HeaderIndices.Add Trim(InputData(LBound(InputData, 1), i)), i
Next
Set GetHeaderIndices = HeaderIndices
End Function
This Function takes an array as an input and gives the user a dictionary with the indices of the headers from the input.
If you are smart (and I say this because too many users just don't use tables) you will have your data in a table, and you will have named that table. If you did, you could do something like this:
Sub DoSomething()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
End Sub
So, if you data looked like this:
Foo Baz Bar
1 Car Apple
3 Van Orange
2 Truck Banana
The function would give you a dictionary like:
Keys Items
Foo 1
Baz 2
Bar 3
Then your subroutines could do something like this:
Sub DoEverything()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
DoSomething(MyData)
End Sub
Sub DoSomething(ByRef MyData as Variant)
Dim HeaderIndices as Scripting.Dictionary
Set HeaderIndices = GetHeaderIndices(MyData)
Dim i as Long
' Loop through all the rows after the header row.
For i = LBound(MyData, 1) + 1 to Ubound(MyData, 1)
If MyData(i, HeaderIndices("Baz")) = "Truck" Then
?MyData(i, HeaderIndices("Foo"))
?MyData(i, HeaderIndices("Baz"))
?MyData(i, HeaderIndices("Bar"))
End If
Next
End Sub
This does require a reference to Scripting.Runtime so if you don't want to add a reference you will need to change any reference to As Scripting.Dictionary to As Object and any New Scripting.Dictionary to CreateObject("Scripting.Dictionary").
Alternatively, I use the following code module to take care of adding references programmatically for all my users:
Public Sub PrepareReferences()
If CheckForAccess Then
RemoveBrokenReferences
AddReferencebyGUID "{420B2830-E718-11CF-893D-00A0C9054228}"
End If
End Sub
Public Sub AddReferencebyGUID(ByVal ReferenceGUID As String)
Dim Reference As Variant
Dim i As Long
' Set to continue in case of error
On Error Resume Next
' Add the reference
ThisWorkbook.VBProject.References.AddFromGuid _
GUID:=ReferenceGUID, Major:=1, Minor:=0
' If an error was encountered, inform the user
Select Case Err.Number
Case 32813
' Reference already in use. No action necessary
Case vbNullString
' Reference added without issue
Case Else
' An unknown error was encountered, so alert the user
MsgBox "A problem was encountered trying to" & vbNewLine _
& "add or remove a reference in this file" & vbNewLine & "Please check the " _
& "references in your VBA project!", vbCritical + vbOKOnly, "Error!"
End Select
On Error GoTo 0
End Sub
Private Sub RemoveBrokenReferences()
' Reference is a Variant here since it requires an external reference.
' It isnt possible to ensure that the external reference is checked when this process runs.
Dim Reference As Variant
Dim i As Long
For i = ThisWorkbook.VBProject.References.Count To 1 Step -1
Set Reference = ThisWorkbook.VBProject.References.Item(i)
If Reference.IsBroken Then
ThisWorkbook.VBProject.References.Remove Reference
End If
Next i
End Sub
Public Function CheckForAccess() As Boolean
' Checks to ensure access to the Object Model is set
Dim VBP As Variant
If Val(Application.Version) >= 10 Then
On Error Resume Next
Set VBP = ThisWorkbook.VBProject
If Err.Number <> 0 Then
MsgBox "Please pay attention to this message." _
& vbCrLf & vbCrLf & "Your security settings do not allow this procedure to run." _
& vbCrLf & vbCrLf & "To change your security setting:" _
& vbCrLf & vbCrLf & " 1. Select File - Options - Trust Center - Trust Center Settings - Macro Settings." & vbCrLf _
& " 2. Place a checkmark next to 'Trust access to the VBA project object model.'" _
& vbCrLf & "Once you have completed this process, please save and reopen the workbook." _
& vbCrLf & "Please reach out for assistance with this process.", _
vbCritical
CheckForAccess = False
Err.Clear
Exit Function
End If
End If
CheckForAccess = True
End Function
And I have the following command in each Workbook_Open event (less than ideal, but only good solution I have so far)
Private Sub Workbook_Open()
PrepareReferences
End Sub

VBA macro which writes a new macro from string on the fly

Is it possible to create Excel VBA macro from a string variable?
Suppose we have FirstMacro:
Sub FirstMacro()
Dim MyString
MyString = "Sub SecondMacro()" & Chr(13) & Chr(10) & "MsgBox " & Chr(34) & "Hello" & Chr(34) & Chr(13) & Chr(10) & "End Sub"
Debug.Print MyString
'Here be code that magicly creates SecondMacro
End Sub
Running the macro, I want to create SecondMacro which is stored in VBA string variable. The second macro can be created either below in the same module or in a new module.
So the second macro from string looks like this:
Sub SecondMacro()
MsgBox "Hello"
End Sub
Sure is possible. It should be noted that you can't add/delete from the module you're running code in.
This will append the code at the end of the module. If you can avoid this though you should, I only use it for adding code to buttons that I've added programatically.
With Workbooks(ThisWorkbook.Name).VBProject.VBComponents("MyModuleHere").CodeModule
.InsertLines .CountOfLines + 1, "Sub... End Sub"
End With
So to add to the "MyModuleHere" code module (assuming you have a module named that), drop this in:
Sub addcode()
Dim subtext As String
subtext = "Sub PrintStuff" & vbCrLf & "msgbox ""Hello World""" & vbCrLf & "End Sub"
With Workbooks(ThisWorkbook.Name).VBProject.VBComponents("MyModuleHere").CodeModule
.InsertLines .CountOfLines + 1, subtext
End With
End Sub
As usual, CPearson adds some really useful insight:
http://www.cpearson.com/excel/vbe.aspx
With regard to removing code, which I think you're hinting at in your comment, I use the below function to find a sub name, and remove it (this assumes that I will know the length of the sub):
Function ClearModule(strShapeName As String)
Dim start As Long
Dim Lines As Long
Dim i As Variant, a As Variant
With Workbooks(ThisWorkbook.Name).VBProject.VBComponents("MyModuleHere").CodeModule
For i = .CountOfLines To 1 Step -1
If Left(.Lines(i, 1), 8 + Len(strShapeName)) = "Sub " & strShapeName & "_Cli" Then
.DeleteLines i, 6
End If
Next
End With
End Function
Here you have more or less all variations which, hopefully, will solve your problem. To test this code copy all of it in a normal code module (by default "Module1") Rename it as "Remin" and write "FirstMacro" in cell A1 of the worksheet you activate, a number in cell A2. Then run the first of the following procedures directly from the VBE window.
Sub SelectMacroToRun()
' 04 Apr 2017
Dim MacroName As String
Dim Arg1 As String
Dim Outcome As Long
With ActiveSheet
MacroName = .Cells(1, 1).Value
Arg1 = .Cells(2, 1).Value
End With
On Error Resume Next
Outcome = Application.Run(ActiveSheet.name & "." & MacroName, Arg1)
If Err Then
MsgBox "The macro """ & MacroName & """ wasn't found", _
vbInformation, "Error message"
Else
If Outcome <> xlNone Then MsgBox "Outcome = " & Outcome
End If
End Sub
Private Function FirstMacro(Optional ByVal Dummy As String) As Long
MsgBox "First Macro"
FirstMacro = xlNone
End Function
Private Function SecondMacro(Arg1 As Long) As Long
MsgBox "Second Macro" & vbCr & _
"Argument is " & Arg1
SecondMacro = Arg1 * 111
End Function
The code will run the FirstMacro, reading the name from the worksheet. Change that name to "SecondMacro" to call the second macro instead. The second macro requires an argument, the first only accepts it and does nothing with it. You don't need to pass any argument, but this code shows how to pass (as many as you want, comma separated) and it also shows how to ignore it - the argument is passed to a dummy variable in the FirstMacro, and the function also returns nothing.
Application.Run "Remin" & MacroName, Arg1
Would just run the macro (it could be a sub). Omit the argument if you don't want to pass an argument. "Remin" is the name of the code sheet where the called macro resides. This name could be extended to include the name of another workbook. However, if the called macro isn't in the same module as the caller it can't be Private.

Excel VBA call function with variable name

I'm trying to call a function with a variable name that is generated at run time based upon a combo box value. This is straightforward in most languages but I can't seem to figure it out in Excel VBA, I suspect this is because I don't really understand how the compiler works. I've found several posts that are close but don't quite seem to do the trick. The code below is wrong but should give an idea of what I want.
Thanks
Sub main()
'run formatting macros for each institution on format button click
Dim fn As String
Dim x As Boolean
'create format function name from CB value
fn = "format_" & CBinst.Value
'run function that returns bool
x = Eval(fn)
...
End Sub
CallByName is what you'll need to accomplish the task.
example:
Code in Sheet1
Option Explicit
Public Function Sum(ByVal x As Integer, ByVal y As Integer) As Long
Sum = x + y
End Function
Code is Module1 (bas module)
Option Explicit
Sub testSum()
Dim methodToCall As String
methodToCall = "Sum"
MsgBox CallByName(Sheet1, methodToCall, VbMethod, 1, 2)
End Sub
Running the method testSum calls the method Sum using the name of the method given in a string variable, passing 2 parameters (1 and 2). The return value of the call to function is returned as output of CallByName.
You should write a function that accepts the CB value as a parameter and then uses a select case to call the appropriate formatting function.
Something similar to this
Function SelectFormatting(Name as String) As Boolean
Select Case CBinst.Value
Case "Text1":
SelectFormatting = Text1FormattingFunction()
Case "Text2":
.
.
.
End Select
End Function
The above will work but not with a large number of names
Use Application.Run(MacroName, Parameters)
You have to may sure that there is a macro but it is better than the above as there is no select statement.
With respect to my answer above you might also find this useful to check whether the macro exists
'=================================================================================
'- CHECK IF A MODULE & SUBROUTINE EXISTS
'- VBA constant : vbext_pk_Proc = All procedures other than property procedures.
'- An error is generated if the Module or Sub() does not exist - so we trap them.
'---------------------------------------------------------------------------------
'- VB Editor : Tools/References - add reference TO ......
'- .... "Microsoft Visual Basic For Applications Extensibility"
'----------------------------------------------------------------------------------
'- Brian Baulsom October 2007
'==================================================================================
Sub MacroExists()
Dim MyModule As Object
Dim MyModuleName As String
Dim MySub As String
Dim MyLine As Long
'---------------------------------------------------------------------------
'- test data
MyModuleName = "TestModule"
MySub = "Number2"
'----------------------------------------------------------------------------
On Error Resume Next
'- MODULE
Set MyModule = ActiveWorkbook.VBProject.vbComponents(MyModuleName).CodeModule
If Err.Number <> 0 Then
MsgBox ("Module : " & MyModuleName & vbCr & "does not exist.")
Exit Sub
End If
'-----------------------------------------------------------------------------
'- SUBROUTINE
'- find first line of subroutine (or error)
MyLine = MyModule.ProcStartLine(MySub, vbext_pk_Proc)
If Err.Number <> 0 Then
MsgBox ("Module exists : " & MyModuleName & vbCr _
& "Sub " & MySub & "( ) : does not exist.")
Else
MsgBox ("Module : " & MyModuleName & vbCr _
& "Subroutine : " & MySub & vbCr _
& "Line Number : " & MyLine)
End If
End Sub
'-----------------------------------------------------------------------------------

Resources