Cannot cancel a background worker running a SQL query - vb.net

I have searched SO and Google but I cannot find the information I am seeking which may indicate I am not using Background Workers correctly. In my case, I am running a SQL query in a Background Worker so the GUI does not freeze for the user. Please note, supportscancellation is set to true.
However, I am trying to stop the background worker if the user wants to cancel the query. Because my background worker does not have a loop, I am unsure how to accomplish this.
My background worker simply looks at a boolean to determine the type of SQL query, then makes a call to another event to run it.
Sub BackgroundWorker1DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
If Psearch = True Then
P_Search()
End If
If Asearch = True Then
A_Search()
End If
If Msearch = True Then
M_Search()
End If
If Dsearch = True Then
D_Search()
End If
End Sub
Based on articles I find, it seems adding an if statement to check for cancellationpending is irrelevant as it would only fire the one time (at the start of the RunWorkerAsync). So, I have tried using a For/Do loop to check for cancellationpending or workercompleted but it still will not cancel and dispose of the background worker. I have validated this by attempting to restart the background worker but I am erroring out due to workser isbusy.
Sub Button7Click(sender As Object, e As EventArgs)
If backgroundWorker1.IsBusy() Then
backgroundWorker1.CancelAsync()
backgroundWorker1.Dispose()
End If
End Sub
Sub D_Search()
Dim sqlConnection1 As New SqlConnection(GlobalConString)
Dim cmd As New SqlCommand
cmd.CommandText = RealQuery
cmd.CommandTimeout = 0
cmd.Connection = sqlConnection1
sqlConnection1.Open()
Dim daQuery As New System.Data.SqlClient.SqlDataAdapter(cmd)
Dim dsQuery As New DataSet
Try
If backgroundWorker1.CancellationPending = False
daQuery = New System.Data.SqlClient.SqlDataAdapter
daQuery.SelectCommand = cmd
daQuery.Fill(dsQuery, "Query")
tblQuery = dsQuery.Tables("Query")
Else
backgroundWorker1.Dispose()
sqlConnection1.Close()
End If
Catch Err As System.Exception
MessageBox.Show(Err.ToString)
Finally
sqlConnection1.Close()
End Try
End Sub

Related

How to properly set up BackgroundWorker in WPF VB

I am working in VB and when the end user clicks on a button, I want to show a “Please wait” window (no progress update necessary) and trigger the process in the background on another thread as it might take a while to complete. I understand BackgroundWorker is the recommended approach for this, and although I have previously set up a BGW on a windows forms application, I am new to WPF and having a hard time understanding the differences.
In order to reuse some of my code, I have written a function (“DbOperation”) that executes a SQL stored procedure (from parameter) and returns a data table (or NULL if no output), and it seems to work well everywhere except in concert with my BGW.
I understand UI should only be affected by the main thread, and background threads should not touch the UI, and the BGW thread should call the long-running process.
Private Sub btnProcessReports_Click(sender As Object, e As RoutedEventArgs) Handles btnProcessReports.Click
'Set up background worker
bgw.WorkerReportsProgress = False
bgw.WorkerSupportsCancellation = False
AddHandler bgw.DoWork, AddressOf bgw_DoWork
AddHandler bgw.RunWorkerCompleted, AddressOf bgw_RunWorkerCompleted
'Show please wait window
f.Label1.Text = "Importing web reports. Please wait."
f.Show()
'Start the work!
bgw.RunWorkerAsync()
End Sub
Private Sub bgw_DoWork(sender As Object, e As DoWorkEventArgs)
'Set current thread as STA
Thread.CurrentThread.SetApartmentState(ApartmentState.STA)
'Trigger job
Dim Qry As String = "EXEC [msdb].[dbo].sp_start_job N'Import Online Payment Portal Reports';"
DbOperation(Qry)
End Sub
Public Function DbOperation(ByVal qry As String, Optional ByVal type As DatabaseQueryReturnType = DatabaseQueryReturnType.NonQuery) As Object
Dim objDB As New Object
Dim dt As DataTable
Dim da As SqlDataAdapter
OpenConnection()
Dim cmd As New SqlCommand(qry)
cmd.Connection = con
If type = DatabaseQueryReturnType.DataTable Then
dt = New DataTable
da = New SqlDataAdapter(cmd)
Try
da.Fill(dt)
Catch ex As Exception
MessageBox.Show("Error retrieving data from database: " & ex.Message.ToString)
End Try
objDB = dt
dt.Dispose()
CloseConnection()
Return objDB
ElseIf type = DatabaseQueryReturnType.NonQuery Then
Try
cmd.ExecuteNonQuery()
Catch ex As Exception
MessageBox.Show("Error executing nonquery: " & ex.Message.ToString)
End Try
objDB = Nothing
CloseConnection()
Return objDB
End If
Return objDB
End Function
When I try running the program, it gives me “The calling thread must be STA, because many UI components require this.” on the line that calls my function. After a bit of research, I learned that you have to call SetApartmentState() before the thread is started, but this doesn’t make sense since the recommended code seems to be:
Thread.CurrentThread.SetApartmentState(ApartmentState.STA)
After adding this line, I then get “Failed to set the specified COM apartment state” on that line of code.
I have also tried commenting out the messagebox lines in the function in case it was trying to open the messageboxes on the UI thread, but it had no impact and I still got the same errors.
What am I missing in my code to get the function to run on the BGW thread?

