Implementing Custom GroupBy function in VB.Net - vb.net

I'm trying to implement a GroupBy function in such a way that it's generic and we don't end up having to keep adding properties every time the client wants something added/changed (yes, I know there's other options. Limited time/budget restrict the scope to this or hard-coding more properties, so I'm trying to opt for the better solution).
Currently I have the following;
<!-- language: lang-vb -->
Dim groupByColumns = New Dictionary(Of String, Boolean) From {
{"Col1", True},
{"Col2", False},
...
}
Dim groupedData = data.GroupBy(Function(r)
Dim groupingResult = New List(Of KeyValuePair(Of String, Object))
For Each column In groupByColumns
Dim groupingValue As Object = Nothing
If column.Value Then
groupingValue = TypeDescriptor.GetProperties(r).Item(grouping.Key).GetValue(r)
End If
groupingResult.Add(New KeyValuePair(Of String, Object)(column.Key, groupingValue))
Next
Return groupingResult
End Function)
If I then do an OrderBy or Select it seems to be returning an IGrouping, which is what I would expect, only the two data rows in the data object are still ungrouped, even though they are identical. I'm out of google-fu, can anybody please assist?

You could combine the fields to group by into one key (probably a string) and then group by that value:
<!-- language: lang-vb -->
Dim groupedData2 = data.GroupBy(Function(r)
Dim groupKey As New List(Of String)
For Each column In groupByColumns
If column.Value Then
Dim p = r.GetType().GetProperties().FirstOrDefault(Function(p) p.Name = column.Key)
Dim groupingValue = p?.GetValue(r)
groupKey.Add(groupingValue)
End If
Next
Return String.Join(",", groupKey)
End Function)

Related

how to check that a list contains another list item in VB.Net

I have two lists like this.
Dim List1 As IList(Of String) = New List(Of String)
Dim ListMatch As IList(Of String) = New List(Of String)
I need to figure out if List1 contains all items of ListMatch. How can i do this is VB.Net?
You can use Not SmallCollection.Except(LargeCollection).Any:
Dim containsAll = Not ListMatch.Except(List1).Any()
Documentation of Enumerable.Except:
This method returns those elements in first that do not appear in
second. It does not also return those elements in second that do not
appear in first.
Since Except is a set method, it doesn't take duplicates into account. So if you also want to know if List1 contains the same count of items of ListMatch you could use(less efficient):
Dim list1Lookup = List1.ToLookup(Function(str) str)
Dim listMatchLookup = ListMatch.ToLookup(Function(str) str)
Dim containsSameCount = listMatchLookup.
All(Function(x) list1Lookup(x.Key).Count() = x.Count())

How to make a generic database record to object conversion function?

I am using the following function to retrieve records from a database and convert the records to a collection of strongly typed objects.
Private Function GetPlantSettingsFiltered(parameters As Dictionary(Of String, Object), queryCondition As String) As PlantSettings
Dim query As String
query = " SELECT * FROM Plant_Settings " _
+ queryCondition
Dim settings As New PlantSettings
Dim table As DataTable = GetQueryResults(parameters, query, GetConnectionString("WeighScaleDB"))
If table Is Nothing Then
Return settings
End If
For Each row As DataRow In table.Rows
settings.Add(New PlantSetting With {
.Setting_ID = ConvertByteArrayToString(TryCast(row("Setting_ID"), Byte())),
.Plant_ID = ConvertByteArrayToString(TryCast(row("Plant_ID"), Byte())),
.Value = row("Setting_Value").ToString(),
.Comments = row("Setting_Comments").ToString()
})
Next
Return settings
End Function
I would like to create a generic version of this function that would work for any of my objects without me creating this function for each object.
For example, if the caller could specify the type, then some other details, the function would return a collection of that type.
Private Function GetObjects(Of T)(parameters As Dictionary(Of String, Object), query As String) As WSAEntityCollection(Of T)
Dim objectCollection As New WSAEntityCollection(Of T)
Dim table As DataTable = GetQueryResults(parameters, query, GetConnectionString("WeighScaleDB"))
If table Is Nothing Then
Return objectCollection
End If
For Each row As DataRow In table.Rows
' Here is my problem
objectCollection.Add(New T With {})
Next
Return objectCollection
End Function
My current problem with this new function is that I do not know how to dynamically match the column names with the parameters of the generic object. Any ideas on how this could be done?

