Efficient way to dynamically populate ListBox vb.net - vb.net

I have a program that allows a user to search for a customer by name. The way I have done this so far (code below) is have the user start typing the customer name in a TextBox (tbCust), the code fires on the TextChanged event and repopulates the ListBox based on what the user has typed. I think the idea here is obvious and commonly used.
This works without minimal lag on my computer but on some other users computers which are more base level machines, there is anywhere from 100ms to 300ms delay between updates which makes for a pretty crappy user experience.
Correct me if i'm wrong here but I feel like this functionality should be easily attainable without any perceived lag for just about any computer out there.
I assume there is a more correct/efficient way of doing this that I'm just not smart enough to come up with on my own (enter, all of you!)
Please shed some light on maybe a more 'appropriate' way of doing this that results in much better performance. I assume my problem lies with querying the database each time the routine runs (every time the user types a letter) but I'm not sure how else to do it while still working with live data.
Many Many Thanks in Advance!
Video of acceptable performance on my computer: Youtube Video #1
Video of unacceptable performance on user computer: YouTube Video #2
User Computer Specs:
Private Sub tbCust_TextChanged(sender As Object, e As EventArgs) Handles tbCust.TextChanged
'This populates the Customer Selection list box with customers whose names start with the
'string of letters in the customer name text box.
If tbCust.TextLength > 0 Then
lbCustSelect.Visible = True
Dim SQL As String
SQL = "SELECT C_CUSTOMER as ID, C_SHIPNAME as Name FROM CUSTOMER WHERE LEFT(C_SHIPNAME," & tbCust.TextLength & ") ='" & tbCust.Text & "'"
'Query Database
AeroDBcon.RunQuery(SQL)
'Fill DataTable with Query results
dtCustomers = AeroDBcon.DBds.Tables(0)
'Tie DataTable to ListBox
lbCustSelect.DataSource = dtCustomers
lbCustSelect.DisplayMember = "Name"
lbCustSelect.ValueMember = "ID"
'If there are no results, hide the ListBox
If dtCustomers.Rows.Count = 0 Then
lbCustSelect.Visible = False
End If
Else
'if there is no text in the customer name text box, hide the listbox
lbCustSelect.Visible = False
End If
End Sub

Filtering in SQL is usually quicker than filtering on client side. But since table CUSTOMER is probably not that large, and there seems to be an overhead issue with querying the database, let's query it all at once, and filter on the client side.
I like strong-typing. Even though you don't use an ORM, we can still create a class to hold your results:
Private Class Customer
Public Property ID As String
Public Property Name As String
End Class
And if we hold a collection of all customers,
Private customers As IEnumerable(Of Customer)
it's simply filtered like this
Dim filteredCustomers = customers.Where(Function(c) c.Name.StartsWith(filterString)).ToList()
Also, I wouldn't run the query on keypress. Nor would I run it on the UI thread (UI event handlers run on the UI, and that will cause your UI to freeze while the query runs). Run the query after a set amount of time since the last keypress, and run it off the UI. A System.Threading.Timer is perfect for this.
Private ReadOnly queryTimer As New System.Threading.Timer(AddressOf executeQuery, Nothing, -1, -1)
Private ReadOnly keyPressDelay As Integer = 100
Private customers As IEnumerable(Of Customer)
Private filterString As String = ""
Private Sub tbCust_TextChanged(sender As Object, e As EventArgs) Handles tbCust.TextChanged
filterString = tbCust.Text
lbCustSelect.Visible = filterString.Length > 0
If filterString.Length > 0 Then queryTimer.Change(keyPressDelay, -1)
End Sub
Private Sub executeQuery(state As Object)
' this could alternately be run in Form_Load
If customers Is Nothing Then
Dim sql = "SELECT C_CUSTOMER as ID, C_SHIPNAME as Name FROM CUSTOMER"
AeroDBCon.RunQuery(sql)
customers =
AeroDBCon.DBds.Tables(0).
AsEnumerable().Select(Function(dr) New Customer With {.ID = dr("ID").ToString(), .Name = dr("Name").ToString()})
End If
Dim filteredCustomers = customers.Where(Function(c) c.Name.StartsWith(filterString)).ToList()
' Dim filteredCustomers = customers.Where(Function(c) c.Name.Contains(filterString)).ToList()
' update the control on the UI thread
lbCustSelect.Invoke(
Sub()
lbCustSelect.DataSource = Nothing
lbCustSelect.DisplayMember = "Name"
lbCustSelect.ValueMember = "ID"
lbCustSelect.DataSource = filteredCustomers
End Sub)
End Sub
You should also dispose the timer when the form disposes. Modify your Dispose method to this
Protected Overrides Sub Dispose(disposing As Boolean)
Try
If disposing Then
components?.Dispose()
queryTimer?.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub

