calling a function using a variable - vba

In a variable I stored a function name, but when I try to call it from a sub() I've got some errors "Type mismatch", maybe I'm doing it wrong.
Public Sub test()
Dim list As Collection
Dim functionName As String
functionName = "asBuiltComplete()"
Set list = functionName
For Each rs In list
Debug.Print rs.getId & " " & rs.getActualDate & " " & rs.getBlDate
Next
End Sub

You can use Application.Run:
functionName = "asBuiltComplete"
Set list = Application.Run(functionName)
While you could use Eval, Application.Run is somewhat more limited in functionality so has less chance of weirdness.
However, this is bad code. You generally want to avoid dynamic function names whenever feasible, and just call the function.

Related

Updating and Existing OLEDB Connection using VBA

I am tying to update an existing connection in a spreadsheet with a modifiable script.
Basically, I want the use to be able to put in a list of Account References, push a button, and Excel spits out the SQL output where those Account References match what we have one system.
I've managed to collate all of the script into one cell (F1 on my Workings tab), so that's all fine, and I've added the preliminary connection (and called it "CustomerContactDetails").
I have enabled the Microsoft ActiveX Data Objects 6.1 Library as well.
My VBA script is:
Sub UpdateScript()
Dim Script As String
SQLScript = Worksheets("Workings").Range("F1").Value
ActiveWorkbook.Connections("CustomerContactDetails").OLEDBConnection.ConnectionString = SQLScript
ActiveWorkbook.Connections("CustomerContactDetails").Refresh
End Sub
I get the error on the fourth line down (ActiveWorkbook.Connections("CustomerContactDetails").OLEDBConnection.ConnectionString = SQLScript):
Run-time error '9';
Subscript out of range
Does anyone know how to help with this please? It feels like I'm not that far way, and I'm not trying to do anything too complicated. I just can't seem to get it to work!
Try something like this and see what output you get
Sub UpdateScript()
Dim Script As String
Dim conn as WorkbookConnection
For Each conn in ActiveWorkbook.Connections
Debug.Print conn.Name
If conn.Name = "CustomerContactDetails" Then
SQLScript = Worksheets("Workings").Range("F1").Value
conn.OLEDBConnection.ConnectionString = SQLScript
conn.OLEDBConnection.Refresh
End If
Next conn
End Sub
EDIT: looks like what you have is a WorkbookQuery
I created one (to an Oracle DB) and you can access and revise it something like this:
Sub Tester()
Dim q As WorkbookQuery, lo As ListObject
Set lo = ActiveSheet.ListObjects("Query1")
Debug.Print ActiveWorkbook.Queries.Count
Set q = ActiveWorkbook.Queries("Query1")
Debug.Print q.Formula
'revise query
q.Formula = "let" & vbCrLf & _
" Source = Oracle.Database(""BLAH"", [HierarchicalNavigation=true," & _
"Query=""select * from table1""])" & _
vbCrLf & "in" & vbCrLf & "Source"
lo.Refresh 'I got a popup "do you want to run this?" here...
End Sub
Note it's nearly always useful to try recording a macro while creating the type of object you're having trouble modifying - that will point you to the syntax you need.

Finding out if a one of several forms are open in MS Access

I need to iterate through a list of forms to see if they are open and then do something with them if they are. The following works:
Public Sub isloadedtester()
Dim iForm As Variant
For Each iForm In CurrentProject.AllForms
Debug.Print iForm.name & ": " & CurrentProject.AllForms(iForm.name).IsLoaded
Next
End Sub
but it loops through all forms. So I thought the following should work to loop through only the forms I care about:
Public Sub isloadedtester2()
Dim iForm As Variant
Dim list
list = Array(Form_some, Form_another)
For Each iForm In list
Debug.Print iForm.name
Debug.Print ".isloaded: " & CurrentProject.AllForms(iForm.name).IsLoaded
Debug.Print "direct: " & SysCmd(acSysCmdGetObjectState, acForm, iForm.name)
Debug.Print "by fn: " & IsLoaded(iForm.name)
Next
End Sub
Public Function IsLoaded(FormName As String, Optional aType As AcObjectType = acForm)
IsLoaded = (SysCmd(acSysCmdGetObjectState, aType, FormName) <> 0)
End Function
However, the second version always sees forms as loaded, no matter which of several ways to read loaded state I use:
.isloaded: True
direct: 1
by fn: True
It's almost like assigning a form to an array makes VBA load the form.
Should I use something other than an array for this? I realize I could do this as an array of names that I also loop through to see if one fits, but that seems awkward, so I first wanted to see if I am just doing something slightly different than it needs to be.
If you only want open forms, then use the Forms collection. It includes only open forms
Dim intFrm As Integer
If Forms.Count > 0 Then
For intFrm = 0 To Forms.Count - 1
'Debug.Print Forms(intFrm).NAME
If Forms(intFrm).Name IsInYourList Then
DoSomething
End IF
Next intFrm
End If
If you have a list of form names, you can do it the "old-fashioned" way with SysCmd
Public Function IsLoaded(FName As String, Optional aType As AcObjectType = acForm)
IsLoaded = (SysCmd(acSysCmdGetObjectState, aType, FName) <> 0)
End Function
and
For Each iForm In list
Debug.Print iForm.Name & ": " & IsLoaded(iForm.Name)
Next