How to sort SortedDictionary by values in .NET 2.0?

I have a SortedDictionary:
Dim myFilterItems As SortedDictionary(Of String, FilterItem)
myFilterItems = New SortedDictionary(Of String, FilterItem)(StringComparer.CurrentCultureIgnoreCase)
The FilterItem class is defined like this:
Private Class FilterItem
Public ValueToSort As Object
Public IsChecked As Boolean
Public IsAbsent As Boolean = False
End Class
I need to enumerate my SortedDictionary sorted by the FilterItem.ValueToSort property. With LINQ, it's easy to do - we get the corresponding IEnumerable and then use For Each:
Dim mySortedValueList As IEnumerable(Of KeyValuePair(Of String, FilterItem))
mySortedValueList = From entry In myFilterItems Order By entry.Value.ValueToSort Ascending
For Each entry As KeyValuePair(Of String, FilterItem) In mySortedValueList
' ...
Next
How to do that effectively in .NET 2.0?
I rewrote my code using Lists as Jon Skeet suggested. As I can see from my tests, I have the same performance like with the LINQ query - or even 5-10% gain. The new version of code looks like this:
Dim mySortedValueList As New List(Of KeyValuePair(Of String, FilterItem))(myFilterItems)
If iArr <> iGAFMValueType.Text Then
mySortedValueList.Sort(AddressOf CompareFilterItemsByValuesToSort)
End If
Private Shared Function CompareFilterItemsByValuesToSort(ByVal itemX As KeyValuePair(Of String, FilterItem), ByVal itemY As KeyValuePair(Of String, FilterItem)) As Integer
Dim myValueX As IComparable = TryCast(itemX.Value.ValueToSort, IComparable)
Dim myValueY As IComparable = TryCast(itemY.Value.ValueToSort, IComparable)
If (myValueX Is Nothing) Then
If (myValueY Is Nothing) Then
Return 0
End If
Return -1
End If
If (myValueY Is Nothing) Then
Return 1
End If
Dim myTypeX As Type = myValueX.GetType()
Dim myTypeY As Type = myValueY.GetType()
If (myTypeX <> myTypeY) Then
Return String.CompareOrdinal(myTypeX.Name, myTypeY.Name)
End If
Return myValueX.CompareTo(myValueY)
End Function
However, while working on this comparison algorithm, I found that I can't compare values of some numeric types - though they can be compared (for instance, SByte and Double values). BTW, the original LINQ query also fails in this case. But this is another story that continues in this question:
How to compare two numeric values (SByte, Double) stored as Objects in .NET 2.0?

SQL LINQ Query: Selecting specific column

Today I am needing to write LINQ queries in VB.net to a database table, but am new to SQL/LINQ. This function below is meant to fill a list of strings with all of the possible "Questions" in the database table that match the QuestionType.
However, I only want to select one single column, the QuestionText column, and not all of the data, whenever I have a match.
Public Shared Function RetrieveQuestions(ByVal QuestionType) As List(Of String)
Dim db As New DBDataContext()
db.CommandTimeout = 300
Dim ListOfQuestions As List(Of String) = New List(Of String)
While True
Dim questionList As List(Of Question) = db.Questions.ToList
Dim question As List(Of String) = (From q As Question In questionList Where q.FormType = QuestionType Select q.QuestionText).ToList
Dim i As List(Of String) = question
If (question IsNot Nothing) Then
ListOfQuestions(ListOfQuestions.Count) = i.QuestionText //ERROR
Else
Exit While
End If
End While
Return ListOfQuestions
End Function
In the function above i am encountering an error when trying to update my list with the new QuestionText. "QuestionText is not a member of System.Collections.Generic.List(Of String)". QuestionText is defined as a varchar in my SQL database, so I know that it is definitely a string. I am not trying to set QuestionText to a list of strings, but rather add it to the end of a list of strings.
Direct answer: you'd need to put the whole If (question IsNot Nothing) Then block in a loop like For Each. As the compiler correctly informs - the i variable holds the whole list, not one of its items. Perhaps you forgot you left the LINQ query?
A better solution: I believe you could just use AndAlso q.QuestionText IsNot Nothing - it spares you the need to allocate a new list and to fill it one by one - the following code should do the trick.
Public Shared Function RetrieveQuestions(ByVal QuestionType) As List(Of String)
Dim db As New DBDataContext()
db.CommandTimeout = 300
Dim ListOfQuestions As List(Of String) = (
From q As Question In db.Questions.ToList
Where
q.FormType = QuestionType
AndAlso q.QuestionText IsNot Nothing
Select q.QuestionText
).ToList
Return ListOfQuestions
End Function