Need a fix for my deadlocking issue with my parallel.foreach

When running my code I seem to encounter deadlocks while trying to update a GUI element from within one of the parallel tasks.
I've tried surrounding the Output function with "Synclock me" to try to ensure that only one task is trying to update the control at a time.
Private Sub RunParallel(records as list(of DataRecord), ou as String)
Dim ParallelOptions As New ParallelOptions
ParallelOptions.MaxDegreeOfParallelism = 10
Parallel.ForEach(records, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
'Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname")
}
If Not CreateUser(ou, ud) Then
'Threading.Thread.Sleep(2000)
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
End Sub
Private Sub Output(ByVal s As String)
SyncLock Me
If Me.InvokeRequired Then
Invoke(Sub()
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End If
End SyncLock
End Sub
The code as supplied seems to run, but if I uncomment out the Output calls in the ProcessRecord() function, it hangs and never gets exits the Parallel.foreach
--- Update
After playing around with suggestions and comments on here I still can't get it to work correctly.
If I take out all of the output from ProcessRecord it seems to work correctly. However with the following code, it now seems to run each ProcessRecord sequentially (not 10 at a time as I intended), and then hangs after the last one.
Output("Dispatching" & vbCrLf)
Dim ParallelOptions As New ParallelOptions With {
.MaxDegreeOfParallelism = 10
}
Parallel.ForEach(recordList, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
'For Each myrecord As DataRecord In recordList
' Task.Factory.StartNew(Sub() ProcessRecord(myrecord, ou))
'Next
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname"),
.DisplayName = $"{record("Forename")} {record("Surname")} (Student)"}
If Not CreateUser(ou, ud) Then
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
End Sub
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Invoke(Sub()
Output(s)
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
If I use the commented out Task.Factory code everything seems to work perfectly, except I cant control how many tasks at a time are launched, and I can't wait till all of them have finished, the for loop just launches all the tasks, and then carries on with the Output("Done....) line.
The synclock statements didn't seem to affect anything either way.
Give this a try
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Me.Invoke(Sub() Output(s))
'Me.BeginInvoke(Sub() Output(s))
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
There may be an issue if you have events tied to Outbox, like text changed. Tested Output method with
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim nums As New List(Of Integer)
For x As Integer = 1 To 500
nums.Add(x)
Next
'because it is in a button, run from a task
Dim t As Task
t = Task.Run(Sub()
Parallel.ForEach(nums, Sub(num)
Output(num.ToString & Environment.NewLine)
End Sub)
End Sub)
End Sub
If you want to go ahead with using a Task-based approach, then you certainly can control how many are launched at a time, and wait for all of them to finish. It requires some additional code for the manual management. This is discussed in some detail in Microsoft documentation: https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
It's not necessarily a bad thing to initiate all of the tasks immediately, then you'll be leaving it to the thread pool to take care of how many to run at a time.
If you want greater control, you can use the "throttling" design from the link. In your "pending" queue, store delegates/lambdas that will themselves kick off Task.Run. Then, as you dequeue from the "pending" queue into the "active" list, you can Invoke on the delegate/lambda to get the Task and Await Task.WhenAny on the "active" list.
One potential benefit of doing things this way is that the work in each top-level Task can be split between UI work running on the UI thread and processor-limited work running on the thread pool.
(I'm not suggesting that this is necessarily the best option for you, just trying to expand on what you should be looking at doing if you really want to pursue using Task instead of Parallel.)

VB.net ContinueWith

I have this code which loops through all my accounts in my list and then does something to the accounts using tasks for each account as a way to speed up the process. Each time the program completes this action, I want the user interface to update the progress bar. I was using Invoke before but it isn't the best option and I couldn't get it working. Now I know this can be done using a background worker but this isn't the best way of making your application multithreaded so I used this. And instead of invoking I heard about ContinueWith but I can't seem to get it working and I get no error message just a red underline.
Code:
progressBar.Value = 0
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While checked = False
If proxies.Count = 0 Then
Exit Sub
'Also can't think of a good way to stop searching through accounts when there are no proxies left in my queue.
End If
Dim proxy As New WebProxy(proxies(0))
proxies.TryDequeue(0)
'Do something
End While
checkedAmount += 1
Dim progress As Integer = ((checkedAmount / combos.Count) * 100)
Task.ContinueWith(progressBar.Value = progress, TaskScheduler.FromCurrentSynchronizationContext()) 'Error here
End Sub)
tasks.Add(t)
Next
Task.WaitAll(tasks.ToArray())
I get no error code as shown here:
I have also tried putting a sub after and stuff but that lead to nothing.
Thanks for any help in advance.
Update tried with invoke:
Private Delegate Sub UpdateProgressBarDelegate(ByVal progressBarUpdate As ProgressBar, ByVal value As Integer)
Dim checkedAmount As Integer = 0
Dim checked As Boolean = False
Private Sub startBtn_Click(sender As Object, e As EventArgs) Handles startBtn.Click
progressBar.Value = 0
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While checked = False
proxies.TryDequeue(0)
'do stuff
End While
checkedAmount += 1
Dim progress As Integer = ((checkedAmount / combos.Count) * 100)
If Me.InvokeRequired = True Then
Me.Invoke(New UpdateProgressBarDelegate(AddressOf UpdateProgressBar), progressBar, progress)
Else
UpdateProgressBar(progressBar, progress)
End If
'Task.ContinueWith(progressBar.Value = progress, TaskScheduler.FromCurrentSynchronizationContext())
End Sub)
tasks.Add(t)
Next
Task.WaitAll(tasks.ToArray())
End Sub
Private Sub UpdateProgressBar(ByVal ProgressBarUpdate As ProgressBar, progress As Integer)
progressBar.Value = progress
End Sub
Still doesn't work not sure why?
Now I know this can be done using a background worker but this isn't the best way of making your application multithreaded
Sort of.
BackgroundWorker is a poor way to run many different Tasks individually. No one wants to deal with a separate BackgroundWorker component for each Task. But one BackgroundWorker is a great way to spawn just one extra thread to manage all your other Tasks and update the progress bar. It's an easy solution here.
Either way, the one thing you'll want to do for sure is move the code to update the ProgressBar out of the individual Tasks. Having that inside a Tasks violates separation of concerns1. Once you do that, you'll also need to change the call to WaitAll() to use WaitAny() in a loop that knows how many tasks you have, so you can still update the ProgressBar as each Task finishes. This will likley have the side effect of fixing your current issue, as well.
Private Async Sub startBtn_Click(sender As Object, e As EventArgs) Handles startBtn.Click
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While Not checked
proxies.TryDequeue(0)
'do stuff
End While
End Sub)
tasks.Add(t)
Next
progressBar.Value = 0
For i As Integer = 1 To tasks.Count
Dim t = Await Task.WhenAny(tasks)
tasks.Remove(t)
progressBar.Value = (i / combos.Count) * 100
Next i
End Sub
1 The problem here illustrates one reason we care about separation of concerns at all. Once I fix this, the code becomes much simpler and the frustrating errors just go away.
The above waitany is unnecessary.
I have found that you might as well put your progress bar code directly into the task run sub:
Dim ProgressBarSync As New Object
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(
Sub()
'do stuff
SyncLock ProgressBarSync
ProgressBar.Increment(1)
End SyncLock
End Sub)
tasks.Add(t)
Next

