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

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.

Related

Getting MetaData from an image file

We have images stored on a DB and they are being used to replace an image within a Word document - that bit works perfectly, except where the replacement image is portrait and it's replacing a landscape one, so I'm trying to get the metadata to determine how the image is orientated using this function
Public Function GetImageTags(ImageFile() As Byte) As String()
Try
Dim vReturnArray() As String = Nothing
Using MS As New System.IO.MemoryStream(ImageFile)
Dim vDecoder As BitmapDecoder = BitmapDecoder.Create(MS, BitmapCreateOptions.None, BitmapCacheOption.Default)
Dim vFrame As BitmapFrame = vDecoder.Frames(0)
Dim vMetadata As BitmapMetadata = TryCast(vFrame.Metadata, BitmapMetadata)
If vMetadata IsNot Nothing And vMetadata.Keywords IsNot Nothing Then
vReturnArray = vMetadata.Keywords.ToArray()
End If
End Using
Return vReturnArray
Catch ex As Exception
EmailError(ex)
Return Nothing
End Try
End Function
...but it throws the toys out with...
This codec does not support the specified property.
at System.Windows.Media.Imaging.BitmapMetadata.GetQuery(String query)
at System.Windows.Media.Imaging.BitmapMetadata.get_Keywords()
...at BitMapMetadata.Keywords. Any idea how I can overcome this and get the keywords?
Thank you
================ UPDATE ================
It appears that the error, and I also tried...
vReturnArray = TryCast(vMetadata.GetQuery("System.Keywords"), String())
... is only returned for some images, but all that I tried returned Nothing for the String()
There is a really good EXIF class on Code Project that is easy to implement, either with a string link to the file
Dim vEXIF As New ImageEXIF(ImagePath)
Dim vOrientation As Integer = vEXIF.Orientation
or as BitMap
Dim vOrientation As Integer = 0
Using vBitmap As System.Drawing.Image = System.Drawing.Image.FromStream(New IO.MemoryStream(ImageFile))
Dim vEXIF As New ImageEXIF(vBitmap)
vOrientation = vEXIF.Orientation
End Using
It would not be difficult to add another Sub to the class for Byte(), but the above conversion is quite straightforward and the class should work with all image types.
You could use MetadataExtractor to access the image metadata.
Check for the presence of ExifDirectoryBase.TagOrientation on any of the contained Exif directories.
Something like this (sorry it's C# as I don't know VB.NET):
var orientation = ImageMetadataReader.ReadMetadata(imagePath)
.OfType<ExifSubIfdDirectory>()
.Select(d => d.GetObject(ExifDirectoryBase.TagOrientation))
.First(o => o != null);

Using CSV .txt files with arrays in VB.NET to edit data?