Related

How can I speed up VB copy

I'm running the following loop successfully when the number of items is low. However, when run against a larger list on the ListView, it seems to be taking way too long. I tested it with a list of 8,700 files and it took about two hours to complete. Is there something I can do to speed this up? I guess that removing the check for the Cancel button would help but I would like to keep that there for usability. As I've mentioned in earlier posts, I'm pretty new to Visual Basic so please provide lots of explanation with your suggestions. Thanks. Here's the code:
For i As Integer = 0 To m_CountTo
' Has the background worker be told to stop?
If BackgroundWorker1.CancellationPending Then
' Set Cancel to True
e.Cancel = True
Exit For
End If
'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.
LVFiles.Items(i).Selected = True
strSourceFilePath = LVFiles.SelectedItems(i).SubItems(0).Text
strDestFilePath = LVFiles.SelectedItems(i).SubItems(3).Text
My.Computer.FileSystem.CopyFile(strSourceFilePath, strDestFilePath, overwrite:=False)
' Report The progress of the Background Worker.
BackgroundWorker1.ReportProgress(CInt((i / m_CountTo) * 100))
' Me.LabelStatus.Text = FormatPercent((i + 1) / (intLVIndex + 1), 2) ' Show Percentage in Label
SetLabelText_ThreadSafe(Me.LabelStatus, FormatPercent(i / m_CountTo, 2))
Next
The Backgroundworker encapsulates a new thread. You cannot directly access controls that are created in another thread. If you do you will get an InvalidOperationException because of a cross-thread operation. The Backgroundworker however offers some functionality to share data (or access to controls) between threads. You should use them.
Private Sub StartBGW_Click(sender As Object, e As EventArgs) Handles StartBGW.Click
Dim dict As New Dictionary(Of String, String)
For i As Integer = 0 To m_CountTo
dict.Add(Me.LVFiles.Items(i).SubItems(0).Text,
Me.LVFiles.Items(i).SubItems(3).Text)
Next
Me.BackgroundWorker1.RunWorkerAsync(dict)
End Sub
First we prepare a dictionary that contains the source as Key and the target as Value. This object is given to the BackgroundWorker as a parameter.
Now comes the essential part:
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim counter As Integer = -1
Dim dict = DirectCast(e.Argument, Dictionary(Of String, String))
For Each kvp In dict
counter += 1
' Has the background worker be told to stop?
If Me.BackgroundWorker1.CancellationPending Then
' Set Cancel to True
e.Cancel = True
Exit For
End If
'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.
My.Computer.FileSystem.CopyFile(kvp.Key, kvp.Value, overwrite:=False)
' Report The progress of the Background Worker.
Me.BackgroundWorker1.ReportProgress(CInt((counter / m_CountTo) * 100), counter)
Next
End Sub
We don't access the ListView anymore. Instead we use the dictionary that is given to us as a parameter through e.Argument. Theres also a slight difference in the BackgroundWorker1.ReportsProgress line. There's a second parameter I have used to pass the current index to the ProgressChanged event which can be obtained via e.UserState.
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Me.LVFiles.Items(Convert.ToInt32(e.UserState)).Selected = True
Me.LabelStatus.Text = e.ProgressPercentage.ToString
End Sub
This event is designed to be raised with a SynchronizationContext of the calling thread, in this case the UI thread. Here we can safely access any control and update them. The index is passed as e.UserState, so we can access the relevant item and set their Selected property to true.
The biggest improvement comes from the change of Me.LVFiles.SelectedItems(i).SubItems(0).Text to Me.LVFiles.Items(i).SubItems(0).Text. I'm not a professional, but it seems that SelectedItems isn't a real list. Instead it iterates through every item using the SendMessage API until the desired index is reached. This is why it takes longer the higher your index is. Everytime it starts with the first item and iterates through them. Lot of operations.
The second improvement is the separation of code that access UI controls. It's all done in one method now. More clear and readable.
Update: #Enigmativity mentioned that SelectedListViewItemCollection implements IList and therefore is a real list. Even though it has no underlying list containing all selected items like you have in ListViewItemCollection. My point was to say, that accessing a single element is more complicated.

