This is quite hard to explain so I will take it step by step.
Hello again John! This is starting to look fantastic! I'm not the fastest thinker you'll ever meet, however I now see this Worksheet-level VBA adjusts the SQL string itself from ? to whatever the range filling is and yes brackets were missing! Never seen that before, but just what the doctor ordered!
Yes, the code changes the SQL command text in the query definition from " IN (?)" (or whatever it is currently) to use the values in cell B1, putting apostrophes around each letter. The resultant SQLin string is, for example, " IN ('A','B','C')".
I could use this formula in B1:
Code:
=TEXTJOIN(", ",TRUE,M1:M4)
Apparently
TEXTJOIN is only available from Excel 2016 onwards and works similar to CONCAT, but also allows to add separators for IN-clause. Please see the screenshot for results.
TEXTJOIN is available in Excel 2016+, but only in the 365 subscription version. Yes, you could use that formula, although this one is better because you don't have to put apostrophes around each letter in M1:M4 (Excel uses the apostrophe at the start of cell value for a special purpose):
Code:
[INDENT]="'"&TEXTJOIN("', '",TRUE,M1:M4)&"'"
[/INDENT]
Or you could use this UDF (user-defined function) in a standard module:
Code:
Public Function JoinValues(CellsToJoin As Range, Optional Separator As String = ",") As String
Dim cell As Range
Dim result As String
result = ""
For Each cell In CellsToJoin
If Not IsEmpty(cell.Value) Then result = result & cell.Value & Separator
Next
JoinValues = Left(result, Len(result) - Len(Separator))
End Function
and this formula:
Code:
[INDENT]="'"&JoinValues(M1:M4,"','")&"'"
[/INDENT]
Could we therefore simplify the VBA so that:
1) take off the comma-adding procedure which I think happens in VBA at the moment?
OK - the new code below just reads the B1 cell value exactly as it is.
2) just check B1 range and forget all about M1:M4 (given that TEXTJOIN is looking there itself) - string in B1 should be already ready-made for the query?
The previous code looked at (in the Intersect function) cell B1 and M1:M4 because it gave you the choice of putting a formula in B1 or a string value. The new code below, which expects a formula in B1, must also look at (in the Intersect function) the cells which are referenced in the B1 cell formula, ie. M1:M4. This is because, as previously stated, when the result of a formula is changed the Worksheet_Change event is not triggered. So if the query parameter (cell B1) is set to 'Refresh automatically when cell value changes', although the B1 result changes as a result of you changing any cell M1:M4, and the query refreshes automatically, you will find that the rows returned are not correct for the values in B1. And if you look at the SQL command text of the query definition you will see that the SQL SELECT statement has not changed - the IN clause is still using the previous cell B1 values. You can test this issue by changing the Intersect line in the new code to:
Code:
If Not Intersect(Target, INvaluesCell) Is Nothing Then
and you will find that the code inside the If .... End If block is never executed, even though the formula result of B1 changes.
3) I'm a beginner at best in VBA and it's a bit difficult for me to read this code, but is it also adding ' to the beginning and end of each value? I am manipulating my M1:M4 range with Excel IF clause anyway (in live environment), I could add " ' " to the start and end of each value too! As on the other screenshot.
Yes, it is surrounding each value with a pair of apostrophes. Yes, you could add ' to the start and end of each value, but note the thing I mentioned above with apostrophes being a special character in Excel.
Any chance you or someone else could help me with adjusting the code? Although I'm already tremendeously thankful for the help!
See updated code below!
In addition to any possible simplifying of the VBA code, it looks like I have to use it for more than 1 query. They are all on the same worksheet though. Could this be possible too? I'm assuming this defines which query the code addresses: Set qt = Me.ListObjects(1).QueryTable
However should I duplicate this code on the Worksheet level and modify just the ListObjects(x) in each or is there a simple way to instead of 1, address ALL ListObjects on the sheet with single Sub?
That Set qt line references the first query on the sheet. The new code below loops through all the queries on the sheet.
Code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim INvaluesCell As Range
Dim SQLin As String
Dim i As Long, p1 As Long, p2 As Long
Dim qt As QueryTable
Set INvaluesCell = Range("B1")
If Not Intersect(Target, Range("M1:M4")) Is Nothing Then
For i = 1 To ListObjects.Count
Set qt = ListObjects(i).QueryTable
p1 = InStr(1, qt.CommandText, " IN (", vbTextCompare)
If p1 > 0 Then
p2 = InStr(p1, qt.CommandText, ")") + 1
SQLin = " IN (" & INvaluesCell.Value & ")"
qt.CommandText = Left(qt.CommandText, p1 - 1) & SQLin & Mid(qt.CommandText, p2)
MsgBox "Target cell changed: " & Target.Address & vbCrLf & vbCrLf & _
"Query destination cell: " & qt.Destination.Address & vbCrLf & vbCrLf & _
"New SQL command text:" & vbCrLf & vbCrLf & qt.CommandText, Title:="Worksheet_Change event"
End If
Next
End If
End Sub
The code includes a MsgBox showing details about the query. If the B1 query parameter is not set to 'Refresh automatically when cell value changes' then add the following line after the qt.CommandText line:
Code:
qt.Refresh BackgroundQuery:=True