How do I find a value in an arraylist? - vb.net

I have an arraylist called arrdirectory. This arraylist contains a structure when query to database.
arrDirectory = New ArrayList
While rdr.Read
With udt_mydir
If Not IsDBNull("dirno") Then
.strdirno = (rdr("dirno"))
Else
.strdirno = "N/A"
End If
If Not IsDBNull("dirname") Then
.strdirname = (rdr("dirname"))
Else
.strdirname = "N/A"
End If
If Not IsDBNull(rdr("dir_image")) Then
.arrImg = rdr("dir_image")
Else
.arrImg = Nothing
End If
If Not IsDBNull(rdr("dir_logo")) Then
.arrLogo = rdr("dir_logo")
Else
.arrLogo = Nothing
End If
End With
arrDirectory.Add(udt_mydir)
End While
How do I find from the arraylist where my string is equal to udt_mydir.strdirname so I can get the whole data strdirno, arrImg and arrLogo?

Why are you using an ArrayList to begin with? Use List<>, its generic counterpart, so you won't need to box/unbox to access its items.
Anyhow, assuming you are on .NET 3.5, finding an element in an IEnumerable is pretty trivial with LINQ's Where(), Single() or SingleOrDefault().
I won't bother figuring out what the syntax is for VB, but here's how it would look in C#, assuming you really want to stick with that horrible ArrayList:
var x = arrDirectory.Cast<YourType>().SingleOrDefault(item => item.strdirname == "my string");

Related

Is there a neat and clean way to handle nulls with Yields?

