MS Access 2007 & SQL: SELECT ... IN ('get', 'list', 'of', 'variables', 'from', 'form'") - sql

In a form a user makes choices using checkboxes.
Then VBA commands collect these choices into a string with comma separated numbers:
Dim choices As String
If (Me!choice1) Then
choices = "1,"
End If
If (Me!choice2) Then
choices = choices & "2,"
End If
Then these choices are saved in an invisible text field:
Me!choices.Value = choices (e.g. "1, 2, 5")
Then a report is open and it's source is a query:
SELECT ... FROM ... WHERE MyTable.MyVariable In (Forms!MyForm!choices) doesn't work.
If I manually write "1, 2, 5" in the query instead of Forms!MyPage!choices then it works.
Have also tried changing the string to other formats '1','2','5' and '1,2,5'
The reference to the variable is correctly spelled. So, how do I send this string to such a query?

Are you sure you removed the last comma in choices variable? It seems, based on the code you provided, that you have 1,2,5, instead of 1,2,5
Try to change this line:
Me!choices.Value = choices
into this solution:
Me!choices.Value = left(choices, len(choices)-1)

At the module level, Dim is equivalent to Private. As such, the expression Forms!MyForm!choices cannot see it. The 'quick fix' would be to change Dim to Public. However, a better solution would be to add a public accessor method. Further, there is no need to use an invisible text box - either just use a private string variable as the backing store, or don't cache the value at all. E.g.,
Option Explicit
Const MaxChoiceCount = 4 ' or whatever it actually is
Const ChoiceControlPrefix = "chkChoice" ' ditto
Private mChoices As String
Property Get Choices() As String
Choices = mChoices
End Property
Private Sub UpdateChoices()
Dim S As String, I As Integer
For I = 1 To MaxChoiceCount
If Me.Controls(ChoiceControlPrefix & I).Value Then S = S & I & ","
Next I
mChoices = Left$(S, Len(S) - 1)
End Sub
or
Option Explicit
Const MaxChoiceCount = 4 ' or whatever it actually is
Const ChoiceControlPrefix = "chkChoice" ' ditto
Function Choices() As String
Dim S As String, I As Integer
For I = 1 To MaxChoiceCount
If Me.Controls(ChoiceControlPrefix & I).Value Then S = S & I & ","
Next I
Choices = Left$(S, Len(S) - 1)
End Property

Related

Access variables and their names from the current scope

Is it possible to:
Access a list of all variables in a VBA runtime environment?
Access the name of a variable with VBA?
Example:
function v2S(str as string) as string
For each variable in Variables
dim I as integer
for I = 1 to 10
v2S = replace(v2S,"%" & variable.name & "%", variable.value)
next
next
end function
Example use case:
Dim skyColor as string
skyColor = "green"
Debug.Print v2S("The sky is %skyColor% today!")
There is an application I can send commands to via a com object and I wish to do something along the lines of:
Dim i a integer
for i = 1 to MI.Eval("numtables()")
MI = GetObject(,"MapInfo.Application.x64")
debug.print MI.Eval(v2S("tableinfo(%i%,1)")) ' Print name of table
next
The above looks much cleaner than:
Dim i a integer
for i = 1 to MI.Eval("numtables()")
MI = GetObject(,"MapInfo.Application.x64")
debug.print MI.Eval(v2S("tableinfo(" & i & ",1)")) ' Print name of table
next
But of course if it were possible I would want it to be general which may be difficult...
For my own use case this is pretty good.
However it still isn't very readable. This is another option. It's more readable but also more cluttered:
Sub Main()
Dim Vars as object, myString as string
set Vars = CreateObject("scripting.Dictionary")
Vars.add "Var1","Val1"
Vars.add "Var2","Val2"
'...
myString = r("Var1: #{Var1} and Var2: #{Var2}", Vars)
End Sub
function r(byval s as string, byval o as object) as string
for each key in o.keys
s = replace(s,"#{" & key & "}",o.item(key))
next
r = s
end function
I wish string interpolation functionality existed by default in VBA.

How can i check for a character after certain text within a listbox?

How can i check for a character after other text within a listbox?
e.g
Listbox contents:
Key1: V
Key2: F
Key3: S
Key4: H
How do I find what comes after Key1-4:?
Key1-4 will always be the same however what comes after that will be user defined.
I figured out how to save checkboxes as theres only 2 values to choose from, although user defined textboxes is what im struggling with. (I have searched for solutions but none seemed to work for me)
Usage:
Form1_Load
If ListBox1.Items.Contains("Key1: " & UsersKey) Then
TextBox1.Text = UsersKey
End If
Which textbox1.text would then contain V / whatever the user defined.
I did try something that kind of worked:
Form1_Load
Dim UsersKey as string = "V"
If ListBox1.Items.Contains("Key1: " & UsersKey) Then
TextBox1.Text = UsersKey
End If
but i'm not sure how to add additional letters / numbers to "V", then output that specific number/letter to the textbox. (I have special characters blocked)
Reasoning I need this is because I have created a custom save settings which saves on exit and loads with form1 as the built in save settings doesn't have much customization.
e.g Can't choose save path, when filename is changed a new user.config is generated along with old settings lost.
Look at regular expressions for this.
Using the keys from your sample:
Dim keys As String = "VFSH"
Dim exp As New RegEx("Key[1-4]: ([" & keys& "])")
For Each item As String in ListBox1.Items
Dim result = exp.Match(item)
If result.Success Then
TextBox1.Text = result.Groups(1).Value
End If
Next
It's not clear to me how your ListBoxes work. If you might find, for example, "Key 2:" inside ListBox1 that you need to ignore, you will want to change the [1-4] part of the expression to be more specific.
Additionally, if you're just trying to exclude unicode or punctuation, you could also go with ranges:
Dim keys As String = "A-Za-z0-9"
If you are supporting a broader set of characters, there are some you must be careful with: ], \, ^, and - can all have special meanings inside of a regular expression character class.
You have multiple keys, I assume you have multiple textboxes to display the results?
Then something like this would work. Loop thru the total number of keys, inside that you loop thru the alphabet. When you find a match, output to the correct textbox:
Dim UsersKey As String
For i As Integer = 1 To 4
For Each c In "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
UsersKey = c
If ListBox1.Items.Contains("Key" & i & ": " & UsersKey) Then
Select Case i
Case 1
TextBox1.Text = UsersKey
Case 2
TextBox2.Text = UsersKey
Case 3
TextBox3.Text = UsersKey
Case 4
TextBox4.Text = UsersKey
End Select
Exit For 'match found so exit inner loop
End If
Next
Next
Also, you say your settings are lost when the filename is changed. I assume when the version changes? The Settings has an upgrade method to read from a previous version. If you add an UpgradeSettings boolean option and set it to True and then do this at the start of your app, it will load the settings from a previous version:
If My.Settings.UpgradeSettings = True Then
My.Settings.Upgrade()
My.Settings.Reload()
My.Settings.UpgradeSettings = False
My.Settings.Save()
End If
Updated Answer:
Instead of using a listtbox, read the settings file line by line and output the results to the correct textbox based on the key...something like this:
Dim settingsFile As String = "C:\settings.txt"
If IO.File.Exists(settingsFile) Then
For Each line As String In IO.File.ReadLines(settingsFile)
Dim params() As String = Split(line, ":")
If params.Length = 2 Then
params(0) = params(0).Trim
params(1) = params(1).Trim
Select Case params(0)
Case "Key1"
Textbox1.Text = params(1)
Case "Key2"
Textbox2.Text = params(1)
End Select
End If
Next line
End If
You can associate text box with a key via its Name or Tag property. Lets say you use Name. In this case TextBox2 is associated with key2. TextBox[N] <-> Key[N]
Using this principle the code will look like this [considering that your list item is string]
Sub Test()
If ListBox1.SelectedIndex = -1 Then Return
Dim data[] As String = DirectCast(ListBox1.SelectedItem, string).Split(new char(){":"})
Dim key As String = data(0).Substring(3)
Dim val As String = data(1).Trim()
' you can use one of the known techniques to get control on which your texbox sits.
' I omit this step and assume "Surface1" being a control on which your text boxes sit
DirectCast(
(From ctrl In Surface1.Controls
Where ctrl.Name = "TextBox" & key
Select ctrl).First()), TextBox).Text = val
End Sub
As you can see, using principle I just explained, you have little parsing and what is important, there is no growing Select case if, lets say, you get 20 text boxes. You can add as many text boxes and as many corresponding list items as you wish, the code need not change.

