Convert an unknown structure to an untyped Object in VB.NET - vb.net

I'd like to convert an unknown basic structure to an Object (no type here).
I'm building a library that will be used by many users to extract data from my system but don't want to do a new function for everyone of them. They have to know what will be the result.
In vb, it is possible to create an Object with some properties and use it as it is a regular Class like so:
Dim myObj as New With { .name = "Matt", .age = "28" }
MsgBox( myObj.name & " is now " & myObj.age & " years old.")
So far, so good.
Next step : my user will give me some instructions that I need to extract data from various DBs, and I've no idea of what the result will be.
What I know after the execution is a list of String containing the columns of the result set and, of course a (set of) rows.
And here is the problem of course
My function (for a single row) so far:
Public Function GetData(ByVal instructions as String) as Object ' User is supposed to know what will be inside, instructions is as XML describing DB, table, query, ...
' Do what is needed to retrieve data
' Here I have a variable cols As List(Of String) ' e.g. ("BP", "NAME", "VAT")
Dim o As New With ???
Return o
End Function
What I've tried: build a fake JSon on the fly, and try to Deserialize to Object.
But even if it seems to work, I (and the user) can't access the property as in my top piece of code like:
MsgBox(o.BP)
I know that I could do
Public Function GetData(Of T As {New})(ByVal instructions as String) As T
Dim o As T
' Use some Reflexion to TryInvokeMember of T
Return o
End Function
But I wanted to remove the hassle to create a class to use my code.
Plus, My librairy will be use in a webservice and the class of the user is then unknown.