Secondary thread causes "Application has stopped working" crashes even when invoking

I have an application which has a form with a DataGridView bound to a BindingSource, which is bound to a DataTable:
bsList.DataSource = dsData
bsList.DataMember = "List"
dgvList.DataSource = bsList
The underlying data which populates dsData.Tables("List") can change whilst the user is working so to combat this I have a background thread which routinely checks the database for changes and updates dsData.Tables("List"). It also changes the colour of any row where another user is currently working.
However, users report that when this background updating functionality is enabled the application routinely CTDs with no application error message. I have been unable to reproduce this and my attempt to log the crashes via writing to a log file in Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException hasn't worked as the log file is never written to, suggesting this event is never triggered.
The thread is instantiated like this:
LiveUpdating = New Thread(AddressOf UpdateUserLocation) With {.IsBackground = True}
LiveUpdating.Start()
This is the UpdateUserLocation sub:
Public Sub UpdateUserLocation()
Do While My.Settings.customBackgroundUpdating = True And formLoaded = True
UserLocations.Clear()
dtUsers = CLS_USERS.GetUsersSequence(winUser)
dtProgress = DAC.GetProgress()
For Each CandRow As DataRow In dsHHData.Tables("List").Rows
Dim CandReadDate As Date
Dim CandRowNextRead As String = DBNull.Value.ToString
If Not (CandRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(CandRow("NEXT READ").ToString, CandReadDate) Then
CandRowNextRead = CandReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim CandRowSending As String = TryCast(CandRow("SENDING"), String)
Dim CandRowNotes As String = TryCast(CandRow("NOTES"), String)
For Each NewRow As DataRow In dtUsers.Rows
If CandRow("SQ").ToString = NewRow("SQ").ToString Then
UserLocations.Add(NewRow("SQ").ToString)
End If
Next
For Each ProgressRow As DataRow In dtProgress.Rows
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
Dim NextReadDate As Date
Dim ProgressRowNextRead As String = DBNull.Value.ToString
If Not (ProgressRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(ProgressRow("NEXT READ").ToString, NextReadDate) Then
ProgressRowNextRead = NextReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim ProgressRowSending As String = TryCast(ProgressRow("SENDING"), String)
Dim ProgressRowNotes As String = TryCast(ProgressRow("NOTES"), String)
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
If CandRowSending <> ProgressRowSending Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableSending), CandRow, ProgressRowSending)
End If
If CandRowNextRead <> ProgressRowNextRead Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNextRead), CandRow, ProgressRowNextRead)
End If
If CandRowNotes <> ProgressRowNotes Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNotes), CandRow, ProgressRowNotes)
End If
End If
End If
Next
Next
dgv.BeginInvoke(
New MethodInvoker(
Sub()
For Each dgv_row As DataGridViewRow In dgv.Rows
If UserLocations.Contains(dgv_row.Cells("SQ").Value.ToString) Then
dgv.DefaultCellStyle.BackColor = My.Settings.customRowHighlight
Else
dgv.DefaultCellStyle.BackColor = Nothing
End If
Next
End Sub))
Thread.Sleep(My.Settings.customRefreshRate * 1000)
Loop
End Sub
The subs that do the DataTable update are like this:
Private Delegate Sub UpdateDataTableDelegate(ByVal CandRow As DataRow, ByVal ProgressRow As String)
Private Sub UpdateDataTableSending(ByVal CandRow As DataRow, ByVal ProgressRowSending As String)
CandRow("SENDING") = ProgressRowSending
End Sub
I know this is not the best way to handle a multi-user environment but the nature of this work requires that all people can access and see the same data. I could force them to refresh regularly but that seems very intrusive.
The crashes only occur when this thread is running and the crashes are regular (and not instant) but I cannot seem to reproduce them and the application is very stable otherwise.
There must be some cross-threading issue but I can't work how when all of the updates to the DataTable or DataGridView are done via a BeginInvoke on the main UI thread.
EDIT: I've just realised that even though I am doing the queries and most of the heavy lifting in the background thread, the updates are stilled called on the main UI thread which would lock the thread. This would be particularly noticeable if there were a lot of updates... Because each one is called individually.
If the UI lock up was long enough, and the user was clicking on stuff, would this cause Windows to treat the application as unresponsive and crash it? If so, is there a better way I could handle these updates?
Any help with resolving this would be enormously appreciated.