I'm a beginner trying to learn VB.NET and I'm not quite sure how I'll explain this, but I'll give it a shot. Basically, I've written a txt file with about 10 lines of data in CSV form.
For example:
John, 10, 14
Michael, 14, 27
Billy, 13, 45
etc, etc....
I just want to be able to read and edit particular lines - not necessarily add new lines.
Just wondering if someone could just outline how I'd go about this - not asking anyone to write the program for me. I just don't know what to do and I couldn't understand other answers I've found on SO that attempted to solve the same problem. I don't know if I'm just a bit dense or something so it'd be great if someone could perhaps give a simple, 'dumbed-down' outline of what I need to do.
Thank you.
as stated in the comments you usually read all the file into memory, manipulate it and write it all back.
dotnet has a method that puts a file content in an array (line by line)
if you need to access individual cells in the CSV you probably want to run the split method on each line and join to merge them back together.
the split/join methods are either an method of the string/array object or its in the Strings namespace
a method for writing the file back is also in the System.IO Namespace
You don't asking to write the code for you, I can understand you but I think there is no way to show how can you do that. I used split/join methods as #weberik mentioned. It is a simple console application:
Public Class Sample
Public Shared Sub Main()
CreateExampleFileIfNotExists()
'lines variable is an "Array"
Dim lines() As String = IO.File.ReadAllLines("Example.txt")
'write items to the console
ShowItems(lines, "Values before edit:")
'edit the items
EditItems(lines)
'write edited items to the console
ShowItems(lines, "Values after edit:")
'save changes?
Save(lines)
'finish
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Public Shared Sub ShowItems(lines() As String, header As String)
Console.WriteLine(header)
Dim headers() As String = {"Name:", "Value1:", "Value2:"}
WriteItems(headers)
For Each line In lines
WriteItems(line.Split(","))
Next
End Sub
Public Shared Sub EditItems(lines() As String)
For i As Integer = 0 To lines.Length - 1
Dim line As String = lines(i)
Dim values() As String = line.Split(",")
'edit the item
values(0) = "Edited " & values(0)
values(1) += i
values(2) *= i
lines(i) = String.Join(",", values)
Next
Console.WriteLine() 'empty line
End Sub
Public Shared Sub WriteItems(itemValues() As String)
Dim line As String = ""
For Each item In itemValues
line &= item & vbTab & vbTab
Next
Console.WriteLine(line)
End Sub
Public Shared Sub CreateExampleFileIfNotExists()
If Not IO.File.Exists("Example.txt") Then
IO.File.WriteAllLines("Example.txt", {"John,10,14", "Michael,14,27", "Billy,13,45"})
End If
End Sub
Public Shared Sub Save(lines() As String)
Console.WriteLine(vbCrLf & "Do you want to save the changes? Y/N")
Dim result = Console.ReadKey()
Console.WriteLine()
Select Case result.Key
Case ConsoleKey.Y
IO.File.WriteAllLines("Example.txt", lines)
Console.WriteLine("The changes has been saved.")
End Select
End Sub
End Class

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)

Convert list (of string) from datareader to string

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

Reading comma-separated lines from textbox

I've been reading and reading tons of answers, modifying the code, and still i cant figure out how to solve this problem.
I have a textbox that receives multiline comma-separated information from a .txt or .csv file. Example:
Pearl Harbour;Ticonderoga CG-45;300;1000 Everett;Ticonderoga
CG-46;310;1200 Pearl Harbour;Burke DDG-110;215;800
Now there will be a combobox to chose a port (in this example the options will be Pearl Harbour and Everett). After choosing "Pearl Harbour", another multiline textbox will show only the lines which have "Pearl Harbour" as the first element.
Now goes what I was able to write:
Public Sub Readfile()
TextBox1.Text = System.IO.File.ReadAllText("libro1.csv")<br>
Dim lines() As String<br>
lines = Split(TextBox1.Text, vbCrLf)<br>
Dim strline0 As String = lines(0)<br>
Dim strArray0() As String = strline0.Split(";")<br>
Dim strline1 As String = lines(1)<br>
Dim strArray1() As String = strline1.Split(";")<br>
...
End Sub
The first problem I find is that for every line the .csv has, I must write that two lines of code to have an array with all the information. But I cant do that because I cant know how many lines the .csv is going to have.
Im kind of lost here. Im not asking anyone to do magic and give me a code I can copy and paste, but I would be grateful if someone can guide me through this.
First off, you'd do better to use a List than an array. Particularly for a collection of strings, they're much easier to work with. With that, you're correct that you can't go individually naming your lines because you don't know how many there will be. That's why you need to create a list of lines and loop through them, like ...
Public Sub Readfile()
TextBox1.Text = System.IO.File.ReadAllText("libro1.csv")
Dim lines As List(of String)
Dim allResults As New List(of List(of String))
lines = Split(TextBox1.Text, vbCrLf)
For Each line In lines
Dim result As List(Of String) = line.Split(CChar(";"))
allResults.Add(result)
Next
End Sub
This will allow you to essentially say, "For each line in the file, take each semi-colon-separated part and put it into a list called 'result'. Then put 'result' into another list of results called 'allResults'."
Behold! The power of loops!

Resources