Collection List empties itself - vb.net
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.
Related
Build a Message
I want to make a Message, using formatting, multiple lines and adding 3 arguments. But I'm having some trouble Public Class vbList 'Declare Dim users As IList(Of User) = New List(Of User)() Public Sub New() InitializeComponent() users.Add(New User With { .Id = 1, .Name = "Suresh Dasari", .Location = "Hyderabad" }) MsgBox("Id: {0}", users.Item(0).Id.ToString() & vbCrLf & "Name: {0}", users.Item(0).Name) & vbCrLf & "Location: {0}", users.Item(0).Location) End Sub I don't get this message below. Don't I need to convert the Id to a string to put it in a message? System.InvalidCastException: 'Conversion from string "1 Name: {0}" to type 'Integer' is not valid.' And whats up with this one, I can't have more than 2 arguments? Too many arguments to 'Public Function MsgBox(Prompt As Object, [Buttons As MsgBoxStyle = ApplicationModal], [Title As Object = Nothing]) As MsgBoxResult'. VSBasics C:\Users\ljhha\Documents\it\vb\VSBasics\VSBasics\vbList.vb 33 Active
Use String.Format or string interpolation to insert the variables. You can insert line breaks that way too or use a multiline literal in the first place. Dim str1 = String.Format("Date: {0}{1}Time: {2}", Date.Now.ToShortDateString(), Environment.NewLine, Date.Now.ToShortTimeString()) Dim str2 = $"Date: {Date.Now.ToShortDateString()}{Environment.NewLine}Time: {Date.Now.ToShortTimeString()}" Dim str3 = $"Date: {Date.Now.ToShortDateString()} Time: {Date.Now.ToShortTimeString()}" MessageBox.Show(str1) MessageBox.Show(str2) MessageBox.Show(str3)
How to make parametrized update statement [duplicate]
I've read a lot about SQL injection, and using parameters, from sources like bobby-tables.com. However, I'm working with a complex application in Access, that has a lot of dynamic SQL with string concatenation in all sorts of places. It has the following things I want to change, and add parameters to, to avoid errors and allow me to handle names with single quotes, like Jack O'Connel. It uses: DoCmd.RunSQL to execute SQL commands DAO recordsets ADODB recordsets Forms and reports, opened with DoCmd.OpenForm and DoCmd.OpenReport, using string concatenation in the WhereCondition argument Domain aggregates like DLookUp that use string concatenation The queries are mostly structured like this: DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = " & Me.SomeTextbox What are my options to use parameters for these different kinds of queries? This question is intended as a resource, for the frequent how do I use parameters comment on various posts
There are many ways to use parameters in queries. I will try to provide examples for most of them, and where they are applicable. First, we'll discuss the solutions unique to Access, such as forms, reports and domain aggregates. Then, we'll talk about DAO and ADO. Using values from forms and reports as parameters In Access, you can directly use the current value of controls on forms and reports in your SQL code. This limits the need for parameters. You can refer to controls in the following way: Forms!MyForm!MyTextbox for a simple control on a form Forms!MyForm!MySubform.Form!MyTextbox for a control on a subform Reports!MyReport!MyTextbox for a control on a report Sample implementation: DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Forms!MyForm!MyTextbox" 'Inserts a single value DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = Forms!MyForm!MyTextbox" 'Inserts from a different table This is available for the following uses: When using DoCmd.RunSQL, normal queries (in the GUI), form and report record sources, form and report filters, domain aggregates, DoCmd.OpenForm and DoCmd.OpenReport This is not available for the following uses: When executing queries using DAO or ADODB (e.g. opening recordsets, CurrentDb.Execute) Using TempVars as parameters TempVars in Access are globally available variables, that can be set in VBA or using macro's. They can be reused for multiple queries. Sample implementation: TempVars!MyTempVar = Me.MyTextbox.Value 'Note: .Value is required DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = TempVars!MyTempVar" TempVars.Remove "MyTempVar" 'Unset TempVar when you're done using it Availability for TempVars is identical to that of values from forms and reports: not available for ADO and DAO, available for other uses. I recommend TempVars for using parameters when opening forms or reports over referring to control names, since if the object opening it closes, the TempVars stay available. I recommend using unique TempVar names for every form or report, to avoid weirdness when refreshing forms or reports. Using custom functions (UDFs) as parameters Much like TempVars, you can use a custom function and static variables to store and retrieve values. Sample implementation: Option Compare Database Option Explicit Private ThisDate As Date Public Function GetThisDate() As Date If ThisDate = #12:00:00 AM# Then ' Set default value. ThisDate = Date End If GetThisDate = ThisDate End Function Public Function SetThisDate(ByVal NewDate As Date) As Date ThisDate = NewDate SetThisDate = ThisDate End Function and then: SetThisDate SomeDateValue ' Will store SomeDateValue in ThisDate. DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeDateField] = GetThisDate()" Also, a single function with an optional parameter may be created for both setting and getting the value of a private static variable: Public Function ThisValue(Optional ByVal Value As Variant) As Variant Static CurrentValue As Variant ' Define default return value. Const DefaultValue As Variant = Null If Not IsMissing(Value) Then ' Set value. CurrentValue = Value ElseIf IsEmpty(CurrentValue) Then ' Set default value CurrentValue = DefaultValue End If ' Return value. ThisValue = CurrentValue End Function To set a value: ThisValue "Some text value" To get the value: CurrentValue = ThisValue In a query: ThisValue "SomeText" ' Set value to filter on. DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeField] = ThisValue()" Using DoCmd.SetParameter The uses of DoCmd.SetParameter are rather limited, so I'll be brief. It allows you to set a parameter for use in DoCmd.OpenForm, DoCmd.OpenReport and some other DoCmd statements, but it doesn't work with DoCmd.RunSQL, filters, DAO and ADO. Sample implementation DoCmd.SetParameter "MyParameter", Me.MyTextbox DoCmd.OpenForm "MyForm",,, "ID = MyParameter" Using DAO In DAO, we can use the DAO.QueryDef object to create a query, set parameters, and then either open up a recordset or execute the query. You first set the queries' SQL, then use the QueryDef.Parameters collection to set the parameters. In my example, I'm going to use implicit parameter types. If you want to make them explicit, add a PARAMETERS declaration to your query. Sample implementation 'Execute query, unnamed parameters With CurrentDb.CreateQueryDef("", "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE Field1 = ?p1 And Field2 = ?p2") .Parameters(0) = Me.Field1 .Parameters(1) = Me.Field2 .Execute End With 'Open recordset, named parameters Dim rs As DAO.Recordset With CurrentDb.CreateQueryDef("", "SELECT Field1 FROM Table2 WHERE Field1 = FirstParameter And Field2 = SecondParameter") .Parameters!FirstParameter = Me.Field1 'Bang notation .Parameters("SecondParameter").Value = Me.Field2 'More explicit notation Set rs = .OpenRecordset End With While this is only available in DAO, you can set many things to DAO recordsets to make them use parameters, such as form recordsets, list box recordsets and combo box recordsets. However, since Access uses the text, and not the recordset, when sorting and filtering, those things may prove problematic if you do. Using ADO You can use parameters in ADO by using the ADODB.Command object. Use Command.CreateParameter to create parameters, and then append them to the Command.Parameters collection. You can use the .Parameters collection in ADO to explicitly declare parameters, or pass a parameter array to the Command.Execute method to implicitly pass parameters. ADO does not support named parameters. While you can pass a name, it's not processed. Sample implementation: 'Execute query, unnamed parameters Dim cmd As ADODB.Command Set cmd = New ADODB.Command With cmd Set .ActiveConnection = CurrentProject.Connection 'Use a connection to the current database .CommandText = "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE Field1 = ? And Field2 = ?" .Parameters.Append .CreateParameter(, adVarWChar, adParamInput, Len(Me.Field1), Me.Field1) 'adVarWChar for text boxes that may contain unicode .Parameters.Append .CreateParameter(, adInteger, adParamInput, 8, Me.Field2) 'adInteger for whole numbers (long or integer) .Execute End With 'Open recordset, implicit parameters Dim rs As ADODB.Recordset Dim cmd As ADODB.Command Set cmd = New ADODB.Command With cmd Set .ActiveConnection = CurrentProject.Connection 'Use a connection to the current database .CommandText = "SELECT Field1 FROM Table2 WHERE Field1 = #FirstParameter And Field2 = #SecondParameter" Set rs = .Execute(,Array(Me.Field1, Me.Field2)) End With The same limitations as opening DAO recordsets apply. While this way is limited to executing queries and opening recordsets, you can use those recordsets elsewhere in your application.
I have built a fairly basic query builder class to get around the mess of string concatenation and to handle the lack of named parameters. Creating a query is fairly simple. Public Function GetQuery() As String With New MSAccessQueryBuilder .QueryBody = "SELECT * FROM tblEmployees" .AddPredicate "StartDate > #StartDate OR StatusChangeDate > #StartDate" .AddPredicate "StatusIndicator IN (#Active, #LeaveOfAbsence) OR Grade > #Grade" .AddPredicate "Salary > #SalaryThreshhold" .AddPredicate "Retired = #IsRetired" .AddStringParameter "Active", "A" .AddLongParameter "Grade", 10 .AddBooleanParameter "IsRetired", False .AddStringParameter "LeaveOfAbsence", "L" .AddCurrencyParameter "SalaryThreshhold", 9999.99# .AddDateParameter "StartDate", #3/29/2018# .QueryFooter = "ORDER BY ID ASC" GetQuery = .ToString End With End Function The output of the ToString() method looks like: SELECT * FROM tblEmployees WHERE 1 = 1 AND (StartDate > #3/29/2018# OR StatusChangeDate > #3/29/2018#) AND (StatusIndicator IN ('A', 'L') OR Grade > 10) AND (Salary > 9999.99) AND (Retired = False) ORDER BY ID ASC; Each predicate is wrapped in parens to handle linked AND/OR clauses, and parameters with the same name only have to be declared once. Full code is at my github and reproduced below. I also have a version for Oracle passthrough queries that uses ADODB parameters. Eventually, I'd like to wrap both in an IQueryBuilder interface. VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "MSAccessQueryBuilder" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = True Attribute VB_PredeclaredId = False Attribute VB_Exposed = True '#Folder("VBALibrary.Data") '#Description("Provides tools to construct Microsoft Access SQL statements containing predicates and parameters.") Option Explicit Private Const mlngErrorNumber As Long = vbObjectError + 513 Private Const mstrClassName As String = "MSAccessQueryBuilder" Private Const mstrParameterExistsErrorMessage As String = "A parameter with this name has already been added to the Parameters dictionary." Private Type TSqlBuilder QueryBody As String QueryFooter As String End Type Private mobjParameters As Object Private mobjPredicates As Collection Private this As TSqlBuilder ' ============================================================================= ' CONSTRUCTOR / DESTRUCTOR ' ============================================================================= Private Sub Class_Initialize() Set mobjParameters = CreateObject("Scripting.Dictionary") Set mobjPredicates = New Collection End Sub ' ============================================================================= ' PROPERTIES ' ============================================================================= '#Description("Gets or sets the query statement (SELECT, INSERT, UPDATE, DELETE), exclusive of any predicates.") Public Property Get QueryBody() As String QueryBody = this.QueryBody End Property Public Property Let QueryBody(ByVal Value As String) this.QueryBody = Value End Property '#Description("Gets or sets post-predicate query statements (e.g., GROUP BY, ORDER BY).") Public Property Get QueryFooter() As String QueryFooter = this.QueryFooter End Property Public Property Let QueryFooter(ByVal Value As String) this.QueryFooter = Value End Property ' ============================================================================= ' PUBLIC METHODS ' ============================================================================= '#Description("Maps a boolean parameter and its value to the query builder.") '#Param("strName: The parameter's name.") '#Param("blnValue: The parameter's value.") Public Sub AddBooleanParameter(ByVal strName As String, ByVal blnValue As Boolean) If mobjParameters.Exists(strName) Then Err.Raise mlngErrorNumber, mstrClassName & ".AddBooleanParameter", mstrParameterExistsErrorMessage Else mobjParameters.Add strName, CStr(blnValue) End If End Sub ' ============================================================================= '#Description("Maps a currency parameter and its value to the query builder.") '#Param("strName: The parameter's name.") '#Param("curValue: The parameter's value.") Public Sub AddCurrencyParameter(ByVal strName As String, ByVal curValue As Currency) If mobjParameters.Exists(strName) Then Err.Raise mlngErrorNumber, mstrClassName & ".AddCurrencyParameter", mstrParameterExistsErrorMessage Else mobjParameters.Add strName, CStr(curValue) End If End Sub ' ============================================================================= '#Description("Maps a date parameter and its value to the query builder.") '#Param("strName: The parameter's name.") '#Param("dtmValue: The parameter's value.") Public Sub AddDateParameter(ByVal strName As String, ByVal dtmValue As Date) If mobjParameters.Exists(strName) Then Err.Raise mlngErrorNumber, mstrClassName & ".AddDateParameter", mstrParameterExistsErrorMessage Else mobjParameters.Add strName, "#" & CStr(dtmValue) & "#" End If End Sub ' ============================================================================= '#Description("Maps a long parameter and its value to the query builder.") '#Param("strName: The parameter's name.") '#Param("lngValue: The parameter's value.") Public Sub AddLongParameter(ByVal strName As String, ByVal lngValue As Long) If mobjParameters.Exists(strName) Then Err.Raise mlngErrorNumber, mstrClassName & ".AddNumericParameter", mstrParameterExistsErrorMessage Else mobjParameters.Add strName, CStr(lngValue) End If End Sub ' ============================================================================= '#Description("Adds a predicate to the query's WHERE criteria.") '#Param("strPredicate: The predicate text to be added.") Public Sub AddPredicate(ByVal strPredicate As String) mobjPredicates.Add "(" & strPredicate & ")" End Sub ' ============================================================================= '#Description("Maps a string parameter and its value to the query builder.") '#Param("strName: The parameter's name.") '#Param("strValue: The parameter's value.") Public Sub AddStringParameter(ByVal strName As String, ByVal strValue As String) If mobjParameters.Exists(strName) Then Err.Raise mlngErrorNumber, mstrClassName & ".AddStringParameter", mstrParameterExistsErrorMessage Else mobjParameters.Add strName, "'" & strValue & "'" End If End Sub ' ============================================================================= '#Description("Parses the query, its predicates, and any parameter values, and outputs an SQL statement.") '#Returns("A string containing the parsed query.") Public Function ToString() As String Dim strPredicatesWithValues As String Const strErrorSource As String = "QueryBuilder.ToString" If this.QueryBody = vbNullString Then Err.Raise mlngErrorNumber, strErrorSource, "No query body is currently defined. Unable to build valid SQL." End If ToString = this.QueryBody strPredicatesWithValues = ReplaceParametersWithValues(GetPredicatesText) EnsureParametersHaveValues strPredicatesWithValues If Not strPredicatesWithValues = vbNullString Then ToString = ToString & " " & strPredicatesWithValues End If If Not this.QueryFooter = vbNullString Then ToString = ToString & " " & this.QueryFooter & ";" End If End Function ' ============================================================================= ' PRIVATE METHODS ' ============================================================================= '#Description("Ensures that all parameters defined in the query have been provided a value.") '#Param("strQueryText: The query text to verify.") Private Sub EnsureParametersHaveValues(ByVal strQueryText As String) Dim strUnmatchedParameter As String Dim lngMatchedPoisition As Long Dim lngWordEndPosition As Long Const strProcedureName As String = "EnsureParametersHaveValues" lngMatchedPoisition = InStr(1, strQueryText, "#", vbTextCompare) If lngMatchedPoisition <> 0 Then lngWordEndPosition = InStr(lngMatchedPoisition, strQueryText, Space$(1), vbTextCompare) strUnmatchedParameter = Mid$(strQueryText, lngMatchedPoisition, lngWordEndPosition - lngMatchedPoisition) End If If Not strUnmatchedParameter = vbNullString Then Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strUnmatchedParameter & " has not been provided a value." End If End Sub ' ============================================================================= '#Description("Combines each predicate in the predicates collection into a single string statement.") '#Returns("A string containing the text of all predicates added to the query builder.") Private Function GetPredicatesText() As String Dim strPredicates As String Dim vntPredicate As Variant If mobjPredicates.Count > 0 Then strPredicates = "WHERE 1 = 1" For Each vntPredicate In mobjPredicates strPredicates = strPredicates & " AND " & CStr(vntPredicate) Next vntPredicate End If GetPredicatesText = strPredicates End Function ' ============================================================================= '#Description("Replaces parameters in the predicates statements with their provided values.") '#Param("strPredicates: The text of the query's predicates.") '#Returns("A string containing the predicates text with its parameters replaces by their provided values.") Private Function ReplaceParametersWithValues(ByVal strPredicates As String) As String Dim vntKey As Variant Dim strParameterName As String Dim strParameterValue As String Dim strPredicatesWithValues As String Const strProcedureName As String = "ReplaceParametersWithValues" strPredicatesWithValues = strPredicates For Each vntKey In mobjParameters.Keys strParameterName = CStr(vntKey) strParameterValue = CStr(mobjParameters(vntKey)) If InStr(1, strPredicatesWithValues, "#" & strParameterName, vbTextCompare) = 0 Then Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strParameterName & " was not found in the query." Else strPredicatesWithValues = Replace(strPredicatesWithValues, "#" & strParameterName, strParameterValue, 1, -1, vbTextCompare) End If Next vntKey ReplaceParametersWithValues = strPredicatesWithValues End Function ' =============================================================================
LINQ SqlMethods.Like (and .Contains) fails
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)
Trying to create a function
So I'm trying to change this sub in to a function so that I can reference it and set a label.text as it's result, this is so I don't have to creating new subs to update different labels. Dim drive As String = "C" Dim disk As ManagementObject = _ New ManagementObject _ ("win32_logicaldisk.deviceid=""" + drive + ":""") disk.Get() Dim serial As String = disk("VolumeSerialNumber").ToString() Label1.Text = ("Serial: " & serial) Can someone tell me how I can change this in to a function? I've tried declaring Serial as an empty String and then changing the last line to read: Return serial = disk("VolumeSerialNumber").ToString() At the moment this just sets Label1.Text to display "False", like I've set it as a Boolean or something?! I'm learning functions at the moment, I'm trying to make things cleaner as up until now I've just been creating different subs to update labels etc... I'm looking for some tips so I can try and get this myself.
It is really a simple refactoring operation. Sub Main Dim driveLetter = "X" Try Dim result = DriveSerialNumber(driveLetter) Console.WriteLine(result) Catch ex as Exception Console.WriteLine("Error: drive " & driveLetter & ": " & ex.Message) End Try End Sub Public Function DriveSerialNumber(drive as String) As String Dim disk As ManagementObject = _ New ManagementObject _ ("win32_logicaldisk.deviceid=""" + drive + ":""") disk.Get() return disk("VolumeSerialNumber").ToString() End Function However, be prepared to receive Exceptions if you pass an invalid drive letter
vb.net List of dynamic type
I'm building a MVC structure for a part of my program. I've done the Models of 5-10 tables and what they have in common is only the constructor. (which takes the recordset.fields) Here's my function to fill these objects: Public Function reqTable(ByVal pTable As String, ByVal pType As Type, ByVal pNoProjet As Integer, Optional ByVal strAdditionnalConditions As String = "") As List(Of Object) Dim lstRetour As List(Of Object) = New List(Of Object) rsRequestCSV = conSQL.Execute("SELECT * FROM " & pTable & " WHERE NoProjet = " & pNoProjet & " " & strAdditionnalConditions) With rsRequestCSV While Not .EOF lstRetour.Add(Activator.CreateInstance(pType, New Object() {rsRequestCSV.Fields})) 'New clsTable(rsRequestCSV.Fields)) .MoveNext() End While End With Return lstRetour End Function What I'm not able to achieve is to return a List(Of pType) instead of List(Of Object). The reason I want this is to have headers in my datagridviews even if they're empty. So is there a way to return a List(Of MyModel'sType) ? thanks in advance!
Just use As pType instead of As Object (but consider using a conventional type argument name, i.e. T instead of pType), remove the now obsolete pType argument, and use the following to create and add the instances: Public Function ReqTable(Of T)(ByVal table As String, ByVal noProject As Integer, Optional ByVal additionalConditions As String = "") As List(Of T) Dim result As New List(Of T)() ' Where is this declared?! It probably should be declared here. request = conSQL.Execute( _ String.Format("SELECT * FROM {0} WHERE NoProjet = {1} {2}", _ table, noProjet, additionnalConditions)) While Not request.EOF result.Add( _ CType(Activator.CreateInstance( _ GetType(T), New Object() {request.Fields}), _ T)) request.MoveNext() End While Return result End Function GetTpe(T) gets you a System.Type instance representing the type argument. Since VB, unlike Java, has reified generic types you can thus create instances from type argument. Apart from that, pay attention that .NET has different code style conventions than Java; for instance, all methods should use PascalCase, not camelCase. And like in Java, use of Hungarian notation is discouraged. Use concise but descriptive names. And, as Rene noted, your code suffers from an SQL injection vulnerability.
Ignoring the SQL injection issue ect try this: Public Function reqTable(of T)(ByVal pTable As String, ByVal pNoProjet As Integer, Optional ByVal strAdditionnalConditions As String = "") As List(Of T) Dim lstRetour As New List(Of T) rsRequestCSV = conSQL.Execute("SELECT * FROM " & pTable & " WHERE NoProjet = " & pNoProjet & " " & strAdditionnalConditions) With rsRequestCSV While Not .EOF lstRetour.Add(Activator.CreateInstance(T, New Object() {rsRequestCSV.Fields})) 'New clsTable(rsRequestCSV.Fields)) .MoveNext() End While End With Return lstRetour End Function