Microsoft Access applying 1 function to all fields automatically

I have a form that keeps track of assigned patient equipment. I have it set so that any changes made to text fields on the form automatically move down to the "comments" section of the form (this is done so that any changes made are documented in case the user forgets to manually document changes). I have a sub that I wrote that accomplishes this that I am currently calling for every single text field. This works but is messy.
Is there a way to apply the sub to all the fields in one procedure without calling it for every individual field? Code is below, please let me know if I can clarify anything.
Private Sub pPEMoveValue(sField)
'Moves the old field value down to the comments section automatically
Dim sOrigValue As String
Dim sCommentValue As String
sOrigValue = sField
sCommentValue = Nz(mPEComments, "")
Me.mPEComments = sCommentValue & vbNewLine & sOrigValue
End Sub
Private Sub sPEBatCharger_Dirty(Cancel As Integer)
pPEMoveValue (Nz(Me.sPEBatCharger.OldValue, ""))
End Sub
This is the solution I came up with to do what you are looking to do. I took advantage of the MS Access Tag system. You can add tags to your controls so you can sort of "Group" them.
First put the form in design view and adjust the tag for all of the fields you want to record to say "Notes".
Then in the Form's BeforeUpdate even you would add this:
Private Sub Form_BeforeUpdate(Cancel As Integer)
Call FindControlsForComments(Me.Form)
End Sub
Then you would use this function to find any fields that have the "Notes" tag and run it through the function you created:
Public Function FindControlsForComments(frm As Form)
Dim ctrl As Access.Control
For Each ctrl In frm
'If the control is tagged for notes
If ctrl.Tag = "Notes" Then
'If the old value is different than the current value
If Nz(ctrl.OldValue, "") <> Nz(ctrl.Value, "") Then
'Add to comment
Call pPEMoveValue(Nz(ctrl.Value, ""))
End If
End If
Next ctrl
End Function
You may have to adjusted this slightly to work with your system but this has worked well for me.

How to use a checkbox to change values in an SQL database in asp.net