One approach could be - to use Dictionary(Of String, Object)
Public Function GetData(instructions as String) As Dictionary(Of String, Object)
Dim data = ' Load data
Dim columns As String() = { "BP", "NAME", "VAT" }
Return columns.ToDictionary(
Function(column) column,
Function(column) data.GetByColumnName(column)
)
End Function
` Usage
Dim result = GetDate("instructions: ['BP', 'NAME']")
' Because user knows it is Integer
Dim bpValue = DirectCast(result.Item("BP"), Integer)

Thanks to #GSerg, #Fabio and a few other searches about ExpandoObject, I did it !
Imports System.Dynamic
Dim o As Object = New ExpandoObject()
For Each col In cols
DirectCast(o, IDictionary(Of String, Object)).Add(col, row.GetString(col))
Next

Related

Using the New List command with a Passed Parameter

I'm trying to Pass a Field Parameter from my form textbox to a Function to create a New List object from the Data Table parameter I'm passing.
In the following code, the first tmpReadTable shows with no syntax error, but when I try to use the Parm with the Datatable name I'm not sure what I'm missing syntax wise. I'm new to this, thanks in advance!
Updated code below:
Thank you for all the helpful replies...sorry I'm not more experienced, I'm coming from a Visual Foxpro background.
To summarize:
I want to pass in my IMPORT table parameters from my form.
The cImportTable is an empty SQL Table to use to import and validate each CSV file row.
I found this example in Murach's VB book but he leaves out how the LIST is being created from a PRODUCTS table in an earlier exercise. So I thought I could just substitute my passed cImportTable to do the same...that's where I'm stuck and maybe you all know of a better way.
Private Function ReadImportFile(ByVal cImportFile As String, ByVal cGroupID As String, ByVal cControlTable As String, ByVal cImportTable As String)
MessageBox.Show(cImportFile + " " + cGroupID + " " + cControlTable)
If Not File.Exists(cImportFile) Then
MessageBox.Show("File: " + cImportFile + " does not exist - cancelling process.")
Return False
End If
Dim curFileStream As New StreamReader(New FileStream(cImportFile, FileMode.Open, FileAccess.Read))
Dim curImportTable = "NewDataSet." + cImportTable
'Here I'm trying to create a LIST or DATASET using my Empty SQL Import Table and read in each row of the CSV file in the DO WHILE loop
'...I'm coming from Visual Foxpro background so am not sure what I'm missing or what is the standard procedure to do this simple task.
'This line gives me a syntax issue - and I'm not even sure what it's suppose to do, I'm taking it from Murach's VB book example,
'but he leaves out this vital piece of how to create this LIST from a Datatable - or if it's even the right method to use.
Dim tmpReadTable = New List(Of curImportTable)
Do While curFileStream.Peek <> -1
Dim row As String = curFileStream.ReadLine
Dim columns() As String = row.Split(",")
Dim ImportRecord As New curImportTable
ImportRecord.GroupId = columns(0)
ImportRecord.MemberId = columns(1)
Loop
'More Processing after Importing CSV file.....
curFileStream.Close()
'If lNoErrors
Return True
End Function
You are using a variable instead of TYPE on the code line #3 here
' This seems to be ok, no syntax error
Dim tmpReadTable = New List(Of NewDataSet.FO_ImportDataTable)
' The variable below implicitely will be of STRING type
Dim curImportTable = "NewDataSet." + cImportTable.ToString
' This line is not going to work
Dim tmpReadTable = New List(Of curImportTable)
' BUT THIS WILL
Dim x = New List(Of String)
Another issue is that Dim tmpReadTable happened twice in your code! can't re-declare variable. On top you declared it as NewDataSet.FO_ImportDataTable
Besides, I recommend declare all variables like Dim curImportTable as String, this way you can recognize types easier. Option Infer is good when you use anonymous types, LINQ, etc

Convert Database Linq Query Results to String List

I am trying to use a LINQ query to grab some data from a database, and since I am grabbing just one column of data I want to store it within a string list. This is the code I have.
Dim POList As New List(Of String)
Using dbContext As New DBLINQDataContext
Dim query = (From o In dbContext.Orders
Where o.Order_Number.StartsWith(JobNumber)
Select o.Order_Number)
POList = query.ToList()
End Using
MessageBox.Show(POList.ToString())
When I run this the data in the MessageBox is
System.Collections.Generic.List`1[System.String]
There is no table data, even though I know there are actual data points for me to be getting :\
POList.ToString() will display the name of the object type.
You can use string.Join(",", POList.ToString()).
You can also define an extension method StringJoin() to show your list as string if you are going to use it frequently.
Here is an extension method that you can use:
public static StringExtensions
{
public static string StringJoin(this IEnumerable<string> strings, string seperator)
{
if (strings == null) return null;
return string.Join(seperator, strings);
}
}
Since this was a vb.net question, I translated #vendettamit 's excellent answer for the benifit of future vb readers.
Public Module StringExtensions
<Extension()>
Public Function StringJoin(MyStrings As IEnumerable(Of String), separator As String) As String
If MyStrings Is Nothing Then
Return Nothing
Else
Return String.Join(separator, MyStrings)
End If
End Function
End Module
To use the extension...
Dim l As New List(Of String) From {"Mathew", "Mark", "Luke", "John"}
Dim s As String = l.StringJoin(", ")
Debug.Print(s)

Get data from a collection

I want to make a collection to have data available
Example:
Dim b As New Collection
colb = New Collection
b.Add("system", "1001", "SYSTEM")
b.Add("network", "1002", "NETWORKA")
b.Add("networksecond", "1010", "NETWORKB")
colb.Add(b, "list")
im looking for a function to get data from this collection:
I want to, based on the ID (Second number) get the first and third value
So if I search for 1010, I need to have the value Network and NETWORKA
VB6 called, they want their Collection back.
No, seriously, please consider using a Dictionary instead of the old, legacy Collection class. Behold the beauty of generics and strong typing:
Dim dic As New Dictionary(Of Integer, Tuple(Of String, String))
dic.Add(1001, Tuple.Create("system", "SYSTEM"))
dic.Add(1002, Tuple.Create("network", "NETWORKA"))
dic.Add(1010, Tuple.Create("networksecond", "NETWORKB"))
' Search
Dim t As Tuple(Of String, String) = Nothing
If dic.TryGetValue(1002, t) Then
Console.WriteLine(t.Item1) ' prints "network"
Console.WriteLine(t.Item2) ' prints "NETWORKA"
End If
As soon as you have more than two values, I suggest that you use a specialized class instead of a Tuple to increase readability.
Also, you can simply use List(Of T). In most cases this is enough. Dictionary is good for fast search out long list by a single key.
'declare model
Public Class NetworkModel
Public Property Id As Integer
Public Property Name1 As String
Public Property Name2 As String
End Class
' load list of models
Private _modelList As New List(Of NetworkModel)()
.......
' search using LINQ
Dim model As NetworkModel = _modelList.FirstOrDefault(Function(m) m.Id = 1001)
If model IsNot Nothing Then . . . . .