How to update progressbar when retrieve data from database to datagridview VB.NET

i have VB.net code to retrieve data from SQL server (stored proc) to Datagridview (dgMC).
everything worked fine, but the progressbar1 not update. i'd like to progressbar1 shows update in percentage for user knows the status of data retrieve. the data is around 1000K.
Friend Delegate Sub SetDataSourceDelegate(table As DataTable)
Private Sub setDataSource(table As DataTable)
' Invoke method if required:
If Me.InvokeRequired Then
Me.Invoke(New SetDataSourceDelegate(AddressOf setDataSource), table)
Else
dgMC.DataSource = table
ProgressBar1.Visible = False
End If
End Sub
Private Sub loadTable()
Dim cnn As SqlConnection = GetConnection()
Dim cmdSearch As New SqlCommand("MC_Display", cnn)
cmdSearch.CommandType = CommandType.StoredProcedure
Try
cnn.Open()
Dim readerSearch = cmdSearch.ExecuteReader
If readerSearch.HasRows Then
Dim dt = New DataTable()
dt.Load(readerSearch)
setDataSource(dt)
Else
Me.Cursor = Cursors.Default
MsgBox("No Data Found.", MsgBoxStyle.Exclamation)
dgMC.DataSource = Nothing
End If
readerSearch.Close()
Catch ex As Exception
Throw ex
Finally
cnn.Close()
End Try
End Sub
Private Sub btnGoMC_Click(sender As Object, e As EventArgs) Handles btnGoMC.Click
ProgressBar1.Visible = True
ProgressBar1.Style = ProgressBarStyle.Marquee
Dim thread As New System.Threading.Thread(New System.Threading.ThreadStart(AddressOf loadTable))
thread.Start()
End Sub
To know exactly the number of data retrieve, you have to create a query with count(*) of your data.
Then, when you are retrieving data, you have to know which row is being retrieve, because you have to calculate the percentage.
And finally, you refresh your progressBar :
Dim percentage As Double = (currentRow / totalRows) * 100
ProgressBar.Value = Int32.Parse(Math.Truncate(percentage).ToString())
I hope it helps you
Since you're not performing anything that is quantifiable (you're issuing a sql query against a database, blocking processing for that thread until the database gives you the data you requested) you're not going to be able to update the progress bar with anything meaningful. In cases like this a Marquee scroll is usually sufficient. Also, you have the progress bar on the UI thread so it can be responsive and kick off a new thread to keep the UI thread from being blocked. At this moment you can't access the progress bar on the UI thread easily.
Some other thoughts though... If you have access to the Task Parallel Library, I would advise using that instead of creating a raw Thread and starting the process execution. You can utilize a type in the TPL called Task, which is an abstraction of Thread and takes care of some details you probably don't need to concern yourself with in this particular application/scenario. It also yields powerful asynchronous programming through the Async/Await paradigm in .NET 4.5. There is an excellent blog series by a guy named Stephen Cleary who has great expertise on this paradigm: Stephen Cleary Async/Await
A brief Example:
Private Sub btnGoMC_Click(sender As Object, e As EventArgs) Handles btnGoMC.Click
ProgressBar1.Visible = True
ProgressBar1.Style = ProgressBarStyle.Marquee
Dim thread As New System.Threading.Thread(New System.Threading.ThreadStart(AddressOf loadTable))
thread.Start()
End Sub
Can become:
`Private Async Sub btnGoMC_Click(sender As Object, e As EventArgs) Handles btnGoMC.Click
ProgressBar1.Visible = True
ProgressBar1.Style = ProgressBarStyle.Marquee
Await Task.Run(Sub() loadTable)
End Sub`

