Update GridView by a BackgroundWorker Completion makes Grid non interactive - vb.net

I am trying to update GridView on completion of a BackgroundWorker, For the first time it works correctly, but if try execute worker again, data will be assigned to the grid but i could not select a Row on the GridView on UI level and also vertical scroll is now shown. If try to double click cells several times then vertical scroll will appear and i could select any row.
Please refer the VB.Net Code
Public Class Form1
Dim Workers() As BackgroundWorker
Dim dtCustomers As DataTable = New DataTable()
Private dtCustomersLock As New Object
Private dgvCustomersLock As New Object
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
dtCustomers.Columns.Add("CustomerID")
dtCustomers.Columns.Add("CustomerName")
dtCustomers.Columns.Add("Age")
LoadWorkers()
End Sub
Private Sub btnLoad_Click(sender As Object, e As EventArgs) Handles btnLoad.Click
btnClear_Click(sender, e)
loadCustomerGrid()
UpdateCustomerGrid()
End Sub
Private Sub LoadWorkers()
ReDim Workers(1)
Workers(1) = New BackgroundWorker
Workers(1).WorkerReportsProgress = True
Workers(1).WorkerSupportsCancellation = True
AddHandler Workers(1).DoWork, AddressOf loadCustomerGrid
AddHandler Workers(1).RunWorkerCompleted, AddressOf UpdateCustomerGrid
End Sub
Private Sub btnLoadThread_Click(sender As Object, e As EventArgs) Handles btnLoadThread.Click
If Not Workers(1).IsBusy Then
dtCustomers.Clear()
Workers(1).RunWorkerAsync()
End If
End Sub
Private Sub loadCustomerGrid()
SyncLock dgvCustomersLock
For i As Integer = 0 To 10
dtCustomers.Rows.Add(i, "Customer" + i.ToString(), "20" + i.ToString())
Next
End SyncLock
Threading.Thread.Sleep(100)
End Sub
Private Sub UpdateCustomerGrid()
SyncLock dtCustomersLock
DataGridView1.DataSource = dtCustomers
DataGridView1.Focus()
End SyncLock
End Sub
Private Sub btnClear_Click(sender As Object, e As EventArgs) Handles btnClear.Click
dtCustomers.Clear()
End Sub
End Class

Because you are accessing the DataGridView1 of UI thread from the Worker thread you get the weird behaviour.
I tested your small app with this code and I got the normal expected behaviour.
I modified your loadCustomerGrid method and added another Method and Delegate method.
Private Sub loadCustomerGrid()
SetDataGrid(GridView1)
Threading.Thread.Sleep(100)
End Sub
Private Sub setDataGrid(ByVal grd As DataGridView)
If grd.InvokeRequired Then
grd.Invoke(New setDataGridInvoker(AddressOf setDataGrid), grd)
Else
For i As Integer = 0 To 10
dtCustomers.Rows.Add(i, "Customer" + i.ToString(), "20" + i.ToString())
Next
End If
End Sub
Private Delegate Sub setDataGridInvoker(ByVal grd As DataGridView)
Explanation:
"The way to safely access controls from worker threads is via delegation. First you test the InvokeRequired property of the control, which will tell you whether or not you can safely access the control. InvokeRequired is one of the few members of the Control class that is thread-safe, so you can access it anywhere. If the property is True then an invocation is required to access the control because the current method is executing on a thread other than the one that owns the control's Handle.
The invocation is performed by calling the control's Invoke or BeginInvoke method. You create a delegate, which is an object that contains a reference to a method. It is good practice to make that a reference to the current method. You then pass that delegate to the Invoke or BeginInvoke method. That will essentially call the referenced method again, this time on the thread that owns the control's Handle."
Source: jmcilhinney post Accessing Controls from Worker Threads http://www.vbforums.com/showthread.php?498387-Accessing-Controls-from-Worker-Threads

Related

Replacement for thread.start() and thread.abort()