How to order list of files by file name with number?

I have a bunch of files in a directory that I am trying to get based off their type. Once I have them I would like to order them by file name (there is a number in them and I would like to order them that way)
My files returned are:
file-1.txt
file-2.txt
...
file-10.txt
file-11.txt
...
file-20.txt
But the order I get them in looks something more closely to this:
file-1.txt
file-10.txt
file-11.txt
...
file-2.txt
file-20.txt
Right now I am using Directory.GetFiles() and attempting to using the linq OrderBy property. However, I am failing pretty badly with what I would need to do to order my list of files like the first list above.
Directory.GetFiles() seems to be returning a list of strings so I am unable to get the list of file properties such as filename or name.
Here is my code currently:
documentPages = Directory.GetFiles(documentPath, "*.txt").OrderBy(Function(p) p).ToList()
Would anyone have any ideas?
It sounds like you might be looking for a "NaturalSort" - the kind of display File Explorer uses to order filenames containing numerals. For this you need a custom comparer:
Imports System.Runtime.InteropServices
Partial Class NativeMethods
<DllImport("shlwapi.dll", CharSet:=CharSet.Unicode)>
Private Shared Function StrCmpLogicalW(s1 As String, s2 As String) As Int32
End Function
Friend Shared Function NaturalStringCompare(str1 As String, str2 As String) As Int32
Return StrCmpLogicalW(str1, str2)
End Function
End Class
Public Class NaturalStringComparer
Implements IComparer(Of String)
Public Function Compare(x As String, y As String) As Integer Implements IComparer(Of String).Compare
Return NativeMethods.NaturalStringCompare(x, y)
End Function
End Class
Use it to sort the results you get:
Dim myComparer As New NaturalStringComparer
' OP post only shows the filename without path, so strip off path:
' (wont affect the result, just the display)
Dim files = Directory.EnumerateFiles(path_name_here).
Select(Function(s) Path.GetFileName(s)).ToList
Console.WriteLine("Before: {0}", String.Join(", ", files))
' sort the list using the Natural Comparer:
files.Sort(myComparer)
Console.WriteLine("After: {0}", String.Join(", ", files))
Results (one-lined to save space):
Before: file-1.txt, file-10.txt, file-11.txt, file-19.txt, file-2.txt, file-20.txt, file-3.txt, file-9.txt, file-99.txt
After: file-1.txt, file-2.txt, file-3.txt, file-9.txt, file-10.txt, file-11.txt, file-19.txt, file-20.txt, file-99.txt
One of the advantages of this is that it doesnt rely on a specific pattern or coding. It is more all-purpose and will handle more than one set of numbers in the text:
Game of Thrones\4 - A Feast For Crows\1 - Prologue.mp3
Game of Thrones\4 - A Feast For Crows\2 - The Prophet.mp3
...
Game of Thrones\4 - A Feast For Crows\10 - Brienne II.mp3
Game of Thrones\4 - A Feast For Crows\11 - Sansa.mp3
A Natural String Sort is so handy, is is something I personally dont mind polluting Intellisense with by creating an extension:
' List<string> version
<Extension>
Function ToNaturalSort(l As List(Of String)) As List(Of String)
l.Sort(New NaturalStringComparer())
Return l
End Function
' array version
<Extension>
Function ToNaturalSort(a As String()) As String()
Array.Sort(a, New NaturalStringComparer())
Return a
End Function
Usage now is even easier:
Dim files = Directory.EnumerateFiles(your_path).
Select(Function(s) Path.GetFileName(s)).
ToList.
ToNaturalSort()
' or without the path stripping:
Dim files = Directory.EnumerateFiles(your_path).ToList.ToNaturalSort()
I'm assuming the file and .txt parts are mutable, and just here as placeholders for file names and types that can vary.
I don't use regular expressions very often, so this may need some work yet, but it's definitely the direction you need to go:
Dim exp As String = "-([0-9]+)[.][^.]*$"
documentPages = Directory.GetFiles(documentPath, "*.txt").OrderBy(Function(p) Integer.Parse(Regex.Matches(p, exp)(0).Groups(1).Value)).ToList()
Looking again, I see I missed that you are filtering by *.txt files, which can help us narrow the expression:
Dim exp As String = "-([0-9]+)[.]txt$"
Another possible improvement brought by the other answer that includes test data is to allow for whitespace between the - and numerals:
Dim exp As String = "-[ ]*([0-9]+)[.]txt$"
It's further worth noting that the above will fail if there are text files that don't follow the pattern. We can account for that if needed:
Dim exp As String = "-[ ]*([0-9]+)[.][^.]*$"
Dim docs = Directory.GetFiles(documentPath, "*.txt")
documentPages = docs.OrderBy(
Function(p)
Dim matches As MatchCollection = Regex.Matches(p, exp)
If matches.Count = 0 OrElse matches(0).Groups.Count < 2 Then Return 0
Return Integer.Parse(matches(0).Groups(1).Value)
End Function).ToList()
You could also use Integer.MaxValue as your default option, depending on whether you want those to appear at the beginning or end of the list.