Excel 2016 VBA - Compare 2 PivotTables fields for matching values

Hi please can someone help, Excel 2016 VBA PivotTable objects. I rarely develop in Excel VBA.
Overall goal:
Compare a single column [P_ID] value list from PivotTable2 against PivotTable1 if they exist or not to enable filtering on those valid values in PivotTable1.
I have some Excel 2016 VBA code which I have adapted from a previous answer from a different internet source.
Logic is: gather data from PivotTable2 from the ComparisonTable dataset (in PowerPivot model), field [P_ID] list of values. Generate a test line as input into function to test for existence of field and value in PivotTable1 against the Mastertable dataset, if true add the line as valid if not skip the line.
Finally filter PivotTable1 with the VALID P_ID values.
It works to a point until it gets to the bFieldItemExists function which generates an error:
Run-time error '1004'
Unable to get the PivotItems property of the PivotField class
Can someone please correct the way of this not working?
Private Sub Worksheet_PivotTableUpdate(ByVal Target As PivotTable)
Dim MyArray As Variant, _
ar As Variant, _
x As String, _
y As String, _
str As Variant
MyArray = ActiveSheet.PivotTables("PivotTable2").PivotFields("[ComparisonTable].[P_ID].[P_ID]").DataRange
For Each ar In MyArray
x = "[MasterTable].[P_ID].&[" & ar & "]"
If ar <> "" And bFieldItemExists(x) = True Then
If str = "" Then
str = "[MasterTable].[P_ID].&[" & ar & "]"
Else
str = str & "," & "[MasterTable].[P_ID].&[" & ar & "]"
End If
End If
Next ar
Dim str2() As String
str2 = Split(str, ",")
Application.EnableEvents = False
Application.ScreenUpdating = False
ActiveSheet.PivotTables("PivotTable1").PivotFields("[MasterTable].[P_ID].[P_ID]").VisibleItemsList = Array(str2)
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Function bFieldItemExists(strName As String) As Boolean
Dim strTemp As Variant
' This line does not work!?
strTemp = ActiveSheet.PivotTables("PivotTable1").PivotFields("[MasterTable].[P_ID].[P_ID]").PivotItems(strName)
If Err = 0 Then bFieldItemExists = True Else bFieldItemExists = False
End Function
The 1004 error occurred due to the use of square brackets [ ]. Remove those.
You also need to use the key word Set when you set an object equal to something. For example Set MyArray = ActiveSheet.PivotTables("PivotTable2").PivotFields("ComparisonTable.P_ID.[P_ID").DataRange.
If you don't use Set you will get a VBA run-time error dialog that says Run-time error '91': Object variable or With block variable not set
I cannot guarantee that my edits will completely solve your problem since I don't have your data set and cannot fully test your code. You will need to use the Debug mode in the VBA editor and single step through the code. To this set a breakpoint on the Set mDataRange = Active.... To set a breakpoint go to the Debug menu and choose the "Toggle Breakpoint" sub-menu item or you can press F9 to set the breakpoint.
Now when you make a change to the Pivot table, the Worksheet_PivotTableUpdate event will fire and the code will top execution at that point.
After the code stops executing due to the breakpoint, you can press the F8 key to single step through your code. If you want to resume execution to the next breakpoint you can press F5. Also when you get the VBA error dialog box, you can hit Debug and then use the F8 key to single step or use the debug windows to see what your variables and objects contain. I'm sure there are some good youtube videos on VBA debugging.
As you single step through the code, you can observe what each variable/object contains using the Immediate window, the Watches window and the Locals window. To open these windows, go to the menu item View and click on each of these sub-menu items.
Here's how you need to edit your code before debugging.
Option Explicit
Private Sub Worksheet_PivotTableUpdate(ByVal Target As PivotTable)
'Better practice is to not use the underscore character to
'continue a Dim declaration line
Dim mDataRange As Range
Dim ar As Range
Dim x As String
Dim y As String
Dim str As Variant
'Use Set to assign the object mDataRange a reference to the the right
'hand side of the equation. Remove the square brackets
'MyArray = ActiveSheet.PivotTables("PivotTable2").PivotFields("[ComparisonTable].[P_ID].[P_ID]").DataRange
Set mDataRange = ActiveSheet.PivotTables("PivotTable2").PivotFields("ComparisonTable.P_ID.P_ID").DataRange
For Each ar In mDataRange
'You need to specify what proprerty from ar you
'want to assign to x. Assuming the value stored in
'ar.Value2 is a string, this should work.
'We use value2 because it is the unformmated value
'and is slightly quicker to access than the Text or Value
'properties
'x = "[MasterTable].[P_ID].&[" & ar & "]"
x = "MasterTable.P_ID." & ar.Value2
'Once again specify the Value2 property as containing
'what value you want to test
If ar.Value2 <> "" And bFieldItemExists(x) = True Then
If str = "" Then
'Remove the square brackets and use the specific property
'str = "[MasterTable].[P_ID].&[" & ar & "]"
str = "MasterTable.P_ID." & ar.Value2
Else
'Remove the square brackets and use the specific property
'str = str & "," & "[MasterTable].[P_ID].&[" & ar & "]"
str = str & "," & "MasterTable.P_ID." & ar.Value2
End If
End If
Next ar
Dim str2() As String
str2 = Split(str, ",")
Application.EnableEvents = False
Application.ScreenUpdating = False
'Remove square brackets
'ActiveSheet.PivotTables("PivotTable1").PivotFields("[MasterTable].[P_ID].[P_ID]").VisibleItemsList = Array(str2)
ActiveSheet.PivotTables("PivotTable1").PivotFields("MasterTable.P_ID.P_ID").VisibleItemsList = Array(str2)
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Function bFieldItemExists(strName As String) As Boolean
'Declare a PivotItem to accept the return value
Dim pvItem As PivotItem
'Since you want to trap for an error, you'll need to let the VBA runtime know
'The following code is a pseudo Try/Catch. This tells the VBA runtime to skip
'the fact an error occured and continue on to the next statement.
'Your next statement should deal with the error condition
On Error Resume Next
'Use Set whenever assigning an object it's "value" or reference in reality
Set pvItem = ActiveSheet.PivotTables("PivotTable1").PivotFields("MasterTable.P_ID.P_ID").PivotItems(strName)
'Assuming that an error gets thrown when strName is not found in the pivot
'Err is the error object. You should access the property you wish to test
If Err.Number = 0 Then
bFieldItemExists = True
Else
bFieldItemExists = False
End If
'Return to normal error functioning
On Error GoTo 0
End Function
Finally, I realize that some of this should be in the comments section, but there was too much I needed to explain to help Learner74. BUT most importantly, I hope I helped him. I have used so many suggestions, recommendations and explanations from the VBA Stack Overflow exchange through the years, I just want to pay it back by paying it forward.
Additional USEFUL Links:
Chip Pearson is the go to site and person for all things VBA
Paul Kelly's Excel Macro Mastery is another go to site for Excel and VBA questions.
Microsoft Excel Object Model which is sometimes useful, but needs improvement. Too many of the objects lack examples, but can at least point you in the right direction.