I need to display a form for some amount of time - basically a "please wait, loading" form with progress bar. When certain operation completes, I want this window to disappear. Here's my try at it:
If IsNothing(mlLabels) Or mblnIsLoading Then Exit Sub
If mstrPrinterA.Equals(Me.cmbPrinters.Text, StringComparison.OrdinalIgnoreCase) Then
Exit Sub
End If
Dim th As New Threading.Thread(AddressOf WaitPrinter)
th.Start()
If mlLabels.IsPrinterOnLine(Me.cmbPrinters.Text) Then
Me.cmbPrinters.BackColor = Drawing.Color.Green
Else
Me.cmbPrinters.BackColor = Drawing.Color.Red
End If
th.Abort()
Do While th.IsAlive
Loop
th = Nothing
mstrPrinterA = Me.cmbPrinters.Text
Private Sub WaitPrinter()
Dim fw As New FormWaiting
fw.ShowDialog()
fw = Nothing
End Sub
However, I then read that using Thread.Start() and Thread.Abort() is not considered a good practice. Is there another way I can do that?
Here is a simple example of what I described in my comment above. Create a WinForms project with two forms, adding a Button to Form1 and a BackgroundWorker to Form2. Add this code to Form1:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'Display a dialogue while the specified method is executed on a secondary thread.
Dim dialogue As New Form2(New Action(Of Integer)(AddressOf Pause), New Object() {5})
dialogue.ShowDialog()
MessageBox.Show("Work complete!")
End Sub
Private Sub Pause(period As Integer)
'Pause for the specified number of seconds.
Threading.Thread.Sleep(period * 1000)
End Sub
and this code to Form2:
Private ReadOnly method As [Delegate]
Private ReadOnly args As Object()
Public Sub New(method As [Delegate], args As Object())
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.method = method
Me.args = args
End Sub
Private Sub Form2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'Execute the specified method with the specified arguments.
method.DynamicInvoke(args)
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
'Close the dialogue when the work is complete.
Close()
End Sub
Run the project and click the Button on the startup form. You'll see the dialogue displayed while the work is executed and then disappear when it's done. The dialogue is written in such a way that it can be used to invoke any method with any arguments. It's the caller that gets to define what the work to be performed is.
In this particular case, the "work" is a simple sleep but you can put anything you like in there. Just note that it is executed on a secondary thread so no direct interaction with the UI is allowed. If you need UI interaction then that could be accomplished but you'd need slightly more complex code. Note that the code as it is also does not allow for returning a result from the executed method, but you could support that fairly easily too.

Update Label And ProgressBar value from another class invoked in a background worker

