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?
Related
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
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`
I have a process running in a separate thread which reads data over Ethernet and raises an event for each read. Then, this data is processed over a tree of classes. At the top of my class tree, I want to store the processed data to SQL tables. This read rate can be rather fast and often at times but on average its slower than my write rate to SQL. This is why I want a queue. Also I want my window not to freeze, so I want a separate thread.
The code below works but I'm fairly inexperienced in multi threading, is this a good and robust way to accomplish my task?
Private addLogQueue As New Queue
Private dequeueing As Boolean = False
Public Sub addLogHandler(ByVal machineName As String, ByVal LogTime As Date, ByVal EventLog As String, ByVal EventValue As String)
addLogQueue.Enqueue("INSERT INTO ... some SQL CODE")
Dim Thread1 As New System.Threading.Thread(AddressOf addLog)
Thread1.Start
End Sub
Private Sub addLog()
If Not dequeueing Then
dequeueing = True
While addLogQueue.Count <> 0
Try
SQLCon.Open()
SQLCmd = New SqlCommand(addLogQueue.Dequeue(), SQLCon)
SQLCmd.ExecuteNonQuery()
SQLCon.Close()
Catch ex As Exception
MsgBox(ex.Message)
If SQLCon.State = ConnectionState.Open Then
SQLCon.Close()
End If
End Try
End While
dequeueing = False
End If
End Sub
I would refer you to the article: Beginners Guide to Threading. Also, for performance reasons you may want to keep the thread open, and the database connection open and simply use a wait event to determine if the queue should be checked for more entries. See this article: An Overview of Synchronization. Keeping the connection open would really depend on the frequency of arrival of messages. Very frequent: keep connection open, intermittent: Close after use.
Dim Thread1 As New System.Threading.Thread(AddressOf addLog)
Private Shared mre As New ManualResetEvent(False)
Private addLogQueue As New Queue
Private shutDown as boolean = false
Public Sub Main
Thread1.Start
End Sub
Public Sub addLogHandler(ByVal machineName As String, ByVal LogTime As Date, ByVal EventLog As String, ByVal EventValue As String)
addLogQueue.Enqueue("INSERT INTO ... some SQL CODE")
mre.Set()
End Sub
Private Sub addLog()
dim cmdString as String
Try
Using SQLCon as New SQLConnection
SQLCon.Open()
While Not shutDown
mre.WaitOne()
mre.Reset()
While addLogQueue.Count <> 0
cmdString = addLogQueue.Dequeue()
SQLCmd = New SqlCommand(cmdString, SQLCon)
SQLCmd.ExecuteNonQuery()
End While
End While
End Using
Catch ex As Exception
My.Application.Log.WriteException(ex,TraceEventType.Error, cmdString)
End Try
End Sub
Also, you may want to lookup a few articles on writing services as this sort of thing works very much better as a service, if you want the program to do unattended monitoring. Also, you could have one thread accepting the command strings and adding them to the queue, and the second thread writing them to the database.
I am currently working in vb.net windows form applications. I have two DataGridViews and I want to cross reference the rows and remove certain rows out of datagridview1, depending on if a checkbox is checked in datagridview2. This problem arises when I check a box for an action taken by a front end user in datagridview1 that updates datagridview2. But, when I refresh both DataGridViews, datagridview2 shows that this action has been taken, but the checkbox column in datagridview1 goes back to unchecked, thus telling the front end user to repeat this action.
On a side note all the data is bound to an sql table, except the checkbox column in datagridview1. Also note, I have a refresh button that repeats the load event to refresh both tables.
Page Load Event Handler:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'load datagridview1'
Dim ds As New DataSet
Dim AA As New DataSet
connectionstring = "Data source = .\sqlexpress; integrated security = true"
connection = New SqlConnection(connectionstring)
sql = "SELECT Shear FROM production.dbo.stagingcompleted"
Try
connection.Open()
adapter = New SqlDataAdapter(sql, connectionstring)
adapter.Fill(ds)
connection.Close()
DataGridView1.DataSource = ds.Tables(0)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
'load datagridview2'
connectionstring = "Data source = .\sqlexpress; integrated security = true"
connection = New SqlConnection(connectionstring)
sql = "SELECT * FROM production.dbo.tblFCOrdered"
Try
connection.Open()
adapter = New SqlDataAdapter(sql, connectionstring)
adapter.Fill(AA)
connection.Close()
DataGridView2.DataSource = AA.Tables(0)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
End Sub
DataGridView1 Event Handler (CellContentClick):
Private Sub DataGridView1_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellContentClick
'update datagridview1 to the action for that row complete'
If e.ColumnIndex <> 0 Then
Exit Sub
End If
Dim v As String = DataGridView1.Rows(e.RowIndex).Cells(1).Value
Select Case MsgBox("Are you sure shear " & v & " has been cut?", MsgBoxStyle.YesNo)
Case MsgBoxResult.No
Exit Sub
Case MsgBoxResult.Yes
Try
Dim connstring = "Data Source=.\sqlexpress; integrated security = true"
Using conn1 As New SqlConnection(connstring)
conn1.Open()
Using comm1 As SqlCommand = New SqlCommand("UPDATE Production.dbo.tblFCOrdered SET FormChannel = 1 WHERE SHEAR = '" & v & "'", conn1)
comm1.ExecuteNonQuery()
conn1.Close()
End Using
End Using
Catch ex As Exception
MsgBox(ex.ToString)
End Try
End Select
I am possibly incorrect and I apologise if you have already tried this. I believe you might benefit from reading up about the page life-cycle, order event handlers and the effects of PostBack (for example this article)
In server controls, certain events, typically click events, cause the page to be posted back immediately to the server. Change events in HTML server controls and Web server controls, such as the TextBox control, do not immediately cause a post. Instead, they are raised the next time a post occurs.
In regards to PostBack and Page.Load:
After a page has been posted back, the page's initialization events (Page_Init and Page_Load) are raised, and then control events are processed. You should not create application logic that relies on the change events being raised in a specific order unless you have detailed knowledge of page event processing.
I think that part of the problem you are experiencing is that the first thing the page will do when you trigger a PostBack, e.g. by clicking on a cell, is the code which you have in your Page.Load event handler. In your code you will find that when the page is processed (on PostBack) the first thing that happens is that the grids are data bound. Because of this any changes made will not be persisted when the other event handlers are called. A common check before performing actions such as data binding in the Page.Load function is to check that the page is not being posted back, if IsPostBack = true it is likely that you will want to skip such actions.
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