LINQ SqlMethods.Like (and .Contains) fails - vb.net

I have a class called Person which has the properties FirstName, LastName and MiddleName and I have a form-wide SortedDictionary(Of Integer, Person) called oPeople.
On Form_Load, I call a method that loads a list of 65 people. Right now this is hard-coded but eventually I'll be grabbing it from a database.
Once the form is loaded, I have a TextBox called txtSearchForName for the user to enter a search term and have the system look through oPeople filtering on LastName for a full or partial match (case insensitive).
Eventually I would like to be able to search for comparisons between FirstName, LastName and MiddleName (if there is one).
At this point all I want to do is loop through the results of the LINQ query and output them to the console window.
Here's the Person class:
Public Class Person
Private _fnm As String = String.Empty
Public Property FirstName() As String
Get
Return _fnm
End Get
Set(ByVal value As String)
_fnm = value.Trim
End Set
End Property
Private _lnm As String = String.Empty
Public Property LastName() As String
Get
Return _lnm
End Get
Set(ByVal value As String)
_lnm = value.Trim
End Set
End Property
Private _mnm As String = String.Empty
Public Property MiddleName() As String
Get
Return _mnm
End Get
Set(ByVal value As String)
_mnm = value.Trim
End Set
End Property
Public Sub New()
End Sub
Public Sub New(ByVal firstName As String,
ByVal lastName As String,
Optional ByVal middleName As String = "")
_fnm = firstName
_lnm = lastName
_mnm = middleName
End Sub
End Class
This is the method I'm using to add people. I'm adding 65 people but have cut the code down:
Private Sub FillPeopleDictionary()
Try
If oPeople.Count > 0 Then oPeople.Clear()
Dim oNewPerson As Person = Nothing
oNewPerson = New Person("Scarlett", "Johansson")
oPeople.Add(1, oNewPerson)
oNewPerson = New Person("Amy", "Adams")
oPeople.Add(2, oNewPerson)
oNewPerson = New Person("Jessica", "Biel")
oPeople.Add(3, oNewPerson)
Catch ex As Exception
MessageBox.Show(ex.Message, "Error [FillPeopleDictionary]", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
This is my LINQ statement followed by the output to console which is called when the user clicks a button:
Dim sSearchTerm As String = txtSearchForName.Text.Trim.ToLower
Dim queryResults = From person In oPeople
'Where SqlMethods.Like(person.Value.LastName.ToLower, "%" & sSearchTerm & "%")
'Where person.Value.LastName.ToLower.Contains("%" & sSearchTerm & "%")
Console.WriteLine("search term: " & sSearchTerm &
Environment.NewLine & Environment.NewLine &
"queryResults.Count: " & queryResults.Count.ToString &
Environment.NewLine)
For Each result In queryResults
If Not String.IsNullOrEmpty(result.Value.MiddleName) Then
Console.WriteLine(result.Key.ToString.PadLeft(2, "0") & ": " & result.Value.FirstName & " " & result.Value.MiddleName & " " & result.Value.LastName)
Else
Console.WriteLine(result.Key.ToString.PadLeft(2, "0") & ": " & result.Value.FirstName & " " & result.Value.LastName)
End If
Next
The LINQ statement works as it stands, with no conditions, so it loops through and correctly lists all of the people in the oPeople collection.
There are two Where clauses commented out below the initial queryResults statement. Those are the two ways I was trying to filter. One approach was to use .Contains and the other was to use .Like however neither works.
If the user was to type "mar", I would hope to get back a list of 6 people from the list of 65(case insensitive):
Meghan Markle
Margo Robbie
Kate Mara
Mary Elizabeth Winstead
Marian Rivera
Amy Smart
Now of course that is searching on FirstName and LastName. Right now I am just trying to get LastName to work. With only the LastName the list would only be:
Meghan Markle
Kate Mara
Amy Smart
Can anyone see what I am doing wrong here? Or should I scrap the idea of using LINQ with a SortedDictionary?

Change your Person class to include a PersonId and pass that through like oNewPerson = New Person(1, "Scarlett", "Johansson").
Change the oPeople to be a List(Of Person) so when adding it would look like this oPeople.Add(oNewPerson).
Your LINQ statement would then look like this:
Dim queryResults = From person In oPeople
Where person.FirstName.ToLower Like "*" & sSearchTerm & "*" Or
person.LastName.ToLower Like "*" & sSearchTerm & "*"
You would also want to change the rest as no longer using a dictionary:
For Each result In queryResults
If Not String.IsNullOrEmpty(result.MiddleName) Then
Console.WriteLine(result.PersonId.ToString.PadLeft(2, CChar("0")) & ": " & result.FirstName & " " & result.MiddleName & " " & result.LastName)
Else
Console.WriteLine(result.PersonId.ToString.PadLeft(2, CChar("0")) & ": " & result.FirstName & " " & result.LastName)
End If
Next
Hope this helps.

Your 2nd Where clause attempt is close, except the Contains function there is String.Contains which does not use the % wildcard characters that SQL uses, so you need:
Dim queryResults =
From person In oPeople
Where person.Value.LastName.ToLower.Contains(sSearchTerm)
You can easily add a check for FirstName with OrElse person.Value.FirstName.ToLower.Contains(sSearchTerm).

Change your Linq query to be as follows:
Dim queryResults = From p In oPeople
Where p.Value.FirstName.ToLower.Contains(sSearchTerm) Or p.Value.LastName.ToLower.Contains(sSearchTerm)

Related

Collection List empties itself

I have a class for adding parameters to a database (PostgreSQL) connection object that inherits System.Collections.CollectionBase. This class is in a common library I use for multiple applications. Usually, it adds the objects without a problem, but I've started encountering a strange issue where the collection empties its objects for no apparent reason. This results in an error with the SQL statement not being built properly.
ENVIRONMENT
Windows 10 Pro x64 (19042.804)
Visual Studio 2017 CE (4.8.0484)
.NET Framework 4.7.2 (Library & Application)
PostgreSQL Server v12.1
Npgsql 5.0.3 (see below)
TROUBLESHOOTING
Here's the (obfuscated) code where the issue is occurring:
SQLCommand = "INSERT INTO table" & vbCrLf
SQLCommand += "(" & vbCrLf
SQLCommand += vbTab & "column1," & vbCrLf
SQLCommand += vbTab & "column2," & vbCrLf
SQLCommand += vbTab & "column3," & vbCrLf
SQLCommand += vbTab & "column4," & vbCrLf
SQLCommand += vbTab & "column5," & vbCrLf
SQLCommand += vbTab & "column6," & vbCrLf
SQLCommand += vbTab & "column7," & vbCrLf
SQLCommand += vbTab & "column8," & vbCrLf
SQLCommand += vbTab & "column9," & vbCrLf
SQLCommand += vbTab & "column10" & vbCrLf
SQLCommand += ")" & vbCrLf
SQLCommand += "VALUES" & vbCrLf
SQLCommand += "(" & vbCrLf
SQLCommand += vbTab & ":parameter1," & vbCrLf
SQLCommand += vbTab & ":parameter2," & vbCrLf
SQLCommand += vbTab & ":parameter3," & vbCrLf
SQLCommand += vbTab & ":parameter4," & vbCrLf
SQLCommand += vbTab & ":parameter5," & vbCrLf
SQLCommand += vbTab & ":parameter6," & vbCrLf
SQLCommand += vbTab & ":parameter7," & vbCrLf
SQLCommand += vbTab & ":parameter8," & vbCrLf
SQLCommand += vbTab & ":parameter9," & vbCrLf
SQLCommand += vbTab & ":parameter10" & vbCrLf
SQLCommand += ")" & vbCrLf
SQLCommand += "RETURNING pkid"
PGDB.Parameters.Add(":parameter1", value1) 'String
PGDB.Parameters.Add(":parameter2", value2) 'String
PGDB.Parameters.Add(":parameter3", value3) 'String
PGDB.Parameters.Add(":parameter4", value4) 'String
PGDB.Parameters.Add(":parameter5", value5) 'String
PGDB.Parameters.Add(":parameter6", value6) 'String
PGDB.Parameters.Add(":parameter7", value7) 'String - THIS IS WHERE THE LIST CLEARS
PGDB.Parameters.Add(":parameter8", value8) 'Boolean
PGDB.Parameters.Add(":parameter9", value9) 'String
PGDB.Parameters.Add(":parameter10", value10) 'String
MyID = Convert.ToInt32(PGDB.ExecuteStatementScalar(SQLCommand))
The PGDB object is my Npgsql database connection object and the Parameters object is the inherited collection. The first six parameters add to the collection without issue, but as soon as it goes to add the seventh, the entire list empties itself and starts over. The executing SQL statement should look like this:
-- EXPECTED SQL - WHAT IS STORED IN THE SQLCommand VARIABLE
INSERT INTO table
(
column1,
column2,
column3,
column4,
column5,
column6,
column7,
column8,
column9,
column10
)
VALUES
(
:parameter1,
:parameter2,
:parameter3,
:parameter4,
:parameter5,
:parameter6,
:parameter7,
:parameter8,
:parameter9,
:parameter10
)
RETURNING pkid
...but, what I get instead is this:
-- ACTUAL (WRONG) SQL EXECUTED BY THE DATABASE
INSERT INTO table
(
column1,
column2,
column3,
column4,
column5,
column6,
column7,
column8,
column9,
column10
)
VALUES
(
:parameter1,
:parameter2,
:parameter3,
:parameter4,
:parameter5,
:parameter6,
$1,
$2,
$3,
$4
)
RETURNING pkid
...which then generates an Npgsql.PostgresException complaining about the syntax when it actually tries to execute the SQL statement:
42601: syntax error at or near ":"
I've put in breakpoints to step through the process but it's always the same behavior. Here are some screenshots from my IDE:
Here's what the Parameters collection object looks like before the first parameter is added:
Here it is after the sixth parameter is added:
And here's what it looks like as soon as it enters execution of the Add() method on the seventh parameter:
To be sure, I also checked the state as it enters execution of the Add() method on the sixth parameter:
This method worked without error at one time, so I'm not sure why it "all of a sudden" stopped working. In my attempt to fix this and get all of the parameters to load correctly, I upgraded the Npgsql library from version 4.1.8 to 5.0.3. After I fought with it for a while - I had to resolve some version conflicts with the System.Buffers library - I was able to get it running again but, unfortunately, I got the exact same results.
Just in case it might be a memory issue, I went ahead and shut down everything and rebooted the computer. That also did not resolve the issue.
For reference, here's a trimmed-down version of the PGSQLParameters class, excluding various overloads for different value types.
Imports Npgsql
#Region "POSTGRESQL PARAMETER COLLECTION OBJECT"
Public Class PGSQLParameters
Inherits System.Collections.CollectionBase
Public Enum SQLDecimalType
SQLMoney = 1
SQLDecimal = 2
End Enum
#Region "COLLECTION ADD AND SUPPORT METHODS"
#Region "PUBLIC METHODS FOR ADDING ITEMS TO THE COLLECTION"
'-- Input Parameter
Public Overloads Sub Add(ByVal ParameterName As String, ByVal DataType As DbType, ByVal Size As Int32, ByVal Value As Object)
If ParameterName.Length = 0 Then
Return
End If
If TypeOf (Value) Is String AndAlso String.IsNullOrEmpty(Convert.ToString(Value)) Then
Value = DBNull.Value
End If
If Not ParameterName.StartsWith(":") Then
ParameterName = ParameterName.Insert(0, ":")
End If
List.Add(BuildParameter(ParameterName, DataType, Size, ParameterDirection.Input, Value))
End Sub
#Region "INPUT OVERLOADS"
'-- String
Public Overloads Sub Add(ByVal ParameterName As String, ByVal Value As String)
Dim StringLength As Integer = 0
If Not Value = Nothing AndAlso Not String.IsNullOrEmpty(Value) Then
StringLength = Value.Length
End If
Add(ParameterName, DbType.String, StringLength, Value)
End Sub
#End Region
#End Region
Private Function BuildParameter(ByVal ParameterName As String, ByVal DataType As DbType, ByVal Size As Int32, ByVal Direction As ParameterDirection, ByVal Value As Object) As NpgsqlParameter
Dim NewParameter As NpgsqlParameter
If Size > 0 Then
NewParameter = New NpgsqlParameter(ParameterName, DataType, Size)
Else
NewParameter = New NpgsqlParameter(ParameterName, DataType)
End If
NewParameter.Direction = Direction
If Not (Direction = ParameterDirection.Output AndAlso Value Is Nothing) Then
NewParameter.Value = DBNull.Value
If Not Value Is Nothing Then
NewParameter.Value = Value
ElseIf TypeOf (Value) Is Boolean Then
NewParameter.Value = Value
End If
End If
Return NewParameter
End Function
#End Region
End Class
#End Region
I don't see any reason for this spontaneous "clearing" of the Parameters collection, especially since all of the parameters leading up to and including the "problem" are String values, so they're all using the exact same method call (I've confirmed this in my walk-through). Additionally, based on the executing SQL, it looks like it's actually somehow preserving the first six parameters in the collection object and just not adding the new ones, which makes absolutely no sense.
Also, it doesn't look like it's completely reinstantiating the object because, as the screenshots show, the Capacity doesn't change from the sixth to the seventh parameter. If I had to say anything, it looks like it's cloning the collection after six parameters have been added, then discarding that clone and/or ignoring it completely. Again, it just makes no sense to me.
I wouldn't doubt that I'm simply overlooking something, but I have no idea what that "something" would be. Has anyone else encountered this type of behavior? Any help or ideas would be greatly appreciated. If I can or need to provide any additional details, let me know.
Might need to post more code - the posted code doesn't seem to exhibit any problem for me:
Drag this icon to your desktop, rename it as .zip and open it:
It has the exact solution files I used to check your issue (it doesn't show the issue on my computer)
I've managed to solve the issue, but the cause is something I wouldn't have expected and probably wouldn't be obvious without a complete listing of a good deal more code. I apologize for not providing enough detail in the original question, but I hadn't guessed where I'd eventually find the cause of the issue.
As I indicated above, the issue always occurred with the same parameter in the sequence. The value I was attempting to assign to that specific parameter was a Public Property (String) of a Lazy(Of T) object in another class.
The object in question looks something like this:
#Region "REAL ESTATE OBJECT"
''' <summary>
''' Standard object containing details about a specific piece of real estate
''' </summary>
Public Class RealEstate
#Region "PRIVATE FIELDS"
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _RealEstateTypeCode As String
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _PhysicalStreet1 As String
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _PhysicalStreet2 As String
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _PhysicalCity As String
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _PhysicalState As String
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private _PhysicalZIPCode As String
#End Region
#Region "PRIVATE PROPERTIES"
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private Property PGDB As PGSQLDB
#Region "LAZY PROPERTIES"
' >> THIS IS THE PROPERTY THAT APPEARS TO HAVE CAUSED THE ISSUE <<
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private Property _RealEstateType As Lazy(Of PropertyType) =
New Lazy(Of PropertyType)(Function()
Return New PropertyType(_RealEstateTypeCode, Me.CIADB)
End Function)
#End Region
#End Region
#Region "PUBLIC PROPERTIES"
Public Property RealEstateID As Integer
Public Property PhysicalStreet1 As String
Get
Return _PhysicalStreet1
End Get
Set(value As String)
If Not value Is Nothing Then
If value.Trim.Length > 120 Then
_PhysicalStreet1 = value.Trim.Substring(0, 120).Trim.ToUpper
Else
_PhysicalStreet1 = value.Trim.ToUpper
End If
Else
_PhysicalStreet1 = String.Empty
End If
End Set
End Property
Public Property PhysicalStreet2 As String
Get
Return _PhysicalStreet2
End Get
Set(value As String)
If Not value Is Nothing Then
If value.Trim.Length > 120 Then
_PhysicalStreet2 = value.Trim.Substring(0, 120).Trim.ToUpper
Else
_PhysicalStreet2 = value.Trim.ToUpper
End If
Else
_PhysicalStreet2 = String.Empty
End If
End Set
End Property
Public Property PhysicalCity As String
Get
Return _PhysicalCity
End Get
Set(value As String)
If Not value Is Nothing Then
If value.Trim.Length > 60 Then
_PhysicalCity = value.Trim.Substring(0, 60).Trim.ToUpper
Else
_PhysicalCity = value.Trim.ToUpper
End If
Else
_PhysicalCity = String.Empty
End If
End Set
End Property
Public Property PhysicalState As String
Get
If _PhysicalState Is Nothing OrElse String.IsNullOrEmpty(_PhysicalState) OrElse _PhysicalState = "XX" Then
If Not _PhysicalZIPCode Is Nothing AndAlso Not String.IsNullOrEmpty(_PhysicalZIPCode) Then
_PhysicalState = Utility.GetStateCodeFromZIP(_PhysicalZIPCode)
End If
End If
Return _PhysicalState
End Get
Set(value As String)
If Not value Is Nothing Then
If value.Trim.Length > 2 Then
_PhysicalState = Utility.GetStateCodeFromName(value.Trim)
Else
If [Enum].IsDefined(GetType(USState), value.Trim.ToUpper) Then
Dim PState As USState
If [Enum].TryParse(value.Trim.ToUpper, PState) Then
_PhysicalState = Utility.GetStateCodeFromName(PState.GetEnumDescription)
End If
Else
_PhysicalState = value.Trim.ToUpper
End If
End If
Else
_PhysicalState = "OK"
End If
End Set
End Property
Public Property PhysicalZIPCode As String
Get
Return _PhysicalZIPCode
End Get
Set(value As String)
If Not value Is Nothing Then
If value.Trim.Length > 10 Then
_PhysicalZIPCode = value.Trim.Substring(0, 10).Trim.ToUpper
Else
_PhysicalZIPCode = value.Trim.ToUpper
End If
Else
_PhysicalZIPCode = String.Empty
End If
End Set
End Property
Public WriteOnly Property RealEstateTypeCode As String
Set(value As String)
_RealEstateTypeCode = value
End Set
End Property
Public ReadOnly Property RealEstateType As PropertyType
Get
Return _RealEstateType.Value
End Get
End Property
#End Region
In my original obfuscated code I simply injected a generic string as a placeholder, but it appears that the issue has everything to do with this specific assignment. The other values being added to the parameter list are "regular" properties of the object that are immediately accessible. But, something seems to "short circuit" when calling the Add() method using a Lazy(Of T) object for the Value parameter.
In order to "fix" the issue, I've declared an additional local String variable that retrieves the value of the needed property from this Lazy(Of T) object. I then use that local variable for assigning to the Add() method's Value parameter:
With REObject 'A RealEstate object
' > DECLARING THIS LOCAL VARIABLE TO RETRIEVE THE VALUE OF THE LAZY PROPERTY
Dim TypeCode As String = .RealEstateType.TypeCode
...
PGDB.Parameters.Add("street1", .PhysicalStreet1)
PGDB.Parameters.Add("street2", .PhysicalStreet2)
PGDB.Parameters.Add("city", .PhysicalCity)
PGDB.Parameters.Add("state", .PhysicalState)
PGDB.Parameters.Add("zip", .PhysicalZIPCode)
' > CHANGED THE FOLLOWING
' PGDB.Parameters.Add("type", .RealEstateType.TypeCode) 'String - THIS IS WHERE THE LIST CLEARS
' > TO USE THE LOCAL VARIABLE INSTEAD OF THE LAZY OBJECT'S PROPERTY VALUE
PGDB.Parameters.Add("type", TypeCode)
' > THE VALUE IS NOW SUCCESSFULLY ADDED
...
End With
Executing this code results in all of the parameters correctly populating with the correct values and the SQL statement executing in the database without error. So, it would seem that the fact that the Lazy(Of T) object hasn't been "filled" yet is causing some unexpected (for me, at least) behavior when entering the Add() method. To be honest, I'm still not sure why it was "resetting" the .List property of the Collection object when attempting to use the Lazy(Of T) object's property value, but at least this resolution achieves the desired goal. Again, I apologize for not providing enough detail initially, but hopefully this will help someone else.

System.IndexOutOfRangeException VB GradeBook

So I get this error when trying to run my code "System.IndexOutOfRangeException: 'Index was outside the bounds of the array.'" I am not exactly sure what the issue is. The user should be able to enter 3 assignment grades and then see the averages
I marked the line where I get the error with 'THIS IS THE ERROR'
Public Class GradeBook
Dim grade(9, 2) As Integer 'Store 10 student grades on 3 tests'
Dim studentCount As Integer = 0 'Number of students entered'
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
GradeListBox.Items.Add(vbTab & vbTab & "Test 1" & vbTab & "Test 2" & vbTab & "Test 3" & vbTab & "Average")
End Sub
Private Sub SubmitButton_Click(sender As Object, e As EventArgs) Handles SubmitButton.Click
grade(studentCount, 0) = Convert.ToInt32(Assignment1TextBox.Text)
grade(studentCount, 1) = Convert.ToInt32(Assignment2TextBox.Text)
grade(studentCount, 2) = Convert.ToInt32(Assignment3TextBox.Text)
Dim output As String = "Student " & studentCount & vbTab
For column = 0 To grade.GetUpperBound(1)
If LetterRadioButton.Checked = True Then
output &= vbTab & LetterGrade(grade(studentCount, column))
Else
output &= vbTab & grade(studentCount, column)
End If
Next
output &= vbTab & CalculateStudentAverage(studentCount)
GradeListBox.Items.Add(output)
studentCount += 1
AverageLabel.Text = CalculateClassAverage()
displayBarChart()
End Sub
Function LetterGrade(ByVal grade As Double) As String
Dim output As String = ""
Select Case grade
Case Is >= 90
output = "A"
Case Is >= 80
output = "B"
Case Is >= 70
output = "C"
Case Is >= 60
output = "D"
Case Is >= 50
output = "E"
End Select
Return output
End Function
Function CalculateStudentAverage(ByVal row As Integer) As String
Dim gradeTotal As Integer = 0
For column = 0 To grade.GetUpperBound(1)
gradeTotal += grade(row, column) 'THIS IS THE ERROR'
Next
Dim studentAverage As String = String.Empty
If LetterRadioButton.Checked = True Then
studentAverage = LetterGrade(gradeTotal / (grade.GetUpperBound(1) + 1))
Else
studentAverage = String.Format("{0:F}", (gradeTotal / grade.GetUpperBound(1) + 1))
End If
Return studentAverage
End Function
Function CalculateClassAverage() As String
Dim classTotal As Integer = 0
For row = 0 To studentCount - 1
For column = 0 To grade.GetUpperBound(1)
classTotal += grade(row, column)
Next
Next
Dim classAverage As String = String.Empty
If LetterRadioButton.Checked = True Then
classAverage = LetterGrade(classTotal / (studentCount * (grade.GetUpperBound(1) + 1)))
Else
classAverage = String.Format("{0:F}", (classTotal / (studentCount * (grade.GetUpperBound(1) + 1))))
End If
Return classAverage
End Function
Sub displayBarChart()
GradeListBox.Items.Clear()
GradeListBox.Items.Add(vbTab & vbTab & "Test 1" & vbTab & "Test 2" & vbTab & "Test 3" & vbTab & "Average")
For row = 0 To studentCount - 1
Dim output As String = "Student " & row & vbTab
For column = 0 To grade.GetUpperBound(1)
If LetterRadioButton.Checked = True Then
output &= vbTab & LetterGrade(grade(row, column))
Else
output &= vbTab & (grade(row, column))
End If
Next
output &= vbTab & CalculateStudentAverage(studentCount)
GradeListBox.Items.Add(output)
studentCount += 1
AverageLabel.Text = CalculateClassAverage()
displayBarChart()
Assignment1TextBox.Clear()
Assignment2TextBox.Clear()
Assignment3TextBox.Clear()
If studentCount = grade.GetUpperBound(0) + 1 Then
InputGradeGroupBox.Enabled = False
End If
Next
End Sub
End Class
To be honest, I suspect you're going about it in a way not suited to OOP. Using arrays for multiple peices of data is prone to error and harder to maintain later.
Lets have a think about how you want to represent your data. You have a bunch of students. In a more complete example each student would have a name, personal details such as address, a list of the courses they're taking and for each course, a list of assignments and scores for each assignment which can also be represented as a grade.
So in essence you have a class called Student and the various bits of information are properties of that student.
Finally you have a list of these students
In your code, you simply have a student number and three assignment scores.
Your basic student class should have the properties of Number, Test1Score, Test2Score and Test3Score. You would also want to be able to get the grade from each score, the average of all the scores and the average of all the grades.
The code below defines the Student class with that information in mind
Friend Class Student
Public Property Number As Integer
Public Property Test1Score As Integer
Public Property Test2Score As Integer
Public Property Test3Score As Integer
Public Function Test1Grade() As String
Return LetterGrade(Test1Score)
End Function
Public Function Test2Grade() As String
Return LetterGrade(Test2Score)
End Function
Public Function Test3Grade() As String
Return LetterGrade(Test3Score)
End Function
Friend Shared Function LetterGrade(ByVal grade As Double) As String
Dim output As String = ""
Select Case grade
Case Is >= 90
output = "A"
Case Is >= 80
output = "B"
Case Is >= 70
output = "C"
Case Is >= 60
output = "D"
Case Is >= 50
output = "E"
Case Else
output = "F"
End Select
Return output
End Function
Friend Function AverageScore() As Double
Return (Test1Score + Test2Score + Test3Score) / 3
End Function
Friend Function AverageGrade() As String
Dim avgscore As Double = AverageScore()
Return LetterGrade(avgscore)
End Function
End Class
The function LetterGrade is Shared so that it can be used in your main form to calculate the average grade for the entire class.
OK Next think about how you want your program to work. You'll need a list of students, so at the beginning of your form class , you'll need this..
Public Class Form1
Private Students As New List(Of Student)
This way, instead of trying to keep track of your variable studentCount you can just use the .Count property of the List e.g Students.Count. You wont need to any adding to it as the Students list keeps track of it automatically.
Next, you'll want the function that calculates the average score/grade of the class..
Function CalculateClassAverage() As String
Dim classTotal As Integer = 0
For Each tmpStudent As Student In Students
With tmpStudent
classTotal += .Test1Score + .Test2Score + .Test3Score
End With
Next
Dim classAverage As String = String.Empty
If LetterRadioButton.Checked = True Then
classAverage = Student.LetterGrade((classTotal / Students.Count) / 3)
Else
classAverage = String.Format("{0:F}", (classTotal / (Students.Count)) / 3)
End If
Return classAverage
End Function
As you can see, there are a number of differences here because of the way your data is stored, but you should be able to follow it. One difference is the With statement, this allows you to save on typing. inside the With.. End With block, instead of typing tmpstudent.Test1Score etc, you can just type .Test1Score
The other thing of note is this line ..
classAverage = Student.LetterGrade( ... etc
You may think that there has been no declaration of a variable called Student. There IS a class, but not a variable. What this is actually doing is calling the LetterGrade function that is shared in the Student class definition.. Have a look at shared functions.
Next is the code to display the information in the ListBox. I haven't changed the name even though it is misleading btw.
Sub displayBarChart()
GradeListBox.Items.Clear()
GradeListBox.Items.Add(vbTab & vbTab & "Test 1" & vbTab & "Test 2" & vbTab & "Test 3" & vbTab & "Average")
For Each tmpstudent As Student In Students
Dim output As String = "Student " & tmpstudent.Number & vbTab
With tmpstudent
If LetterRadioButton.Checked = True Then
output &= .Test1Grade & vbTab & .Test2Grade & vbTab & .Test3Grade & vbTab & .AverageGrade
Else
output &= .Test1Score & vbTab & .Test2Score & vbTab & .Test3Score & vbTab & .AverageScore
End If
End With
GradeListBox.Items.Add(output)
AverageLabel.Text = CalculateClassAverage()
Next
If Students.Count = 10 Then
InputGradeGroupBox.Enabled = False
End If
End Sub
More use of the With..End With statements again, but the rest should be straighforward to follow.
A little extra bit of code.. When you click on the LetterRadioButton, the data in the listBox and ClassAveragelabel are refreshed with the appropriate letters/grades
Private Sub LetterRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles LetterRadioButton.CheckedChanged
AverageLabel.Text = CalculateClassAverage()
displayBarChart()
End Sub
Finally, to bring it all together, you have the code for the button click..
Private Sub SubmitButton_Click(sender As Object, e As EventArgs) Handles SubmitButton.Click
Dim tmpStudent As New Student With {.Number = Students.Count + 1,
.Test1Score = Convert.ToDouble(Assignment1TextBox.Text),
.Test2Score = Convert.ToDouble(Assignment2TextBox.Text),
.Test3Score = Convert.ToDouble(Assignment3TextBox.Text)}
Dim output As String = "Student " & tmpStudent.Number & vbTab
With tmpStudent
If LetterRadioButton.Checked = True Then
output &= vbTab & .Test1Grade & vbTab & .Test2Grade & vbTab & .Test3Grade
Else
output &= vbTab & .Test1Score & vbTab & .Test2Score & vbTab & .Test3Score
End If
End With
Students.Add(tmpStudent)
If LetterRadioButton.Checked Then
output &= vbTab & tmpStudent.AverageGrade
Else
output &= vbTab & tmpStudent.AverageScore
End If
GradeListBox.Items.Add(output)
AverageLabel.Text = CalculateClassAverage()
displayBarChart()
Assignment1TextBox.Clear()
Assignment2TextBox.Clear()
Assignment3TextBox.Clear()
End Sub
Hopefully in addition to my comment to your question, this provides a slightly better insight of OOP.

Access SubForm selection depends on Combo box

I would like to Filter my sub form based on my Combo box filter. I'm getting code error. I need help with this.
After Update I have written one event:
Private Sub cboSelected_AfterUpdate()
Dim MyName As String
MyName = " select * from [ITP_Checklist Log] where ([ITP_Checklist Log].[Name] = " & Me.cboSelected & " )"
Me.ITP_Checklist_Log_subform.Form.RecordSource = MyName
Me.ITP_Checklist_Log_subform.Form.Requery
End Sub
Error:
Run-time error '3464'
Data Type Mismatch in Criteria expression.
Use quotes for string values - and Requery is only needed if you don't change the recordsource:
Private Sub cboSelected_AfterUpdate()
Dim MyName As String
MyName = "select * from [ITP_Checklist Log] where ([ITP_Checklist Log].[Name] = '" & Me!cboSelected.Value & "')"
Debug.Print MyName
If Me!ITP_Checklist_Log_subform.Form.RecordSource = MyName Then
Me!ITP_Checklist_Log_subform.Form.Requery
Else
Me!ITP_Checklist_Log_subform.Form.RecordSource = MyName
End If
End Sub

using toupper and tolower in visual basic

Trying to get this to change the case of the first letter of the parsed strings segments. So if a user enters "JOHN WAYNE DOE" in txtName then it would display "John Wayne Doe"
I entered it the way it shows it in the book but the message box displays the parsed string however it was entered so in the above example the return is "JOHN WAYNE DOE"
I figure its a logic error since I am known to do that a lot just have no idea where i made the error.
Dim name As String = txtName.Text
name = name.Trim
Dim names() As String = name.Split(CChar(" "))
Dim firstName As String = names(0)
Dim middleName As String = names(1)
Dim lastName As String = names(2)
Dim firstLetters1 As String = firstName.Substring(0, 1).ToUpper
Dim otherletters1 As String = firstName.Substring(1).ToLower
Dim firstLetters2 As String = middleName.Substring(0, 1).ToUpper
Dim otherletters2 As String = middleName.Substring(1).ToLower
Dim firstletters3 As String = lastName.Substring(0, 1).ToUpper
Dim otherletters3 As String = lastName.Substring(1).ToLower
MessageBox.Show("First Name: " & firstName & vbCrLf & "Middle Name: " & middleName & vbCrLf & "Last Name: " & lastName)
Just to mention this alternative
Dim currentCulture As CultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture
Dim titleCase = currentCulture.TextInfo.ToTitleCase(txtName.Text)
Console.WriteLine(titleCase)
Dim names() As String = titleCase.Split(" "c)
......
This method ensure the proper casing of the string respecting the current culture.
And doesn't require so many direct splitting and string concatenations with the inherent memory footprint. (Internally, a StringBuilder is used to manipulate the input string, with just one final ToString() to return the result)
Try this:
MessageBox.Show(_
"First Name: " & firstLetters1 & otherletters1 & vbCrLf & _
"Middle Name: " & firstLetters2 & otherletters2 & vbCrLf & _
"Last Name: " & firstLetters3 & otherletters3)
String is immutable class, your ToUpper and ToLower calls create new instances. In the message box you are passing the old unprocessed instances.
Update
Alternatively, you can use our old call:
MessageBox.Show("First Name: " & firstName & vbCrLf & "Middle Name: " & middleName & vbCrLf & "Last Name: " & lastName)
as long as you do this before:
firstName = firstLetters1 & otherletters1
middleName = firstLetters2 & otherletters2
lastName = firstLetters3 & otherletters3
This might get you a better idea on how string's immutability works.
Dim Name As String = "JOHN WAYNE DOE"
Name = Microsoft.VisualBasic.StrConv(Name, VbStrConv.ProperCase)
The will give the output "John Wayne Doe"

Databind to object parameter that has one or more optional parameters

I have an object, called 'PERSON'
This person object has a title, firstName & Surname property as well as many other which are at the moment irrelevant. It also has a read only property called Fullname which concatenates the two or three parameters mentioned above depending on an optional parameter 'withTitles' passed over when you call PERSON.FULLNAME
PERSON.FULLNAME(true) <- Will add titles if there are any
PERSON.FULLNAME(false) <- Will give the name without the title
Public ReadOnly Property FullName(Optional ByVal withTitle As Boolean = False) As String
Get
Dim _ttle As String = Me.Title
Select Case withTitle
Case True
If _ttle.Length > 0 Then _ttle += " " Else _ttle = String.Empty
Case False
_ttle = String.Empty
End Select
If Me.FirstName <> "" And Me.LastName <> "" Then
Return _ttle & Me.FirstName & " " & Me.LastName
ElseIf Me.FirstName = "" And Me.LastName <> "" Then
Return _ttle & Me.LastName
ElseIf Me.FirstName <> "" And Me.LastName = "" Then
Return _ttle & Me.FirstName
ElseIf Me.FirstName = "" And Me.LastName = "" Then
Return Me.ContactName
End If
End Get
End Property
My problem surfaces when I try to bind my PERSONCOLLECTION (a collection of the PERSON object) to a RadioButtonList or any other binding control at that.
RadioButtonList1.DataSource = _personCollection
RadioButtonList1.DataTextField = "FullName"
RadioButtonList1.DataValueField = "ContactID"
RadioButtonList1.DataBind()
I get an error: PERSON does not contain a property of 'FullName'. If I change this to any other property that does not take a parameter it works as expected.
Now I'm guessing that the binding procedure can't handle optional or mandatory parameters for object properties, is this right? Is there a better way to do it?
I thought about looping through the collection to add them manually but that kinda defeats the object of DataBinding!
Any help would be apreciated.
Kev.
I would recommend creating two properties, FullName and FullNameWithTitle. Although properties can take parameters they are intended to be indexers into the object and not actionable values. For instance a class that represents a collection of Color objects might have an Item property with an optional index being the specific color to return.