hello I have a problem to update a progress bar and a label inside a StatusStrip in the main form.
there are 2 controls in the form inside a StatusStrip:
Progressbar (ToolStripProgressBar)
ProgressLabel (ToolStripStatusLabel)
Basically I have this situation:
Public Class Main
Public Sub TEST(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles TEST.DoWork
Dim tmp as New NAMESPACE1.CLASS2(VALUES)
End Sub
End Class
Namespace NAMESPACE1
Public Class CLASS2
Public Sub New(VALUES)
Main.Progressbar.Value = 15
Main.ProgressLabel.Text = "hello!"
End Sub
End Class
End Namespace
The problem is that text or value of the controls are updated (I see it using breakpoints) in the code but not in the form in which progressbar is always a 0% and label always as nothing.
I think it's an update or refresh problem of the main form. i have tried to do Main.Refresh() and Main.Update() but it does not work anyway.
Thanks in advance.
You have 2 issues in play. The first is that Main is a class name, not a runtime reference or object variable. See Idle_Mind's answer for using Me to get the runtime object reference.
The second problem is that since Class2 is created in DoWork, it is created on the background thread, which will prevent it from accessing UI controls (which are created on the UI thread). You will get an illegal cross thread operation exception (even if you dont see it).
I'd suggest that Class2 does nothing useful which can't be done using the ReportProgress method. Getting rid of it also gets rid of the form reference issue since an event is raised on the same thread as the UI controls:
Private WithEvents bgw As BackgroundWorker
...
' in a button click or whatever starts the worker:
bgw = New BackgroundWorker
bgw.WorkerReportsProgress = True
bgw.RunWorkerAsync(5) ' times to loop
...
Private Sub bgw_DoWork(sender As Object,
e As DoWorkEventArgs) Handles bgw.DoWork
' NOTE
' This code executes on a different thread
' so do not reference UI controls!
' e.Argument is the value passed - amount of work
Dim max As Integer = CInt(e.Argument)
For n As Integer = 1 To max
Threading.Thread.Sleep(250) ' emulates work
' causes the ProgressChanged event to fire:
bgw.ReportProgress(n, String.Format("{0} of {1}", n.ToString, max.ToString))
Next
End Sub
Private Sub bgw_ProgressChanged(sender As Object,
e As ProgressChangedEventArgs) Handles bgw.ProgressChanged
'ProgressChanged fires on the UI thread, so it is safe to
' referenece controls here
TextBox4.Text = e.UserState.ToString
TextBox4.Refresh()
End Sub
Paste the code and you can see the message change in the TextBox. The same would work using your ProgressBar and ProgressLabel.
bgw.ReportProgress(n, arg)
The first argument will map to e.ProgressPercentage in the ProgressChanged event. The second is optional - UserState. I used it to pass a string for illustrative purposes (the form can already know the amount of work since it told the BGW what to do.)
If Class2 has some other purpose, you can use it as long as it is created on the UI thread (in the form) and used on that thread (ie in ProgressChanged event). You also need a method to talk to the controls so you dont have to create a new one each time:
Private myObj As Class2 ' declaration
...
myObj = New Class2(Me) ' instance with frm ref
In class2:
Public Sub Update(value As Integer, msg As String)
frmMain.Progressbar.Value = value
frmMain.ProgressLabel.Text = msg
End Sub
Then in the ProgressChanged event:
myObj.Update(x, y)
Where x and y are the value and message from whereever.
Here's an example of passing a reference to MAIN as suggested by Plutonix. I've intentionally left your pseudo-code style intact:
Public Class MAIN
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
TEST.RunWorkerAsync()
End Sub
Private Sub TEST_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles TEST.DoWork
Dim tmp As New NAMESPACE1.CLASS2(Me, VALUES) ' <-- Form reference being passed via 1st parameter
End Sub
End Class
Namespace NAMESPACE1
Public Class CLASS2
Private frmMain As MAIN
Public Sub New(ByVal frmMain As MAIN, VALUES)
Me.frmMain = frmMain
Me.frmMain.Progressbar.Value = 15
Me.frmMain.ProgressLabel.Text = "hello!"
End Sub
End Class
End Namespace

How to call a Sub without knowing which form is loaded into panel?

On every DataGridView1_SelectionChanged event I need to run a Private Sub OnSelectionChanged() of the form that is loaded into Panel1 (see the image http://tinypic.com/r/2nu2wx/8).
Every form that can be loaded into Panel1 has the same Private Sub OnSelectionChanged() that initiates all the necessary calculations. For instance, I can load a form that calculates temperatures or I can load a form that calculates voltages. If different element is selected in the main form’s DataGridView1, either temperatures or voltages should be recalculated.
The problem is - there are many forms that can be loaded into Panel1, and I’m struggling to raise an event that would fire only once and would run the necessary Sub only in the loaded form.
Currently I’m using Shared Event:
'Main form (Form1).
Shared Event event_UpdateLoadedForm(ByVal frm_name As String)
'This is how I load forms into a panel (in this case frm_SCT).
Private Sub mnu_SCT_Click(sender As Object, e As EventArgs) Handles mnu_SCT.Click
frm_SCT.TopLevel = False
frm_SCT.Dock = DockStyle.Fill
Panel1.Controls.Add(frm_SCT)
frm_SCT.Show()
Var._loadedForm = frm_SCT.Name
RaiseEvent event_UpdateLoadedForm(Var._loadedForm)
End Sub
‘Form that is loaded into panel (Form2 or Form3 or Form4...).
Private WithEvents myEvent As New Form1
Private Sub OnEvent(ByVal frm_name As String) Handles myEvent.event_UpdateLoadedForm
‘Avoid executing code for the form that is not loaded.
If frm_name <> Me.Name Then Exit Sub
End Sub
This approach is working but I’m sure it can be done way better (I'd be thankful for any suggestions). I have tried to raise an event in the main form like this:
Public Event MyEvent As EventHandler
Protected Overridable Sub OnChange(e As EventArgs)
RaiseEvent MyEvent(Me, e)
End Sub
Private Sub DataGridView1_SelectionChanged(sender As Object, e As EventArgs) _
Handles DataGridView1.SelectionChanged
OnChange(EventArgs.Empty)
End Sub
but I don't know to subscribe to it in the loaded form.
Thank you.
Taking into account Hans Passant’s comments as well as code he posted in related thread I achieved what I wanted (see the code below).
Public Interface IOnEvent
Sub OnSelectionChange()
End Interface
Public Class Form1
' ???
Private myInterface As IOnEvent = Nothing
' Create and load form.
Private Sub DisplayForm(frm_Name As String)
' Exit if the form is already displayed.
If Panel1.Controls.Count > 0 AndAlso _
Panel1.Controls(0).GetType().Name = frm_Name Then Exit Sub
' Dispose previous form.
Do While Panel1.Controls.Count > 0
Panel1.Controls(0).Dispose()
Loop
' Create form by its full name.
Dim T As Type = Type.GetType("Namespace." & frm_Name)
Dim frm As Form = CType(Activator.CreateInstance(T), Form)
' Load form into the panel.
frm.TopLevel = False
frm.Visible = True
frm.Dock = DockStyle.Fill
Panel1.Controls.Add(frm)
' ???
myInterface = DirectCast(frm, IOnEvent)
End Sub
Private Sub DataGridView1_SelectionChanged(sender As Object, e As EventArgs) _
Handles DataGridView1.SelectionChanged
' Avoid error if the panel is empty.
If myInterface Is Nothing Then Return
' Run subroutine in the loaded form.
myInterface.OnSelectionChange()
End Sub
End Class
One last thing – it would be great if someone could take a quick look at the code (it works) and confirm that it is ok, especially the lines marked with “???” (I don’t understand them yet).

SerialPort and Control Updating in MDI form

As my title implies i have the following problem, i am receiving data from serial port and i update a richtextbox in a MDI Form with the control.invoke method
(Code in SerialPort.DataReceived Event)
If myTerminal.Visible Then
myTerminal.MyRichTextBox1.Invoke(New MethodInvoker(Sub()
myTerminal.MyRichTextBox1.AppendText(dataLine & vbCrLf)
End Sub))
End If
But as a mdi form it has the ability to close and reopen. So when the serialport is sending data to richtextbox and the user click the close button and the form gets disposed. Then the error "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."... Any Idea????
My regards,
Ribben
That code is not in the SerialPort.DataReceived event it is in the event handler. (Yes, I'm nitpicking, but it points to a solution.) The best thing to do is have the form that owns myTerminal add the handler when it is created and remove the handler when it closes.
Thank you for your answer but unfortunately that's not the solution. First of all my SerialPort Class must inform 2 Forms (Form with richtextbox, Form with Listview) and another class which is responsible for drawing (Unmanaged Directx 9.0c about 4 Forms), so to implement right the serialport class i have made my own events. Again to the problme, it caused because the Serialport.DataReceived everytime it occurs creates a thread in the threadpool and when i dispose the form simply it's too slow to catch up with all the threads and so there is at least one thread which invokes the control which is already disposed!
As a temp solution i came up with (The Below code is in the TerminalForm Class which inherits Form):
Private VisibleBoolean As Boolean = False
Private Index As Integer = 0
Private Sub DataToAppend(ByVal _text As String)
If VisibleBoolean Then
Me.MyRichTextBox1.Invoke(New MethodInvoker(Sub()
Me.MyRichTextBox1.AppendText(_text & vbCrLf)
End Sub))
ElseIf Index = 1 Then
Index = 0
myDispose()
RemoveHandler myserialport.DataToSend2, AddressOf DataToAppend
End If
End Sub
Private Sub Me_Activated(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Activated
VisibleBoolean = True
AddHandler myserialport.DataToSend2, AddressOf DataToAppend
End Sub
Private Sub myDispose()
If Index = 0 And Not Me.IsDisposed Then
Me.Invoke(New MethodInvoker(Sub()
MyBase.Dispose(True)
End Sub))
End If
End Sub
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
End Sub
Protected Overrides Sub OnFormClosing(ByVal e As System.Windows.Forms.FormClosingEventArgs)
Index = 1
VisibleBoolean = False
End Sub
I know i don't like either but at least it's working!
Anyother improvement or suggestion is more

Creating a form in a new thread (From an Event)

I have a small form that notifies the user on the completion of events (such as SMO restore).
I want this form to appear from various sources (such as the below SMO Restore complete event) so I guess I need to create a new thread before creating the form? As it could be called from outside the UI thread. (I also need to pass a string to this form)
The child form fades in out using a timer + Opacity.
What am I doing wrong here?
Private Sub CompleteEventHandler(ByVal sender As Object, ByVal e As Microsoft.SqlServer.Management.Common.ServerMessageEventArgs)
MyThread = New System.Threading.Thread(AddressOf DoStuff)
MyThread.Start("meh")
End Sub
Private Delegate Sub DoStuffDelegate(ByVal MsgString As String)
Private Sub DoStuff(ByVal MsgString As String)
If Me.InvokeRequired Then
Me.Invoke(New DoStuffDelegate(AddressOf DoStuff))
Else
Dim TempMessage As New frmNotification
TempMessage.lblMessage.Text = MsgString
TempMessage.Show()
End If
End Sub
Don't start a new thread, there's no point since you're already running on another thread and InvokeRequired will always be True. The mistake is that you call Me.Invoke() but forget to pass the "MsgString" argument. You'll also want to use Me.BeginInvoke(), no need to wait. Thus:
Private Sub CompleteEventHandler(ByVal sender As Object, ByVal e As EventArgs)
Me.BeginInvoke(New DoStuffDelegate(AddressOf DoStuff), "meh")
End Sub
Private Sub DoStuff(ByVal MsgString As String)
Dim TempMessage As New frmNotification
TempMessage.lblMessage.Text = MsgString
TempMessage.Show()
End Sub