Convert list (of string) from datareader to string - arrays

I've captured some data from a datareader into a list and now I'm wondering what's the best way to put these out into strings.
The requirement I have is I need to grab field names from a table and then out put these as headings on a page.
objConn = New SqlConnection(strConnection)
objConn.Open()
objCmd = New SqlCommand(strSQL, objConn)
rsData = objCmd.ExecuteReader(0)
If rsData.HasRows Then
Do While (rsData.Read())
Dim SubjectNames As New List(Of String)
SubjectNames.Add(rsData.GetString("subject"))
Loop
SubjectList = SubjectNames.ToArray
Now I was thinking ToArray and was wondering how then to output all of the list entries into strings that I can then use later on.
It's also prudent to note that I'm doing this all inline as the CMS I have to use doesn't allow any code behind.

If i understand you correctly you don't know how to convert a List(Of String) to a string. You can use String.Join and specify which string you want to use to join the words:
Dim SubjectNames As New List(Of String)
Do While (rsData.Read())
SubjectNames.Add(rsData.GetString("subject"))
Loop
Dim headers As String = String.Join(", ", SubjectNames)
Note that i've moved the declaration of the list outside of the loop because otherwise you would always create a new list for every record.
Edit: So you don't know how to access elements in a List or Array.
Elements in a List(Of String) or String() are already individual strings. You access each element via indexer. For example: Dim firstSubject = SubjectNames(0) or via Linq: firstSubject = SubjectNames.First(). For the 10th element: Dim subject10 = SubjectNames(9) since indexes are zero based.
MSDN: Arrays in Visual Basic

Related

Comma-separated string to data table error: input array is longer

I am trying to put data received from an API call that is comma-delimited into a datatable.
The data comes in something like this:
Name,ID,Date,Supervisior CRLF
Joe,123,1/1/2020,George CRLF
Mike,456,2/1/2020,George CRLF
Dan,789,4/1/2021,George
If there is only one row of data then my code works the data displays on screen just fine.
If there is more than one row I get an error "Input array is longer than the number of columns in this table."
I tried doing a split on comma and environment new line (also vbCRLF); none of those resolved the issue.
Any ideas on how I can resolve this?
Here is my code:
Dim vartable As DataTable = New DataTable()
vartable.Columns.Add("Name", GetType(String))
vartable.Columns.Add("ID", GetType(String))
vartable.Columns.Add("Date", GetType(String))
vartable.Columns.Add("Supervisior", GetType(String))
Dim inputstring As String
inputstring = (apiresponse) 'redacted API code as it works fine If I just display 'raw data to text field
Dim rowData As String() = inputstring.Split(New Char() {",",Environment.NewLine})
vartable.Rows.Add(rowData) 'this is where I get the input array error if 'more than one row of 'data
GridView1.DataSource = vartable
GridView1.DataBind()
It looks like you're expecting the Split() function to act on each of the delimiters separately, so you get an array of arrays, with each element in the outer array holding one line/row. This is not how it works.
You need to separate the lines first, and then in a loop for each line separate the contents by comma. The test way to do this is NOT by calling Split(). Instead, you can use a StringReader (which is different from StreamReader):
Using rdr As New StringReader(apiresponse)
Dim line As String = rdr.ReadLine()
While line IsNot Nothing
Dim rowData As String() = line.Split(","c)
vartable.Rows.Add(rowData)
line = rdr.ReadLine()
End While
End Using
But I would be surprised to learn the code to access the API doesn't also need to deal with streams at some point, even if you don't see it directly, meaning there exists a possible version of this that is even more efficient from using StreamReader connected to the api response directly.
For fun, since it's been a while since I've had to do this in VB, I made this extension module:
Public Module TextExt
<Extension()>
Public Iterator Function AsLines(data As TextReader) As IEnumerable(Of String)
Dim line As String = data.ReadLine()
While line IsNot Nothing
Yield line
line = data.ReadLine()
End While
End Function
<Extension()>
Public Iterator Function AsLines(data As String) As IEnumerable(Of String)
Using rdr As New StringReader(data)
For Each line As String In AsLines(rdr)
Yield line
Next
End Using
End Function
End Module
Which would let me write it like this:
For Each row As String() In apiresponse.AsLines().Select(Function(ln) ln.Split(","c))
vartable.Rows.Add(row)
Next
Finally, I need to add my customary warning about how it's a really bad idea to use .Split() as a CSV parser.

How can I store an array in my.settings in vb.net