Access VBA How can I filter a recordset based on the selections in a multi select list box?

I am trying to use the OpenForm function to filter based on the selections in a multi select list box. what is the correct syntax for this, or is there a better way to go about it? For the sake of example let's say:
List Box has options Ken, Mike, and Sandy.
Car has options Car1, Car2, and Car 3. All cars are owned by 1 or more people from that list box.
If someone from the list box is selected, I would like to open a form containing the cars owned by those people selected.
Thank you!
Ok So I figured out a way to do it:
Create a string to hold a query
Use a For loop to populate the string based on each item selected
Put that string as a filter in the OpenForm command.
Here is the specific code I used to to it. My example in the original post used Cars and People, but my actual context is different: Estimators and Division of Work are the filters. Let me know if you have any questions about it if you're someone who has the same question! Since it might be confusing without knowing more about what exactly I'm trying to accomplish.
Dim strQuery As String
Dim varItem As Variant
'query filtering for estimators and division list box selections
strQuery = ""
If Me.EstimatorList.ItemsSelected.Count + Me.DivisionList.ItemsSelected.Count > 0 Then
For Each varItem In Me.EstimatorList.ItemsSelected
strQuery = strQuery + "[EstimatorID]=" & varItem + 1 & " OR "
Next varItem
If Me.EstimatorList.ItemsSelected.Count > 0 And Me.DivisionList.ItemsSelected.Count > 0 Then
strQuery = Left(strQuery, Len(strQuery) - 4)
strQuery = strQuery + " AND "
End If
For Each varItem In Me.DivisionList.ItemsSelected
strQuery = strQuery + "[DivisionID]=" & varItem + 1 & " OR "
Next varItem
strQuery = Left(strQuery, Len(strQuery) - 4)
End If
Using the JOIN function for cleaner and safer code
When you find yourself repeatedly building incremental SQL strings with delimiters like "," "AND" "OR" it is convenient to centralize the production of array data and then use the VBA Join(array, delimiter) function.
If the interesting keys are in an array, a user selection from a multiselect listbox to build a SQL WHERE fragment for the form filter property could look like this:
Private Sub lbYear_AfterUpdate()
Dim strFilter As String
Dim selction As Variant
selction = ListboxSelectionArray(lbYear, lbYear.BoundColumn)
If Not IsEmpty(selction) Then
strFilter = "[Year] IN (" & Join(selction, ",") & ")"
End If
Me.Filter = strFilter
If Not Me.FilterOn Then Me.FilterOn = True
End Sub
A generic function to pick any column data from selected lisbok rows may look like this:
'Returns array of single column data of selected listbox rows
'Column index 1..n
'If no items selected array will be vbEmpty
Function ListboxSelectionArray(lisbox As ListBox, Optional columnindex As Integer = 1) As Variant
With lisbox
If .ItemsSelected.Count > 0 Then
Dim str() As String: ReDim str(.ItemsSelected.Count - 1)
Dim j As Integer
For j = 0 To .ItemsSelected.Count - 1
str(j) = CStr(.Column(columnindex - 1, .ItemsSelected(j)))
Next
ListboxSelectionArray = str
Else
ListboxSelectionArray = vbEmpty
End If
End With
End Function
A few array builders in the application library and coding can be made look more VB.NET