While CommitReader.Read()
Yield New Commit() With {
.FirstValue = CommitReader.GetInt32(CommitReader.GetOrdinal("FirstValue")),
.SecondValue = CommitReader.GetString(CommitReader.GetOrdinal("SecondValue")).Trim(),
'Lots of values
End While
I know I can do something like this; however there are 24 properties and I would like to make this part as clean as possible
While CommitReader.Read()
new Commit (){
Dim index As Integer = reader.GetOrdinal("FirstValue")
If reader.IsDBNull(index) Then
FirstValue = String.Empty
Else
FirstValue = reader(index)
End If
index = reader.GetOrdinal("SecondValue")
If reader.IsDBNull(index) Then
SecondValue = String.Empty
Else
SecondValue = reader(index)
End If
}
End While
Is there a better way to handle this type of thing? I am mainly a C# developer so if the syntax is off a little sorry, I am winging it in VB.
It's a shame that SqlDataReader doesn't have the generic Field extension method like DataRow does, but you could define your own extension method (has to be in a module in VB.NET) to help with the null checks, perhaps something like this:
<Extension>
Function GetValue(Of T)(rdr As SqlDataReader, i As Integer) As T
If rdr.IsDBNull(i) Then
Return Nothing
End If
Return DirectCast(rdr.GetValue(i), T)
End Function
And use it something like this:
While CommitReader.Read()
Yield New Commit() With {
.FirstValue = CommitReader.GetValue(Of Integer?)(CommitReader.GetOrdinal("FirstValue")),
.SecondValue = CommitReader.GetValue(Of String)(CommitReader.GetOrdinal("SecondValue")),
'Lots of values
End While
I haven't tested this fully to make sure it handles all data types appropriately (may be worth looking at DataRowExtensions.Field to see how it does it).
Note that you are using String.Empty as the "null" value for strings, while this will use Nothing/null (I also had to remove the .Trim call to avoid NREs). If you want empty string instead, you could use (adding the Trim back in):
.SecondValue = If(CommitReader.GetValue(Of String)(CommitReader.GetOrdinal("SecondValue")), String.Empty).Trim()
You may also want to move the GetOrdinal calls out of the loop to improve performance.
Obviously you have repetition in your code if ... else ... condition.
So you can extract it in another method.
For your case generic extension method seems good candidate.
Public Module Extensions
<Extension>
Public Function GetValueOrDefault(Of T)(originalValue As object,
defaultValue As T) As T
If originalValue = DbNull.Value Then
Return defaultValue
End If
return DirectCast(originalValue, T)
End Function
End Module
Then use it:
While CommitReader.Read() = True
Dim temp = new Commit With
{
Dim index As Integer = reader.GetOrdinal("FirstValue")
FirstValue = reader(index).GetValueOrDefault(String.Empty)
Dim index As Integer = reader.GetOrdinal("SecondValue")
FirstValue = reader(index).GetValueOrDefault(String.Empty)
}
End While
You can create another overload which return "default" value for given type if it is DbNull
<Extension>
Public Function GetValueOrDefault(Of T)(originalValue As object) As T
Return originalValue.GetValueOrDefault(Nothing)
End Function
Nothing in vb.net is default value, for reference types it is null for Integer it is 0 for example.
For using this overload you need provide type parameter explicitly
While CommitReader.Read() = True
Dim temp = new Commit With
{
Dim index As Integer = reader.GetOrdinal("FirstValue")
FirstValue = reader(index).GetValueOrDefault(Of String)()
Dim index As Integer = reader.GetOrdinal("SecondValue")
FirstValue = reader(index).GetValueOrDefault(Of String)()
}
End While
Notice that your solution executing reader twice, for checking is it null and for reading value. This can cause "tiny" performance issue.
So in extension method above we read value only once and then check value for DbNull.
If you concatenate a string with a Null you get the string:
FirstValue = reader(index) & ""
Kind of "unprofessional" but saves a lot of coding time if all you are doing is converting a possible Null to an empty string. Easy to forget however, so later data dependent errors may pop up.

Reflection Optimization

I've recently implemented reflection to replace the more tedious aspects of our data retrieval from a SQL database. The old code would look something like this:
_dr = _cmd.ExecuteReader (_dr is the SQLDataReader)
While _dr.Read (_row is a class object with public properties)
_row.Property1 = Convert.ToInt16(_dr("Prop1"))
_row.Property2 = Convert.ToInt16(_dr("Prop2"))
_row.Property3 = Convert.ToInt16(_dr("Prop3"))
If IsDBNull(_dr("Prop4")) = False Then _row.Prop4 = _dr("Prop4")
...
Since my code base has a lot of functionality like this, reflection seemed like a good bet to simplify it and make future coding easier. How to assign datareader data into generic List ( of T ) has a great answer that was practically like magic for my needs and easily translated into VB. For easy reference:
Public Shared Function GenericGet(Of T As {Class, New})(ByVal reader As SqlDataReader, ByVal typeString As String)
'Dim results As New List(Of T)()
Dim results As Object
If typeString = "List" Then
results = New List(Of T)()
End If
Dim type As Type = GetType(T)
Try
If reader.Read() Then
' at least one row: resolve the properties
Dim props As PropertyInfo() = New PropertyInfo(reader.FieldCount - 1) {}
For i As Integer = 0 To props.Length - 1
Dim prop = type.GetProperty(reader.GetName(i), BindingFlags.Instance Or BindingFlags.[Public])
If prop IsNot Nothing AndAlso prop.CanWrite Then
props(i) = prop
End If
Next
Do
Dim obj = New T()
For i As Integer = 0 To props.Length - 1
Dim prop = props(i)
If prop Is Nothing Then
Continue For
End If
' not mapped
Dim val As Object = If(reader.IsDBNull(i), Nothing, reader(i))
If val IsNot Nothing Then SetValue(obj, prop, val)
Next
If typeString = "List" Then
results.Add(obj)
Else
results = obj
End If
Loop While reader.Read()
End If
Catch ex As Exception
Helpers.LogMessage("Error: " + ex.Message + ". Stacktrace: " + ex.StackTrace)
End Try
Return results
End Function
The only caveat with this is that it is somewhat slower.
My question is how to optimize. Sample code I find online is all in C# and does not convert neatly into VB. Scenario 4 here seems like exactly what I want, but converting it to VB gives all kinds of errors (Using CodeFusion or converter.Telerik.com).
Has anyone done this in VB before? Or can anyone translate what's in that last link?
Any help's appreciated.
Couple ideas for you.
Don't use the DataReader when reading ALL records at once, it is slower than using a DataAdapter.
When you use the DataAdapter to fill a DataSet, you can iterate through the rows and columns which does NOT use reflection and will be much faster.
I have a program I created (and many other programmers do this too) that generate the code from a database for me. Each table and row is a class that is specifically named an generated in such a way that I can use intellisense and prevents many run-time errors by making them compile-time errors when data changes. This is very much like the EntityFramework but lighter because it fits MY specific needs.

Object initializing with db null check

I am trying to initialize an object with data from database return in a dataset as below:
Class Pdf
Public FileId As Integer
Public AccountNumber As Integer
Public DateSaved As DateTime
Public FileName As String
Public DateImported As DateTime
Scenerio 1
I can intialize the object like this:
Dim pdf = New Pdf With {.FileId = ds.Tables(0).Rows(i)("fileid"),
.AccountNumber = ds.Tables(0).Rows(i)("accountnumber"),
.DateSaved = ds.Tables(0).Rows(i)("datesaved"),
.FileName = ds.Tables(0).Rows(i)("filename"),
.DateImported = ds.Tables(0).Rows(i)("dateimported")
}
But this is not working, because column data can be null and I am not if how to do a db null check in this approach.
Then I have scenerio 2:
Dim pdf As New pdf
If Not IsDBNull(ds.Tables(0).Rows(i)("fileid")) Then
PdfFileId = ds.Tables(0).Rows(i)("fileid")
Else
PdfFileId = 0
End If
If Not IsDBNull(ds.Tables(0).Rows(i)("accountnumber")) Then
pdf.AccountNumber = ds.Tables(0).Rows(i)("accountnumber")
Else
pdf.AccountNumber = 0
End If
If Not IsDBNull(ds.Tables(0).Rows(i)("datesaved")) Then
pdf.DateSaved = Format(ds.Tables(0).Rows(i)("datesaved"), "yyyy-MM-dd")
Else
pdf.DateSaved = Nothing
End If
If Not IsDBNull(ds.Tables(0).Rows(i)("dateimported")) Then
pdf.DateImported= Format(ds.Tables(0).Rows(i)("dateimported"), "yyyy-MM-dd")
Else
pdf.DateImported= Nothing
End If
How can I do this to avoid doing so many If statements below. This way seems inefficient to me, can anyone suggest an better approach to initializing the object in scenario one or two? If the question is unclear, please do let me know, I will try and explain.
Please note this is sample data.
From reading T Field<T>(this DataRow row, string columnName), I believe that there is a check for the DBNull.Value for both reference and value types, returning a default value if DBNull.Value is passed.
So you can use it instead of checking for DBNull.Value each time:
.FileName = ds.Tables(0).Rows(i).Field(Of String)("FileName")
If the value of the specified DataColumn is null and T is a reference type or nullable type, the return type will be null. The Field method will not return Value.
DataRowExtensions.Field
Since you cant use this then #TimSchmelter provided an answer which you could build upon:
.FileId = If(row.IsNull("fileid"), 0, Convert.ToInt32(row("fileid"))
Its not inefficient but a lot of code, so maybe there is a more readable/maintainable approach.
The If operator is one, you can also use DataRow.IsNull:
For i As Int32 = 0 To ds.Tables(0).Rows.Count - 1
Dim row = ds.Tables(0).Rows(i)
Dim pdf = New Pdf With {
.FileId = If(row.IsNull("fileid"), 0, row.Field(Of Int32)("fileid")),
.AccountNumber = If(row.IsNull("accountnumber"), 0, row.Field(Of Int32)("accountnumber")),
.DateSaved = If(row.IsNull("datesaved"), Date.MinValue, row.Field(Of Date)("datesaved")),
.FileName = If(row.IsNull("filename"), "", row.Field(Of String)("filename")),
.DateImported = If(row.IsNull("dateimported"), Date.MinValue, row.Field(Of Date)("dateimported"))
}
Next
(changed the name of your class from Object to Pdf)
You should also set Option Strict to On, then you need to code type safe but you gain compile time safety and avoid unwanted conversions.

Create ReadOnly Property of CSV String vb.net

What is the best way to go about taking a List(Of objects) and adding a ReadOnly Property that displays a csv list of one of the Properties of the object? Is there a good way to convert that list to a string by specifying "Name".
Ex
Object = New Item()
Object.ID = 1
Object.Name = "Test"
li.Add(Object)
Object = New Item()
Object.ID = 2
Object.Name = "Test 2"
li.Add(Object)
Object = New Item()
Object.ID = 3
Object.Name = "Test 3"
li.Add(Object)
I'm thinking a for each would need to be done here or is there a better way to do this?
Return "Test, Test 2, Test 3" from the list values
Essentially, this does what I'm wanting to do but I would like to know if there is a BETTER way to do it.:
Public ReadOnly Property ItemList() As String
Get
Dim returnvalue As String = String.Empty
If Items.Count > 0 Then
For Each Item In Items
returnvalue = returnvalue & Item.Name & ", "
Next
Return Left(returnvalue, returnvalue.Length - 2)
Else
Return ""
End If
End Get
End Property
I have this link to share, the solution is similar, and may need a few tweeks. The conversion from C# to VB is simple enough and can show you where to make a few changes.
Write C# Lists of objects in CSV file
However, I believe your code is fine, with the exception of how you create your csv string.
I would recommend using string builder. This way, if an item exists, you build a new line. Otherwise, you don't. This would keep your from needed to remove (left) the last inserted comma. You would also want to catch the last item in your list, and avoid the comma at the end there.
as an exmaple:
Dim sb As StringBuilder
--if not last item then --
sb.AppendFormat ("{0},{1}",Item.Name,",")
--else this is last item--
sb.AppendFormat ("{0},",Item.Name)
The link I shared is a much better solution. My example is only to build your method more reliable.
Or, if you change your list of object to an array, or list of string...
Dim sampleList = New List(Of String)() From { _
"lala", _
"lulu", _
"lele" _
}
Dim data = String.Join(",", sampleList)

Dynamic query Linq to xml VB.NET

Hey,
I want to write a query that the "where" in the query is a string something like"
Dim query as string= "Name =xxxx and Date > 10 "
Dim t = from book in doc.Descendants("books") Select _
[Name] = book..value, [Date] = book..value....
Where (query)
I build the query string on run time
Thanks...
I'm not saying this is your case but I see this a lot from people that came from ASP classic where we used to build dynamic SQL strings all of the time. We tend to hope that LINQ will give us some more power in part of the code but let us use strings elsewhere. Unfortunately this isn't true. Where takes a boolean argument and there's no way around that. You can write your own parser that uses reflection and eventually returns a boolean but you'd be writing a lot of code that could be error prone. Here's how you really should do it:
Assuming this is our data class:
Public Class TestObject
Public Property Name As String
Public Property Job As String
End Class
And here's our test data:
Dim Objects As New List(Of TestObject)
Objects.Add(New TestObject() With {.Name = "A", .Job = "Baker"})
Objects.Add(New TestObject() With {.Name = "B", .Job = "President"})
Objects.Add(New TestObject() With {.Name = "C", .Job = "Bus Driver"})
Objects.Add(New TestObject() With {.Name = "D", .Job = "Trainer"})
What you want to do is create a variable that represents the data to search for:
''//This variable simulates our choice. Normally we would be parsing the querystring, form data, XML values, etc
Dim RandNum = New Random().Next(0, 3)
Dim LookForName As String = Nothing
Select Case RandNum
Case 0 : LookForName = "A"
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
''//Query based on our name
Dim Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
If sometimes you need to search on Job and sometimes and sometimes you don't you just might have to write a couple of queries:
Dim Subset As List(Of TestObject)
Select Case RandNum
Case 0
Subset = (From O In Objects Select O Where (O.Name = "A" And O.Job = "Baker")).ToList()
Case Else
Select Case RandNum
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
End Select
And just to explain writing your own query parser (which is a path that I recommend you DO NOT go down), here is a very, very, very rough start. It only supports = and only strings and can break at multiple points.
Public Shared Function QueryParser(ByVal obj As Object, ByVal ParamArray queries() As String) As Boolean
''//Sanity check
If obj Is Nothing Then Throw New ArgumentNullException("obj")
If (queries Is Nothing) OrElse (queries.Count = 0) Then Throw New ArgumentNullException("queries")
''//Array of property/value
Dim NameValue() As String
''//Loop through each query
For Each Q In queries
''//Remove whitespace around equals sign
Q = System.Text.RegularExpressions.Regex.Replace(Q, "\s+=\s+", "=")
''//Break the query into two parts.
''//NOTE: this only supports the equal sign right now
NameValue = Q.Split("="c)
''//NOTE: if either part of the query also contains an equal sign then this exception will be thrown
If NameValue.Length <> 2 Then Throw New ArgumentException("Queries must be in the format X=Y")
''//Grab the property by name
Dim P = obj.GetType().GetProperty(NameValue(0))
''//Make sure it exists
If P Is Nothing Then Throw New ApplicationException(String.Format("Cannot find property {0}", NameValue(0)))
''//We only support strings right now
If Not P.PropertyType Is GetType(String) Then Throw New ApplicationException("Only string property types are support")
''//Get the value of the property for the supplied object
Dim V = P.GetValue(obj, Nothing)
''//Assumming null never equals null return false for a null value
If V Is Nothing Then Return False
''//Compare the two strings, return false if something doesn't match.
''//You could use String.Compare here, too, but this will use the current Option Compare rules
If V.ToString() <> NameValue(1) Then Return False
Next
''//The above didn't fail so return true
Return True
End Function
This code would allow you to write:
Dim Subset = (From O In Objects Select O Where (QueryParser(O, "Name = A", "Job = Baker"))).ToList()
No, there is nothing directly like what you're looking for where you can pass in a string. As they say, when all you have is a hammer, everything looks like a nail...The real problem is that you need to learn what LINQ is good at and apply it to your code (if it is a good fit), rather than try and make it do what you could with a dynamically built SQL query string.
What you should probably be doing is making those "Where" clauses strongly typed anyway. Your current code has a lot of potential to blow up and be hard to debug.
What you could do instead is something like this (sorry, using C#, been a while since I've touched VB.NET):
var query = from book in doc.Descendants("books")
select book;
if(needsNameComparison)
{
query = query.where(book.Name == nameToCompare);
}
if(needsDateComparison)
{
query = query.Where(book.Date > 10);
}
List<book> bookList = query.ToList();
With LINQ, "query" isn't actually run until the "ToList()" call. Since it uses late execution, the query is dynamic in that it's being built on until it actually needs to run. This is similar to the code you were looking to use before since you were building a query string ahead of time, then executing it at a specific point.