I am trying to store a user input array in a my.settings variable in vb.net. I would like for the user to enter an array in the form {1,2,3}, store that as a string in the setting and then be able to use the setting value later to create a new array. The code will be something like:
Dim inputarray()
Dim outputarray()
inputarray=textbox1.text
my.settings.inputstoredarray.add(inputarray)
outputarray=my.settings.inputstoredarray
textbox2.text=outputarray(0)
'If the user types "{1,2,3}' in textbox1, textbox2 should show "1"
I have tried multiple versions of this but there seem to always be type conversion errors. I don't understand why it works if I hardcode:
inputarray={1,2,3}
and yet the below code does not work:
inputarray=my.settings.inputstoredarray
How can I store a user provided array in my.settings and retrieve it for use later?
does not work even if I go into settings and set the string value for the setting to {1,2,3}
Set up your setting in Project Properties, Settings tab as follows.
Then save the setting as follows.
Private Sub SaveStringToSettings()
My.Settings.StringOfInts = TextBox1.Text 'User types in {1, 2, 3}
End Sub
To retrieve the setting and turn it into an array
Private Sub CreateArrayFromSettings()
Dim SettingValue = My.Settings.StringOfInts
Debug.Print(SettingValue) 'View this in the Immediate window
'Get rid of the braces
Dim TrimmedString = SettingValue.Trim({"{"c, "}"c})
'Split the string by the commas into an array
Dim Splits = TrimmedString.Split(","c)
'Get rid of the spaces
For i = 0 To Splits.Length - 1
Splits(i) = Splits(i).Trim
Next
TextBox1.Text = Splits(0) 'Displays 1
End Sub
You can browse for additional setting types in your Project properties/Settings tab but you'll find it fairly limited with the most common message being that you can't use that type. Array is one you can't use, I'm afraid.
But you can use the fully-supported StringCollection:
Dim saveTest As New StringCollection() From {"1", "2", "3", "4"}
My.Settings.MySetting = saveTest
My.Settings.Save()
Dim loadTest As StringCollection = My.Settings.MySetting
Another option would be to use, say, and XML or JSON string that you de-serialize but that's getting a bit involved.
You can't store an array in application settings. What you can do is create a setting of type StringCollection. You can then use it as a StringCollection in code or you can transfer the data back and forth between the StringCollection and an array if you really need an array.
Start by opening the Settings page of the project properties and creating a new setting of type System.Collections.Specialized.StringCollection. For this example, I'll name it MyStringCollection but you should name it appropriately for your app. When you do this, notice that the Value field is empty by default. This means that the setting is Nothing by default. That's OK but it means that you need to actually create the collection object in code before using it for the first time, e.g.
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
If My.Settings.MyStringCollection Is Nothing Then
My.Settings.MyStringCollection = New StringCollection
End If
'Use My.Settings.MyStringCollection here.
End Sub
Alternatively, you can force the settings UI to create the object for you. To do that, select the Value field for that setting and click the browse (...) button. Add any arbitrary character and click the OK button. Notice that the Value field is populated with an XML snippet that contains the text you entered. Click the browse (...) button again, delete the text and click OK again. Notice that the XML remains, even though the text you had entered is removed. That XML will now create the StringCollection object automatically, so you don't need code to do so.
If you're happy wiith a collection of String values then you can use the setting directly in code wherever you like. It works, for the most part, like a List(Of String), allowing you to add and remove items at will. If you specifically need an array or you need a type other than String then you will have to do some translation, e.g.
'Load the collection items into a String array.
Dim myStringArray = My.Settings.MyStringCollection.Cast(Of String)().ToArray()
'Load the collection items into an Integer array.
Dim myIntegerArray = My.Settings.MyStringCollection.Cast(Of String)().Select(Function(s) CInt(s)).ToArray()
'Repopulate the collection from a String array.
My.Settings.MyStringCollection.Clear()
My.Settings.MyStringCollection.AddRange(myStringArray)
'Repopulate the collection from an Integer array.
My.Settings.MyStringCollection.Clear()
My.Settings.MyStringCollection.AddRange(myIntegerArray.Select(Function(n) n.ToString()).ToArray())
If you want to display the contents of your collection in a TextBox than you might do something like this:
TextBox1.Text = String.Join(",", My.Settings.MyStringCollection.Cast(Of String)())
That will create a single, comma-delimited String containing all the items. To repopulate the collection from a TextBox containing such text, do this:
My.Settings.MyStringCollection.Clear()
My.Settings.MyStringCollection.AddRange(TextBox1.Text.Split(","c))

Improve performance finding matches in a big list/array