VBA string interpolation syntax

What is the VBA string interpolation syntax? Does it exist?
I would to to use Excel VBA to format a string.
I have a variable foo that I want to put in a string for a range.
Dim row as Long
row = 1
myString = "$row:$row"
I would like the $row in the string to be interpolated as "1"
You could also build a custom Format function.
Public Function Format(ParamArray arr() As Variant) As String
Dim i As Long
Dim temp As String
temp = CStr(arr(0))
For i = 1 To UBound(arr)
temp = Replace(temp, "{" & i - 1 & "}", CStr(arr(i)))
Next
Format = temp
End Function
The usage is similar to C# except that you can't directly reference variables in the string. E.g. Format("This will {not} work") but Format("This {0} work", "will").
Public Sub Test()
Dim s As String
s = "Hello"
Debug.Print Format("{0}, {1}!", s, "World")
End Sub
Prints out Hello, World! to the Immediate Window.
This works well enough, I believe.
Dim row as Long
Dim s as String
row = 1
s = "$" & row & ":$" & row
Unless you want something similar to Python's or C#'s {} notation, this is the standard way of doing it.
Using Key\Value Pairs
Another alternative to mimic String interpolation is to pass in key\value pairs as a ParamArray and replace the keys accordingly.
One note is that an error should be raised if there are not an even number of elements.
' Returns a string that replaced special keys with its associated pair value.
Public Function Inject(ByVal source As String, ParamArray keyValuePairs() As Variant) As String
If (UBound(keyValuePairs) - LBound(keyValuePairs) + 1) Mod 2 <> 0 Then
Err.Raise 5, "Inject", "Invalid parameters: expecting key/value pairs, but received an odd number of arguments."
End If
Inject = source
' Replace {key} with the pairing value.
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
Inject = Replace(Inject, "{" & keyValuePairs(index) & "}", keyValuePairs(index + 1), , , vbTextCompare)
Next index
End Function
Simple Example
Here is a simple example that shows how to implement it.
Private Sub testingInject()
Const name As String = "Robert"
Const age As String = 31
Debug.Print Inject("Hello, {name}! You are {age} years old!", "name", name, "age", age)
'~> Hello, Robert! You are 31 years old!
End Sub
Although this may add a few extra strings, in my opinion, this makes it much easier to read long strings.
See the same simple example using concatenation:
Debug.Print "Hello, " & name & "! You are " & age & " years old!"
Using Scripting.Dicitionary
Really, a Scripting.Dictionary would be perfect for this since they are nothing but key/value pairs. It would be a simple adjustment to my code above, just take in a Dictionary as the parameter and make sure the keys match.
Public Function Inject(ByVal source As String, ByVal data As Scripting.Dictionary) As String
Inject = source
Dim key As Variant
For Each key In data.Keys
Inject = Replace(Inject, "{" & key & "}", data(key))
Next key
End Function
Dictionary example
And the example of using it for dictionaries:
Private Sub testingInject()
Dim person As New Scripting.Dictionary
person("name") = "Robert"
person("age") = 31
Debug.Print Inject("Hello, {name}! You are {age} years old!", person)
'~> Hello, Robert! You are 31 years old!
End Sub
Additional Considerations
Collections sound like they would be nice as well, but there is no way of accessing the keys. It would probably get messier that way.
If using the Dictionary method you might create a simple factory function for easily creating Dictionaries. You can find an example of that on my Github Library Page.
To mimic function overloading to give you all the different ways you could create a main Inject function and run a select statement within that.
Here is all the code needed to do that if need be:
Public Function Inject(ByVal source As String, ParamArray data() As Variant) As String
Dim firstElement As Variant
assign firstElement, data(LBound(data))
Inject = InjectCharacters(source)
Select Case True
Case TypeName(firstElement) = "Dictionary"
Inject = InjectDictionary(Inject, firstElement)
Case InStr(source, "{0}") > 0
Inject = injectIndexes(Inject, CVar(data))
Case (UBound(data) - LBound(data) + 1) Mod 2 = 0
Inject = InjectKeyValuePairs(Inject, CVar(data))
Case Else
Err.Raise 5, "Inject", "Invalid parameters: expecting key/value pairs or Dictionary or an {0} element."
End Select
End Function
Private Function injectIndexes(ByVal source As String, ByVal data As Variant)
injectIndexes = source
Dim index As Long
For index = LBound(data) To UBound(data)
injectIndexes = Replace(injectIndexes, "{" & index & "}", data(index))
Next index
End Function
Private Function InjectKeyValuePairs(ByVal source As String, ByVal keyValuePairs As Variant)
InjectKeyValuePairs = source
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
InjectKeyValuePairs = Replace(InjectKeyValuePairs, "{" & keyValuePairs(index) & "}", keyValuePairs(index + 1))
Next index
End Function
Private Function InjectDictionary(ByVal source As String, ByVal data As Scripting.Dictionary) As String
InjectDictionary = source
Dim key As Variant
For Each key In data.Keys
InjectDictionary = Replace(InjectDictionary, "{" & key & "}", data(key))
Next key
End Function
' QUICK TOOL TO EITHER SET OR LET DEPENDING ON IF ELEMENT IS AN OBJECT
Private Function assign(ByRef variable As Variant, ByVal value As Variant)
If IsObject(value) Then
Set variable = value
Else
Let variable = value
End If
End Function
End Function
Private Function InjectCharacters(ByVal source As String) As String
InjectCharacters = source
Dim keyValuePairs As Variant
keyValuePairs = Array("n", vbNewLine, "t", vbTab, "r", vbCr, "f", vbLf)
If (UBound(keyValuePairs) - LBound(keyValuePairs) + 1) Mod 2 <> 0 Then
Err.Raise 5, "Inject", "Invalid variable: expecting key/value pairs, but received an odd number of arguments."
End If
Dim RegEx As Object
Set RegEx = CreateObject("VBScript.RegExp")
RegEx.Global = True
' Replace is ran twice since it is possible for back to back patterns.
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
RegEx.Pattern = "((?:^|[^\\])(?:\\{2})*)(?:\\" & keyValuePairs(index) & ")+"
InjectCharacters = RegEx.Replace(InjectCharacters, "$1" & keyValuePairs(index + 1))
InjectCharacters = RegEx.Replace(InjectCharacters, "$1" & keyValuePairs(index + 1))
Next index
End Function
I have a library function SPrintF() which should do what you need.
It replaces occurrences of %s in the supplied string with an extensible number of parameters, using VBA's ParamArray() feature.
Usage:
SPrintF("%s:%s", 1, 1) => "1:1"
SPrintF("Property %s added at %s on %s", "88 High St, Clapham", Time, Date) => ""Property 88 High St, Clapham added at 11:30:27 on 25/07/2019"
Function SprintF(strInput As String, ParamArray varSubstitutions() As Variant) As String
'Formatted string print: replaces all occurrences of %s in input with substitutions
Dim i As Long
Dim s As String
s = strInput
For i = 0 To UBound(varSubstitutions)
s = Replace(s, "%s", varSubstitutions(i), , 1)
Next
SprintF = s
End Function
Just to add as a footnote, the idea for this was inspired by the C language printf function.
I use a similar code to that of #natancodes except that I use regex to replace the occurances and allow the user to specifiy description for the placeholders. This is useful when you have a big table (like in Access) with many strings or translations so that you still know what each number means.
Function Format(ByVal Source As String, ParamArray Replacements() As Variant) As String
Dim Replacement As Variant
Dim i As Long
For i = 0 To UBound(Replacements)
Dim rx As New RegExp
With rx
.Pattern = "{" & i & "(?::(.+?))?}"
.IgnoreCase = True
.Global = True
End With
Select Case VarType(Replacements(i))
Case vbObject
If Replacements(i) Is Nothing Then
Dim Matches As MatchCollection
Set Matches = rx.Execute(Source)
If Matches.Count = 1 Then
Dim Items As SubMatches: Set Items = Matches(0).SubMatches
Dim Default As String: Default = Items(0)
Source = rx.Replace(Source, Default)
End If
End If
Case vbString
Source = rx.Replace(Source, CStr(Replacements(i)))
End Select
Next
Format = Source
End Function
Sub TestFormat()
Debug.Print Format("{0:Hi}, {1:space}!", Nothing, "World")
End Sub