possible of multiple sqldependancy in vb.net?

Basically my code is based on here
http://www.dreamincode.net/forums/topic/185244-using-sqldependency-to-monitor-sql-database-changes/
Current situation is i'm having 2 table wish to monitor so i simple duplicate another similar code with first sqldependancy, but it's failed and seem like the latest sqldependancy will replace the previous sqldependancy function.
here is the code of mine
Public Sub GetNames()
If Not DoesUserHavePermission() Then
Return
End If
lbQueue.Items.Clear()
' You must stop the dependency before starting a new one.
' You must start the dependency when creating a new one.
Dim connectionString As String = GetConnectionString()
SqlDependency.Stop(connectionString)
SqlDependency.Start(connectionString)
Using cn As SqlConnection = New SqlConnection(connectionString)
Using cmd As SqlCommand = cn.CreateCommand()
cmd.CommandType = CommandType.Text
cmd.CommandText = "SELECT PatientID FROM dbo.[patient_queue]"
cmd.Notification = Nothing
' creates a new dependency for the SqlCommand
Dim dep As SqlDependency = New SqlDependency(cmd)
' creates an event handler for the notification of data changes in the database
AddHandler dep.OnChange, AddressOf dep_onchange
cn.Open()
Using dr As SqlDataReader = cmd.ExecuteReader()
While dr.Read()
lbQueue.Items.Add(dr.GetInt32(0))
doctor.lbqueue.items.add(dr.GetInt32(0))
End While
End Using
End Using
End Using
End Sub
Private Sub dep_onchange(ByVal sender As System.Object, ByVal e As System.Data.SqlClient.SqlNotificationEventArgs)
' this event is run asynchronously so you will need to invoke to run on the UI thread(if required)
If Me.InvokeRequired Then
lbQueue.BeginInvoke(New MethodInvoker(AddressOf GetNames))
My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Asterisk)
Else
GetNames()
End If
' this will remove the event handler since the dependency is only for a single notification
Dim dep As SqlDependency = DirectCast(sender, SqlDependency)
RemoveHandler dep.OnChange, AddressOf dep_onchange
End Sub
Public Sub GetMedID()
If Not DoesUserHavePermission() Then
Return
End If
lbMedQueue.Items.Clear()
' You must stop the dependency before starting a new one.
' You must start the dependency when creating a new one.
Dim connectionString As String = GetConnectionString()
SqlDependency.Stop(connectionString)
SqlDependency.Start(connectionString)
Using cn As SqlConnection = New SqlConnection(connectionString)
Using cmd As SqlCommand = cn.CreateCommand()
cmd.CommandType = CommandType.Text
cmd.CommandText = "SELECT RecordID FROM dbo.[medicine_queue]"
cmd.Notification = Nothing
' creates a new dependency for the SqlCommand
Dim dep As SqlDependency = New SqlDependency(cmd)
' creates an event handler for the notification of data changes in the database
AddHandler dep.OnChange, AddressOf dep_onchange2
cn.Open()
Using dr As SqlDataReader = cmd.ExecuteReader()
While dr.Read()
lbMedQueue.Items.Add(dr.GetInt32(0))
End While
End Using
End Using
End Using
End Sub
Private Sub dep_onchange2(ByVal sender As System.Object, ByVal e As System.Data.SqlClient.SqlNotificationEventArgs)
' this event is run asynchronously so you will need to invoke to run on the UI thread(if required)
If Me.InvokeRequired Then
lbMedQueue.BeginInvoke(New MethodInvoker(AddressOf GetMedID))
My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Asterisk)
Else
GetMedID()
End If
' this will remove the event handler since the dependency is only for a single notification
Dim dep As SqlDependency = DirectCast(sender, SqlDependency)
RemoveHandler dep.OnChange, AddressOf dep_onchange2
End Sub
finally i called GetNames,GetMedID on load form, it worked fine,just GetMedID is functioning and GetNames does not firing event when onchanged.
I think the main problem here is that you are calling .Stop and then .Start within each of your data access methods, thus cancelling and restarting the dependency each time you access the methods.
You just need to call .Start once when your application starts, and similarly .Stop when it ends.
For example, in a web application the best place for this is the Global.asax Application_Start and Application_End events.
I think you are right, I ran into the same issue. A second call to SqlDependency.Start(connectionString), even after a New SqlDependency(cmd), replaced the existing, initial default Service Broker service and queue.
Service Broker creates a default Service and Queue on each Start using the a GUID as part of the Service and Queue names: Service{GUID} and Queue{GUID} - but there seems to only be one defualt Service/Queue pair available
You can verify this by putting a break-point immediately after the first Start and immediately after the second Start. Go to SQL Server, go to your dBase and look at the
Service Broker/Services and Service Broker/Queues folders. You'll need to right-click on the Services and Queues folders and select refresh after the second break-point