I am building a tool to find images in a big folder of images (400k images). On that folder I have images like this:
c:\images\100001_01.jpg
c:\images\100001_05.jpg
c:\images\100001_07.jpg
c:\images\100005_05.jpg
c:\images\100010_00.jpg
Then I have my references in a text box, but only the 6 digit number:
100001
100005
100006
Etc
So I have let's say 1000 references I need to have the images for, I want to loop through all the image folder and take the file if exists. I have built this using both an array with loops and a list and getting the index. I thought getting the index of the list would be much faster, but they are actually the same.
Here are the two routines I have developed, one is using a list and then getting FindIndex to get the index. The second option is looping through all the references and at the same time looping through all images to check if any contains that reference - that is 400 million loops if I use a set of 1000 references!
Using a list takes 69 seconds, however looping through the arrays takes 64 seconds. Nevertheless taking all images on the directory using GetFile takes 120 seconds already.
Can you think of any way to make this faster?
Private Sub ExtractImagesUsingList()
Dim ListOfReferences As New List(Of String) 'the actual list of references is in a textbox, ie.: 100001, 100002, etc
For Each line In txtBox.Lines
ListOfReferences.Add(line.ToString)
Next
Dim ListOfimages As New List(Of String)
For Each file In IO.Directory.GetFiles("c:\images\")
ListOfimages.Add(file)
Next
For Each ref In ListOfReferences
Dim index As Integer = ListOfimages.FindIndex(Function(x As String) x.Contains(ref))
Next
End Sub
Private Sub ExtractImagesUsingArrayLoop()
Dim ListOfreferences As New List(Of String)'the actual list of references is in a textbox
For Each line In txtBox.Lines
If line.Length > 1 Then
ListOfReferences.Add(line.ToString)
End If
Next
Dim ArrayImages() As String = IO.Directory.GetFiles("c:\images\")
For Each reference In ListOfReferences
For Each image In ArrayImages
If image.Contains(reference) Then
Exit For ' I exist the FOR here because I am only interested in one image per reference
End If
Next
Next
End Sub
It's not 100% clear what you're trying to accomplish, but you could improve performance by implementing Directory.EnumerateFiles.
Additionally, you could pair this with the built-in search function using a wildcard match against the known reference key from the reference list.
Finally, if the image directory is not subject to frequent changes, you could cache the images in a dictionary to speed up future searches.
Here is a rough example of those ideas. Note I've removed any references to form controls, and assume parameters are instead being passed in.
Private _imageMap As Dictionary(Of String, ICollection(Of String))
Public ReadOnly Property ImageMap As Dictionary(Of String, ICollection(Of String))
Get
If _imageMap Is Nothing Then
_imageMap = New Dictionary(Of String, ICollection(Of String))()
End If
Return _imageMap
End Get
End Property
Public Sub RefreshImageMap()
_imageMap = Nothing
End Sub
Public Function GetImagePaths(imageFolder As String, referenceKey As String) As ICollection(Of String)
Dim imagePaths As ICollection(Of String) = Nothing
If Not ImageMap.TryGetValue(referenceKey, imagePaths) Then
imagePaths = Directory.EnumerateFiles(imageFolder, $"{referenceKey}_*.jpg").ToList()
ImageMap.Add(referenceKey, imagePaths)
End If
Return imagePaths
End Function
Also if you wanted to run multiple reference keys through a function, but also needed to keep the original reference key passed in, you could add something like this:
Public Iterator Function GetImagePaths(imageFolder As String, referenceKeys As IEnumerable(Of String)) As IEnumerable(Of KeyValuePair(Of String, ICollection(Of String)))
For Each referenceKey As String In referenceKeys
Dim imagePaths = GetImagePaths(imageFolder, referenceKey)
Yield New KeyValuePair(Of String, ICollection(Of String))(referenceKey, imagePaths)
Next
End Function
None of this is tested, and this lacks proper parameter checks on functions, but it should give you a direction to try.

Is it possible to dimension a new variable based on array value?

I have a lot of fields for which I would need to define a new variable. The list of fields is likely to change, so I'd like to put the listing of fields in a string to populate an array. From there, I want to loop through the array and dimension the variable. When I run code similar to below, I get the compile error "Constant expression required." If something like this isn't possible, is there a better way to achieve a similar result?
Public Sub Test()
Dim strFields as String
Dim arrFields() as String
strFields = "Field1|Field2|Field3"
arrFields = split(strFields,"|")
For Each x In arrFields()
Dim arrFields(x) As String
Next x
End Sub

How to have a global Dictionary in VB.NET/WPF application to save data from different windows?

I am new to VB.NET and WPF.
I am building a "Questionnaire" app. Users will be presented sequentially with different questions/tasks (windows). After they respond on each question/task and press a "submit" button a new window will open with a new question/task, and previous window will close. After each question, when the button is pressed, I need to store data to some global object. After all questions are answered the data of this object should be written out to the output file.
I figured out that Dictionary will be the best to store the results after each window.
I am not sure how, where to create this global Dictionary and how to access it. Should I use View Model? If yes, can you give an example? Or, should it be just a simple class with shared property? (something like this)
EDIT 2: I tried many different ways recommended online
GlobalModule:
Module GlobalModule
Public Foo As String
End Module
GlobalVariables:
Public Class GlobalVariables
Public Shared UserName As String = "Tim Johnson"
Public Shared UserAge As Integer = 39
End Class
Global properties:
Public Class Globals
Public Shared Property One As String
Get
Return TryCast(Application.Current.Properties("One"), String)
End Get
Set(ByVal value As String)
Application.Current.Properties("One") = value
End Set
End Property
Public Shared Property Two As Integer
Get
Return Convert.ToInt32(Application.Current.Properties("Two"))
End Get
Set(ByVal value As Integer)
Application.Current.Properties("Two") = value
End Set
End Property
End Class
Here is where I save the data to global variables/properties in the first window. I need to store data in this subroutine before closing an old window and opening a new window. I use MessageBox just for testing.
Private Sub btnEnter_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnEnter.Click
Dim instructionWindow As InstructionsWindow
instructionWindow = New InstructionsWindow()
Application.Current.Properties("number") = textBoxValue.Text
Globals.One = "2"
Globals.Two = 3
MessageBox.Show("GlobalVariables: UserName=" & GlobalVariables.UserName & " UserAge=" & GlobalVariables.UserAge)
GlobalVariables.UserName = "Viktor"
GlobalVariables.UserAge = 34
GlobalModule.Foo = "Test Foo"
'testing if it saved tha value
'MessageBox.Show(Application.Current.Properties("number"))
Application.Current.MainWindow.Close()
instructionWindow.ShowDialog()
End Sub
Next subroutine is where I am trying to retrieve the value from global Properties/variables in the second window, but message boxes come out empty. There might also the case that I am assigning values in a wrong way, or not reading them in a right way (casting?) :
Private Sub FlowDocReader_Initialized(ByVal sender As Object, ByVal e As System.EventArgs) Handles FlowDocReader.Initialized
' Get a reference to the Application base class instance.
Dim currentApplication As Application = Application.Current
MessageBox.Show(currentApplication.Properties("number"))
MessageBox.Show("One = " & Globals.One & " Two = " & Globals.Two)
MessageBox.Show("GlobalVariables: UserName=" & GlobalVariables.UserName & " UserAge=" & GlobalVariables.UserAge)
MessageBox.Show("GlobalModule.Foo = " & GlobalModule.Foo)
Dim filename As String = My.Computer.FileSystem.CurrentDirectory & "\instructions.txt"
Dim paragraph As Paragraph = New Paragraph()
paragraph.Inlines.Add(System.IO.File.ReadAllText(filename))
Dim document As FlowDocument = New FlowDocument(paragraph)
FlowDocReader.Document = document
End Sub
Thanks.
You can make public Dictionary property for form and put your dictionry to this property or make constructor with Dictionary argument.
You already have this dictionary Application.Properties
Look here, please.
First, you can define a dictionary (list of lists) as follows at the beginning of a form or in a module
Dim dic As New Dictionary(Of String, List(Of String))
As the user completes questions on a form, write the partucular form number and query results to a single record in the dic before going to the next form (place this code into the "Next" button):
'Assume q1response=3, q2response=4,..., qpresponse="text", etc.
Dim myValues As New List(Of String)
myValues.Add(formname)
myValues.Add(q1response)
myValues.Add(q2response)
.
.
myValues.Add(qpresponse)
dic.Add(username, myValues)
When a user is done, there will be multiple records in the dictionary, each of which starts with their name and is followed by question responses. You can loop through multiple dictionary records, where each record is for a user using the following:
For Each DictionaryEntry In dic 'this loops through dic entries
Dim str As List(Of String) = DictionaryEntry.Value
'here you can do whatever you want with results while you read through dic records
'username will be = str(0)
'formname will be str(1)
'q1 response on "formname" will be str(2)
'q2 response on "formname" will be str(3)
'q3 response on "formname" will be str(4)
...
Next
The trick is that there will be multiple dictionary records with results for one user, where record one can have results like "John Doe,page1,q1,q2,q3" and record 2 will be "John Doe,page2,q4,q5,q6." Specifically, the "str" in the above loop will be an array of string data containing all the items within each dictionary record, that is, in str(0), str(1), str(2),... This is the information you need to work with or move, save, analyze, etc.
You can always put all the code I provided in a class (which will be independent of any form) and dimension the sic is a Sub New in this class, with the updating .Add values lines in their own sub in this same class). Then just Dim Updater As New MyNewClassName. Call the Updater in each continue button using Call Updater.SubNameWithAddValues(q1,q2,...qp). It won't matter where you are in your program since you using a specific class. The one thing I noticed with my code is that you can only use the line that adds the "key" or the username once, so use it after the last query -so put it in a Sub Finished in your new class and call as Call Updater.Finished(username,q30,q31,last)

Resources