Only one textbox value entered appears in listbox

Simple beginners issue here, go easy. I've got a few text boxes that the user can put values into + pick a date, and I want them to appear in a list box. Unfortunately only the 2nd text box's value appears multiple times. This can be seen here: http://i.stack.imgur.com/kCqrz.png
Here is the full form code: http://pastebin.com/MDb1hSCA
Here's where the data is added to an array:
stockArray(nofDataDay, lowValue) = possibleLow
stockArray(nofDataDay, highValue) = possibleHigh
stockArray(nofDataDay, openValue) = possibleOpen
stockArray(nofDataDay, closeValue) = possibleClose
dateArray(nofDataDay) = Convert.ToDateTime(WeatherDateTimePicker.Text)
nofDataDay = nofDataDay + 1
And here's where it's displayed:
For day = 0 To nofDataDay - 1
StockListBox.Items.Add(dateArray(day).ToShortDateString & _
delimiter & stockArray(day, openValue).ToString & _
delimiter & stockArray(day, closeValue).ToString & _
delimiter & stockArray(day, highValue).ToString & _
delimiter & stockArray(day, lowValue).ToString & _
delimiter & AverageStock(stockArray(day, lowValue), stockArray(day, highValue)))
Next
For some reason, it's only adding the Close value.
You never set the value of your column index variables (viz. openValue, closeValue, highValue, lowValue). They all default to zero, so you are in just adding the first column multiple times. You could set the value of them when you declare them, like this:
Dim lowValue As Integer = 0
Dim highValue As Integer = 1
Dim openValue As Integer = 2
Dim closeValue As Integer = 3
You'll need to declare your array larger too:
Dim stockArray(30, 3) As Integer
However, by default Dim declares fields as public, and since that's probably not what you really want, I would recommend changing them to private. Also, the column indexes really ought to be constants:
Private Const lowValue As Integer = 0
Private Const highValue As Integer = 1
Private Const openValue As Integer = 2
Private Const closeValue As Integer = 3
Private stockArray(30, 3) As Integer
However, this kind of bug would not be possible you designed your code better. Rather than using a two-dimensional array, I would recommend making a class that stores all the data for a single item. Then, rather than an array, use a List(T) object to store the list of items. For instance:
Public Class MyItem
Public Date As Date
Public LowValue As Integer
Public HighValue As Integer
Public OpenValue As Integer
Public CloseValue As Integer
End Class
Private myItems As New List(Of MyItem)()
Then, you can add the items like this:
Dim item As New MyItem()
item.Date = Convert.ToDateTime(WeatherDateTimePicker.Text)
item.LowValue = possibleLow
item.HighValue = possibleHigh
' ...
myItems.Add(item)
And then you can read the items from the list like this:
For Each item As MyItem in myItems
StockListBox.Items.Add(item.Date.ToShortDateString() & _
delimiter & item.OpenValue.ToString() & _
delimiter & item.CloseValue.ToString() & _
delimiter & item.HighValue.ToString() & _
delimiter & item.LowValue.ToString() & _
delimiter & AverageStock(item.LowValue, item.HighValue))
Next
As you can see, doing it that way is much more self-documenting, less confusing, and less bug-prone.