I currently have system which has a web front end and a back office system. User can book properties online or call our office to book a property. In the admin system on the web front, I have two check boxes to determine if the property is available on the front end or the back office system. This is controlled using a Check box. The code for the check box is as follows;
<asp:CheckBox ID="CheckBoxAvailableToWeb" runat="server" TextAlign="Left" Text="Available for web bookings"
Checked="true" />
I have an field in an SQL Database called "isAvailableToWeb" which has a Boolean result. What I want to achieve is if the check box is checked, the value of the "isAvailableToWeb" field is set to "True" or set to "False" if un-checked.
I have tried to complete this function using the following code;
Protected Sub CheckBoxAvailableToWeb_CheckedChanged(sender As Object, e As EventArgs, ByVal beachhutid As Long)
Using dbContext = New bbhasDBEntities
Dim item
item = (From i In dbContext.tblBeachHuts Where i.beachHutId = beachhutid Select i).First()
If CheckBoxAvailableToWeb.Checked = True Then
item.AvailableToWeb = True
Else
item.AvailableToWeb = False
End If
dbContext.tblBeachHuts.Attach(item)
Call dbContext.SaveChanges()
End Using
End Sub
This code doesn't throw up any errors but doesn't also make the change that I would like to see.
I have a button on this page that saves the information so I would also like to know if it would be better, once the code is working, to put it in that Sub.
You need to call dbContext.SaveChanges to persist to the database (assuming bbhasDBEntities is an instance of DbContext), assign to the entity rather than the boolean and call First() to get the first matching element rather than a collection.
Protected Sub CheckBoxAvailableToWeb_CheckedChanged(sender As Object, e As EventArgs, ByVal beachhutid As Long)
Using dbContext = New bbhasDBEntities
Dim item
item = (From i In dbContext.tblBeachHuts Where i.beachHutId = beachhutid Select i).First()
If CheckBoxAvailableToWeb.Checked = True Then
item.AvailableToWeb = True
Else
item.AvailableToWeb = False
End If
Call dbContext.SaveChanges()
End Using
End Sub
Try using 1 and 0 instead of true and false when assigning values into AvailableToWeb.
I use this method with a dataset and it works correctly.

Limiting my result set in VB

So I can fill the combobox I have going in Visual Studio just how I want with ALL results with the following:
Dim pnum As New List(Of String)
For Each polnumber As InsuredDataSet.Claims_InsuredRow In Me.InsuredDataSet.Claims_Insured
pnum.Add(polnumber.Policy_Number)
Next
pnum.Reverse()
Me.Policy_NumberComboBox.DataSource = pnum
Awesome. Now I want to limit the pnum by taking what was input/selected from Insured_NameTextBox on the form and only returning the Policy_Number with a matching Insured_Name. I figure this can be performed with an If statement, but everything I try (stringcompare, InsuredName_TextBox = Me.InsuredDataSet.ClaimsInsured, etc.) either doesn't limit the results OR limits the results entirely so nothing shows up. Any idea where to put the If statement and what should be compared?
UPDATE:
I think there is some confusion so I'm including the entire load sub below:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'TODO: This line of code loads data into the 'IncidentsDataSet.Claims_Incidents' table. You can move, or remove it, as needed.
Me.Claims_IncidentsTableAdapter.Fill(Me.IncidentsDataSet.Claims_Incidents)
'TODO: This line of code loads data into the 'InsuredDataSet.Claims_Insured' table. You can move, or remove it, as needed.
Me.Claims_InsuredTableAdapter.Fill(Me.InsuredDataSet.Claims_Insured)
'textbox autocomplete mode
Dim Iname As New AutoCompleteStringCollection()
For Each insname As InsuredDataSet.Claims_InsuredRow In Me.InsuredDataSet.Claims_Insured
Iname.Add(insname.Insured_Name)
Next
Me.Insured_NameTextBox.AutoCompleteCustomSource = Iname
'combobox autocomplete code (now sorting by last included!)
Dim pnum As New List(Of String)
For Each polnumber As InsuredDataSet.Claims_InsuredRow In Me.InsuredDataSet.Claims_Insured
pnum.Add(polnumber.Policy_Number)
Next
pnum.Reverse()
Me.Policy_NumberComboBox.DataSource = pnum
End Sub
Try something like this:
Me.Policy_NumberComboBox.DataSource = InsuredDataSet.Claims_Insured.Where(Function(r) r.Insured_Name = Insured_NameTextBox.Text).Select(Function(r) r.Policy_Number).Reverse()
We're getting closer. Based on the update to your question, you're running this code when the form loads. However, at the point where the form loads, your textbox will always be empty. What do you do when the value in the textbox changes, to re-filter your data?
This is C#
Me.InsuredDataSet.Claims_Insured.Where(x => x.Insured_Name == Insured_NameTextBox.Text);