Extract All Named Ranges Into A Class

I have a workbook with a very large amount of named ranges (well over 200). I really need a way to work quickly and easily with all of the named ranges so I can then work with / populate them using VBA.
My solution up until now has been to have code inside a bunch of get properties in my public NamedRanges module, to set the property equal to the named range, like so:
Public Property Get LotNumber49() As range
Set LotNumber49 = Common.GetRange(Strings.LotNumber49)
End Property
Where Strings.LotNumber49 is a property which contains the name of the named range as recorded in the workbook, and Common.GetRange is a method that returns a new instance of the desired range object.
While this solution works well (I can now access an instance of that named range by calling NamedRanges.LotNumber49) It is definitely time consuming and tedious to type up the property in the Strings class and another property in the NamedRanges class.
Is there a better way to accomplish this quick referencing of named ranges that anyone can think of? Perhaps iterating over the collection returned by the Workbook.Names property?
Thank you all, I have this workbook to work on as well as four others, which means a whole lot of named ranges!
Get Named Range by String
Why not a simple procedure like so:
Function GetNR(namedRange as String) as Range
Set GetNR = ActiveWorkbook.Names(namedRange).RefersToRange
End Function
Then simply get the named range like so:
Sub Example()
Debug.Print GetNR("NAME").Value
End Sub
Named Range Suggestion in VBA Project
Alternatively if you want the names to popup in your VBA project you need to redefine the Constants in the Strings class. Try this procedure:
Sub GetAllNames()
Dim res As String, n As Name
For Each n In ActiveWorkbook.Names
If InStr(n.Name, "!") = 0 Then res = res & "Const " & n.Name & "=""" & n.Name & """" & vbNewLine
Next n
Dim fFile As Long
fFile = FreeFile
Open "out.txt" For Output As #fFile
Print #fFile, res
Close #fFile
End Sub
You need to repeat this occasionally when modifying the named ranges:
Run the GetAllNames procedure
Open the out.txt file
Copy the outputs to your Strings class or whatever
Now to get a named range use your Common.GetRange method along with your Strings name or simply use the approach above to generate also the Getter code like so:
Sub GetAllGetters()
Dim res As String, n As Name
For Each n In ActiveWorkbook.Names
If InStr(n.Name, "!") = 0 Then res = res & "Public Property Get " & n.Name & "() As range" & vbNewLine & "Set " & n.Name & " = Common.GetRange(Strings." & n.Name & ")" & vbNewLine & "End Property"
Next n
Dim fFile As Long
fFile = FreeFile
Open "outGetters.txt" For Output As #fFile
Print #fFile, res
Close #fFile
End Sub

Module variables don't survive CodeModule.InsertLines call

I am trying to add a button to my worksheet during run-time. This button should just display different worksheet that is also created on during run-time. I added button like this:
Dim btnShowTable
Set btnShowTable = ActiveSheet.Buttons.Add(rowRange.Left + 10, rowRange.Top + 10, rowRange.Width - 20, rowRange.Height - 20)
btnShowTable.Caption = "Show table data"
btnShowTable.OnAction = AddClickHandler_ShowSheet("ClickModule", "TableView", tableSheet)
Function AddClickHandler_ShowSheet(ByVal moduleName As String, ByVal btnName As String, ws As Worksheet)
Dim methodName As String
methodName = btnName & "_" & AddClickHandler_GetId() & "_Click"
Dim LineNum As Long
Dim VBCodeMod As CodeModule
Set VBCodeMod = ThisWorkbook.VBProject.VBComponents(moduleName).CodeModule
With VBCodeMod
LineNum = .CountOfLines + 1
.InsertLines LineNum, _
"Sub " & methodName & "()" & Chr(13) & _
" " & ws.CodeName & ".Select" & Chr(13) & _
"End Sub"
End With
AddClickHandler_ShowSheet = moduleName & "." & methodName
End Function
Function that creates the button is in one module while AddClickHandler_ShowSheet is in another.
The idea is that I would have separate module that would contain all these click handlers so that I could easily delete all of them.
This works ok. The handlers are created and buttons work as expected. The issue that I have is that, when this InsertLines method is called, all of my module variables in a module that contains the function for button creation are lost.
I have 4 module variables
Dim xmldoc As New MSXML2.DOMDocument
Dim xmlDataMap() As DataNode
Dim xmlDataMapLast As Integer
Dim xmlTables As Collection
after a call to InsertLines all of them became empty except for xmlDataMapLast which contains correct value of 14.
If I try to debug this method when I step over InserLines call I get an error "Can't enter break mode at this time." and I can't debug anything until my function ends. If I comment out the call to AddClickHandler_ShowSheet my variables remain intact, so it must be something related to that call.
Am I trying to achieve the impossible or am I just doing it the wrong way?
It's no surprise the module is reset.
In fact, I would expect value types to reset, too. It's a bit of surprise
they survive.
Why would you need to store variables in the module you use for code generation?
Store them in a separate module and only use this module for code generation.
Having that said, why would you dynamically add code in the first place?
OnAction supports parameters:
Sub asdff()
Worksheets(1).Buttons(1).OnAction = "'Module1.ParametrizedHandler 5, ""Hi there""'"
End Sub
Public Sub ParametrizedHandler(ByVal foo As Long, ByVal bar As String)
MsgBox foo, vbInformation, bar
End Sub
Note the absence of parentheses in the call string, and the single quotes around it.