VB.Net I'm trying to write an extension for a generic linq search, however I'm not sure how to return more than one result 0.o

I'm a bit new to vb.net and used to working in perl, this is what I'd like to do.
I wanted something similar to DBIX::Class::Resultset's search (from cpan) in my vb.net project, so that I can give my function a hash containing keys and values to search on a table.
Currently it returns a single matching result of type T where I want it to return all results as a data.linq.table(of T)
How should I alter my expression.lambda so that I can say table.Select(Predicate) to get a set of results? After that I think it should be as simple as saying results.intersect(result) instead of Return test.
Any help will be very much appreciated.
Thanks in advance
-Paul
<System.Runtime.CompilerServices.Extension()> _
Public Function Search(Of T As Class)(ByVal context As DataContext, _
ByVal parameters As Hashtable) As T
Dim table = context.GetTable(Of T)()
Dim results As Data.Linq.Table(Of T)
For Each Parameter As DictionaryEntry In parameters
Dim column As Object = Parameter.Key
Dim value As String = Parameter.Value
Dim param = Expression.Parameter(GetType(T), column)
Dim Predicate = Expression.Lambda(Of Func(Of T, Boolean)) _
(Expression.[Call](Expression.Convert(Expression.Property(param, column), _
GetType(String)), GetType(String).GetMethod("Contains"), _
Expression.Constant(value)), New ParameterExpression() {param})
Dim test = table.First(Predicate)
Return test
' result.intersect(result)
Next
'Return results
End Function
This works assuming you want an "AND" conjunction between predicates
For instance:
Dim h = New System.Collections.Hashtable
h.Add("FieldA", "01 5149")
h.Add("FieldB", "WESTERN")
Dim t = (New DBDataContext).Search(Of DBrecord)(h)
Debug.Print(t.Count.ToString)
Would return those records where fieldA matched AND fieldb matched.
If you wanted OR, DiceGuy's right, use UNION.
Here's the search...
Note, I used STARTSWITH instead of contains because it's alot faster for large sets
You can always change it back.
<System.Runtime.CompilerServices.Extension()> _
Public Function Search(Of T As Class)(ByVal context As DataContext, _
ByVal parameters As Hashtable) As IQueryable(Of T)
Dim table = context.GetTable(Of T)()
Dim results As IQueryable(Of T) = Nothing
For Each Parameter As DictionaryEntry In parameters
Dim column = DirectCast(Parameter.Key, String)
Dim value As String = DirectCast(Parameter.Value, String)
Dim param = Expression.Parameter(GetType(T), column)
Dim Predicate = Expression.Lambda(Of Func(Of T, Boolean)) _
(Expression.[Call](Expression.Convert(Expression.Property(param, column), _
GetType(String)), GetType(String).GetMethod("StartsWith", New Type() {GetType(String)}), _
Expression.Constant(value)), New ParameterExpression() {param})
Dim r = table.Where(Predicate)
If results Is Nothing Then
results = r
Else
results = results.Intersect(r)
End If
Next
Return results
End Function
Well, for starters let's change the return type to Data.Linq.Table(Of T).
Then instead of table.First(Predicate), try table.Where(Predicate)
Finally 'Intersect' will only give you results that contain all your parameters. If that's what you want, then fantastic! If not, then try 'Union' instead.
Let me know where that gets you and we can work from there.