Readonly in keyvaluepair

Well I have created a program that takes some files (Mp3) and change their tags
recently I wanted to add some new Subs (like: Take the songs name and make every letter in it upercase). The problem is that i use a list with its items to be keyvaluepairs
Public MP3List As New List(Of KeyValuePair(Of String, String))
When i tried to edit the key or value of any Item in that list i get an error (That this is READONLY)
Example:
For Each Song In MP3List
Song.Key = "Something"
Next
I add items like this :
Private Function OpenAFile()
Dim MP3List1 = MP3List
Dim oFileDialog As New OpenFileDialog
oFileDialog.Title = "Επέλεξε ένα MP3 Άρχειο"
oFileDialog.Filter = "MP3 Files|*.mp3|All Files|*.*"
oFileDialog.Multiselect = True
Dim Path As String = ""
Dim Name As String = ""
Dim NewPair As New KeyValuePair(Of String, String)
If oFileDialog.ShowDialog = Windows.Forms.DialogResult.OK Then
For Each sPath In oFileDialog.FileNames
Path = New String(sPath)
Name = New String(Strings.Split(Path, "\").ToList(Strings.Split(Path, "\").ToList.Count - 1))
NewPair = New KeyValuePair(Of String, String)(Name, Path)
If Not MP3List1.Contains(NewPair) Then MP3List1.Add(NewPair)
Next
End If
Return MP3List1
End Function
So the idea is this: Each time i press A button to add a song it will run the function OpenAFile() and it was working fine then . Now that i want to change a key or value i get this error
Thanks for the Help and sorry for bad english
The Keys in a KeyValuePair are readonly because they are often used as the key in a hash table. Changing the key would cause issues where you would lose your item in the hash.
If you want to do something like this, you could always create your own data type that stores a key and value. An overly simplified example would be as follows.
Public Structure PathNamePair
Public Property Path As String
Public Property Name As String
Public Sub New(path As String, name As String)
Me.Path = path
Me.Name = name
End Sub
End Structure
I will note that in order to get better performance with your Contains method, you should also implement IEquatable(Of T), but that's probably beyond the scope of this question. I will also note that it is not best practice to have a ValueType (Structure) that is mutable.