Threads are the bane of my existence - vb.net

I have a button which on click will run a Sub, creating a process which runs a script.
When this script is finished an Exited handler will fire and run another Sub which cleans up so that the application is ready to go anew without restarting it.
I disable the button during the run and try to re-enable it when the Exit is fired, however it tells me that the button is in another thread. So I tried using SynchronizedContext and Post:
Declared at the start of my class:
Class MainWindow
Private sc As SynchronizationContext = SynchronizationContext.Current
Not sure if I'm doing that correctly but it worked for me elsewhere in the code where I had the same problem.
The exit handling sub:
Private Sub CMD_Exited(ByVal sender As Object, ByVal e As EventArgs)
myProcess.CancelOutputRead()
myProcess.CancelErrorRead()
sc.Post(AddressOf Button_Click, Button1.IsEnabled = True)
Close()
End Sub
Which errors:
Method 'Private Sub Button_Click(sender As Object, e As RoutedEventArgs)' does not have a signature compatible with delegate 'Delegate Sub SendOrPostCallback(state As Object)'.
What can I do here? Changing the button signature will cause incompatibilities elsewhere.
Are there better ways to get around this threads issue?

Visual Vincent is correct, you need to invoke on the UI thread. Specifically you need to read this How to: Make Thread-Safe Calls to Windows Forms Controls.
Public Delegate Sub DoProcessStuffOnUIThreadHandler()
Private Sub CMD_Exited(ByVal sender As Object, ByVal e As EventArgs)
If Me.Button1.InvokeRequired Then
Dim d As New DoProcessStuffOnUIThreadHandler(AddressOf DoProcessStuffOnUIThread)
Me.Button1.Invoke(d)
Else
DoProcessStuffOnUIThread()
End If
End Sub
Private Sub DoProcessStuffOnUIThread()
myProcess.CancelOutputRead()
myProcess.CancelErrorRead()
Button1.IsEnabled = True
Close()
End Sub
(28-SEP-2017) Edit to add an alternative, that I used frequently in my WinForms code days, for brevity:
Public Delegate Sub DoProcessStuffOnUIThreadHandler()
Private Sub CMD_Exited(ByVal sender As Object, ByVal e As EventArgs)
If Me.Button1.InvokeRequired Then
Dim d As New DoProcessStuffOnUIThreadHandler(AddressOf CMD_Exited)
Me.Button1.Invoke(d)
Else
myProcess.CancelOutputRead()
myProcess.CancelErrorRead()
Button1.IsEnabled = True
Close()
End If
End Sub
The added example simply reduces code use. Both examples end in the same result. Hope that helps.

Related

CrystalReportViewer.RefreshReport hangs when running from BackgroundWorker

I'm trying to "enhance" my reporting code by adding a loading screen while the Crystal Report is being prepared/loaded. Before I started trying to add the loading screen, all of my reports would come up just fine, but the cursor change just wasn't "enough" of an indication that the application was still working on pulling the report - some of them can take a while - so I wanted to provide a more "obvious" visual cue.
In order to accomplish this, I've put the report creation method calls into a BackgroundWorker that exists in the loading screen itself (I haven't gotten around to learning how to use Async/Await well enough yet to feel comfortable using that instead). The loading screen comes up correctly and everything appears to work as expected until it actually attempts to display the report on screen. At that point, the "Please wait while the document is processing." box comes up (in the CrystalReportViewer control in the form used to display reports), but it just sits there, not even spinning. Eventually, my IDE throws an error about receiving a ContextSwitchDeadlock and I pretty much just have to cancel execution.
Here's my dlgReportLoading "splash screen" with a PictureBox control that contains an animated GIF:
Imports System.Windows.Forms
Public Class dlgReportLoading
Private DisplayReport As Common.CRReport
Private WithEvents LoadReportWorker As System.ComponentModel.BackgroundWorker
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
End Sub
Private Sub dlgReportLoading_Load(sender As Object, e As EventArgs) Handles Me.Load
Me.Cursor = Cursors.WaitCursor
Me.TopMost = True
Me.TopMost = False
LoadReportWorker = New System.ComponentModel.BackgroundWorker
LoadReportWorker.RunWorkerAsync()
End Sub
Private Sub dlgReportLoading_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
Me.Cursor = Cursors.Default
End Sub
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
If Not DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None Then
Select Case DisplayReport.ReportOption
Case Common.CRReport.GenerateReportOption.DisplayOnScreen
'-- This is the method I'm currently testing
DisplayReport.ShowReport()
Case Common.CRReport.GenerateReportOption.SendToPrinter
DisplayReport.PrintReport()
Case Common.CRReport.GenerateReportOption.ExportToFile
DisplayReport.ExportReport()
End Select
End If
DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None
'--
'-- This code was in use before trying to generate the reports in the background
'If Not DisplayReport.CrystalReport Is Nothing Then
' DisplayReport.CrystalReport.Dispose()
'End If
'--
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
End Class
As noted in the code above, I'm currently testing the ShowReport() method as defined here:
Protected Friend Sub ShowReport()
Dim ReportViewer As frmReportPreview
Me.PrepareReport()
ReportViewer = New frmReportPreview(Me)
With ReportViewer
.WindowState = FormWindowState.Maximized
.Show()
End With
End Sub
And the frmReportPreview is this:
Imports System.ComponentModel
Public Class frmReportPreview
Private DisplayReport As Common.CRReport
Private ReportToDisplay As CrystalDecisions.CrystalReports.Engine.ReportDocument
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
PrepareReportForDisplay()
Me.rptViewer.ReportSource = Nothing
Me.rptViewer.ReportSource = ReportToDisplay
' SET ZOOM LEVEL FOR DISPLAY:
' 1 = Page Width
' 2 = Whole Page
' 25-100 = zoom %
Me.rptViewer.Zoom(1)
Me.rptViewer.Show()
End Sub
Private Sub frmReportPreview_Shown(sender As Object, e As EventArgs) Handles Me.Shown
'-- HANGS HERE
Me.rptViewer.RefreshReport()
End Sub
Private Sub frmReportPreview_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
ReportToDisplay.Dispose()
Me.rptViewer.ReportSource = Nothing
End Sub
'...CODE FOR PREPARING THE REPORT TO BE DISPLAYED
End Class
The dlgReportLoading form pops up correctly and the animation plays until the frmReportPreview pops up in front of it (it doesn't close). The little box that has what is normally an animated spinning circle indicating the report data is being loaded appears, but almost immediately freezes in place.
I have a breakpoint in the LoadReport_DoWork() method of my dlgReportLoading form after the call to the ShowReport() method, but it never gets to that point. I also have one in the LoadReport_Complete() method of that form that it never hits either and that dialog never actually closes.
I put another breakpoint at the end of the frmReportPreview_Shown method, right after the Me.rptViewer.RefreshReport() call, but it never hits that either, so it seems clear that this is where things are getting stuck, but only when the report is being generated through the BackgroundWorker. If I just call the ShowReport() method without sending it through the "splash screen" and BackgroundWorker, everything generates and displays normally.
I've tried putting the RefreshReport() method into its own BackgroundWorker with no change in the behavior. I've tried making the frmReportPreview object display modally with ShowDialog() instead of just Show(). None of this seems to help the issue.
I have a feeling something is being disposed of too early somewhere, but I can't figure out what that would be. I can provide the rest of the report preparation code from frmReportPreview if required, but that all seems to be working without error, as far as I can tell. I'm not averse to trying alternate methods of accomplishing my goal of showing the user a loading screen while all the report preparation is taking place - e.g., Async/Await or other multi-threading methods - so any suggestions are welcome. Please let me know if any additional clarification is needed.
ENVIRONMENT
Microsoft Windows 10 Pro 21H1 (OS build 19043.1348)
Microsoft Visual Studio Community 2017 (v15.9.38)
Crystal Reports for .NET Framework v13.0.3500.0 (Runtime version 2.0.50727)
EDIT: I forgot to mention that this whole mess is being called from a GenerateReport() method in my CRReport class defined as:
Public Sub GenerateReport(ByVal ReportGeneration As GenerateReportOption)
Me.ReportOption = ReportGeneration
If Me.ReportOption = GenerateReportOption.None Then
'...CODE FOR REQUESTING A GENERATION OPTION FROM THE USER
End If
Dim ReportLoadingScreen As New dlgReportLoading(Me)
ReportLoadingScreen.ShowDialog()
End Sub
Which, in turn, is being called from my main form like this:
Private Sub PrintMyXMLReport(ByVal XMLFile As IO.FileInfo)
Dim MyXMLReport As New IO.FileInfo("\\SERVER\Applications\Reports\MyXMLReport.rpt")
Dim Report As New Common.CRReport(MyXMLReport, XMLFile)
Report.GenerateReport(Common.CRReport.GenerateReportOption.DisplayOnScreen)
End Sub
You should separate the heavy lifting and UI operations into distinct methods in order to put them into the appropriate BackgroundWorker events:
Protected Friend Sub PrepareReport()
' perform long-running background work
End Sub
Protected Friend Sub ShowReport()
Dim ReportViewer = New frmReportPreview(Me) With {.WindowState = FormWindowState.Maximized}
ReportViewer.Show()
End Sub
Private DisplayReport As Common.CRReport
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
DisplayReport.PrepareReport()
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
DisplayReport.ShowReport()
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
because LoadReport_DoWork actually runs on a new non-UI thread, and LoadReport_Complete runs on the caller thread, which is a UI thread. Only there can you interact with the UI and show Forms etc.

declared event which fires, but is not heard

I have a vb.net application which contains two forms. One is called "MapComponentForm" and another called "ComponentPropertiesForm". In the MapComponentForm I have defined an event which I want fired when the OK button is clicked. If the ComponentPropertiesForm is open, I want it to "hear" this event and act accordingly. Stepping through the code the event seems to fire as expected but the ComponentPropertiesForm seems to be oblivious to this. I've attached the code from the two forms after stripping out the non relevant code in hopes that someone can tell me why my event goes unheeded. I used the information here:
https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/events/walkthrough-handling-events
in constructing my own code.
Thanks for any suggestions.
Public Class MapComponentForm
Public Event PartMapped(ByRef status As Boolean)
Public Sub New(shape As Visio.Shape)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_shape = shape
End Sub
Private Sub OkButton_Click(sender As Object, e As EventArgs) Handles OkButton.Click
'If the component properties window is open, refresh it
If Application.OpenForms().OfType(Of ComponentPropertiesForm).Any Then
RaiseEvent PartMapped(True)
End If
end sub
end class
Public Class ComponentPropertiesForm
Private WithEvents mPartMap As MapComponentForm
Private Sub ComponentPropertiesForm_Load(sender As Object, e As EventArgs) Handles Me.Load
mPartMap = New MapComponentForm(Nothing)
End Sub
Private Sub mPartMap_PartMapped(ByRef status As Boolean) Handles mPartMap.PartMapped
If status = True Then
MsgBox("something got mapped")
End If
End Sub
end class

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.

backgroundworker throws "An error occurred creating the form..."

I have main forms that has a button that opens a master-customer on datagrid. I use dataview for the purpose of filtering the data using dataview.rowfilter.
The problem is, during the form load. It takes 5-6 seconds (the program is unresponsive during that time). What I'm trying to do is to load the data to the dataview on the background and show it on the gridview on workercompleted.
it gave me this error: "An error occurred creating the form. See Exception.InnerException for details. The error is: Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it." --> on dowork
I read somewhere that i should use Invoke. But i don't know how to use it.
here is my code:
Private Sub custcall_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
TBfind.Enabled = False
SetMyCustomFormat("yyyy-MM-dd HH:mm:ss")
BWcustload.RunWorkerAsync()
End Sub
Private Sub BWcustload_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BWcustload.DoWork
mydataview = New DataView(datatablecust)
End Sub
Private Sub BWcustload_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BWcustload.RunWorkerCompleted
DGVcustomer.DataSource = mydataview
TBfind.Enabled = True
End Sub
Have you tried moving the code to the event Form_Shown?
Also, have you tried putting the code in your DoWork section inside a SyncLock?
Try this:
SyncLock mydataview
mydataview = New DataView(datatablecust)
End SyncLock
As for the STA model ... try adding this to your code and set the startup object to Sub Main()
<STAThread()> _
Public Shared Sub Main()
Dim mainForm As New custcall()
Application.Run(mainForm)
End Sub
EDITED:
As far as invoke is concerned. ... that would definitely work, too ... but I don't know that it would have solved the STAThread issue.
To use invoke, first you have to declare a delegate sub in your form:
Delegate Sub LoadDataCallback()
Declare a function to take care of the actual loading of the data:
Private Sub LoadData()
mydataview = New DataView(datatablecust)
End Sub
Then, you would start a new thread in your Shown event (or Load event):
Dim mythread As New Thread(Sub()
Dim callLoad As New LoadDataCallBack(LoadData)
Me.Invoke(callLoad)
End Sub)
mythread.Start()
This gets around having to use SyncLock (although in some cases you may want to if it doesn't end up working correctly).
Don't forget to add Imports System.Threading to the top of your code file.

Update label from mainform class with backgroundworker from another class

I have two classes.
Public Class MainForm
Private Project As clsProject
Private Sub btnDo_Click
...
Backgroundworker.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject
End Sub
and two methods inside MainForm
Public Shared Sub setLabelTxt(ByVal text As String, ByVal lbl As Label)
If lbl.InvokeRequired Then
lbl.Invoke(New setLabelTxtInvoker(AddressOf setLabelTxt), text, lbl)
Else
lbl.Text = text
End If
End Sub
Public Delegate Sub setLabelTxtInvoker(ByVal text As String, ByVal lbl As Label)
end class
I want to update the labels of MainForm from the clsProject constructor.
MainForm.setLabelTxt("Getting prsadasdasdasdasdry..", MainForm.lblProgress)
but it does not update them.
What am I doing wrong?
The problem is that you are using the global MainForm instance to access the label in a background thread here:
Public Class clsProject
Public Sub New()
' When accessing MainForm.Label1 on the next line, it causes an exception
MainForm.setLabelTxt("HERE!", MainForm.Label1)
End Sub
End Class
It's OK to call MainForm.setLabelTxt, since that is a shared method, so it's not going through the global instance to call it. But, when you access the Label1 property, that's utilizing VB.NET's trickery to access the global instance of the form. Using the form through that auto-global-instance variable (which always shares the same name as the type) is apparently not allowed in non-UI threads. When you do so, it throws an InvalidOperationException, with the following error message:
An error occurred creating the form. See Exception.InnerException for details. The error is: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.
I'm guessing that the reason you are not seeing the error is because you are catching the exception somewhere and you are simply ignoring it. If you stop using that global instance variable, the error goes away and it works. For instance, if you change the constructor to this:
Public Class clsProject
Public Sub New(f As MainForm)
' The next line works because it doesn't use the global MainForm instance variable
MainForm.setLabelTxt("HERE!", f.Label1)
End Sub
End Class
Then, in your MainForm, you would have to call it like this:
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject(Me) ' Must pass Me
End Sub
Using the global instance from the background thread is not allowed, but when we use the same label from the background thread, without going through that global variable it works.
So it's clear that you cannot use the global MainForm variable from a background thread, but what may not be clear is that it's a bad idea to use it ever. First, it's confusing because it shares the same name as the MainForm type. More importantly, though, it is a global variable, and global state of any kind is almost always bad practice, if it can be avoided.
While the above example does solve the problem, it's still a pretty poor way of doing it. A better option would be to pass the setLabelTxt method to the clsProject object or even better have the clsProject simply raise an event when the label needs to be changed. Then, the MainForm can simply listen for those events and handle them when they happen. Ultimately, that clsProject class is probably some sort of business class which shouldn't be doing any kind of UI work anyway.
You cannot execute any action on GUI-elements from the BackgroundWorker directly. One way to "overcome" that is by forcing the given actions to be performed from the main thread via Me.Invoke; but this is not the ideal proceeding. Additionally, your code mixes up main form and external class (+ shared/non-shared objects) what makes the whole structure not too solid.
A for-sure working solution is relying on the specific BGW methods for dealing with GUI elements; for example: ProgressChanged Event. Sample code:
Public Class MainForm
Private Project As clsProject
Public Shared bgw As System.ComponentModel.BackgroundWorker
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
bgw = BackgroundWorker1 'Required as far as you want to called it from a Shared method
BackgroundWorker1.WorkerReportsProgress = True
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject
End Sub
Public Shared Sub setLabelTxt(ByVal text As String)
bgw.ReportProgress(0, text) 'You can write any int as first argument as far as will not be used anyway
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Me.Label1.Text = e.UserState 'You can access the given GUI-element directly
Me.Label1.Update()
End Sub
End Class
Public Class clsProject
Public Sub New()
MainForm.setLabelTxt("Getting prsadasdasdasdasdry..")
End Sub
End Class
Try:
Me.Invoke(...)
instead of lbl.Invoke(.... I had to do this. This is my implementation:
Delegate Sub SetTextDelegate(ByVal args As String)
Private Sub SetTextBoxInfo(ByVal txt As String)
If txtInfo.InvokeRequired Then
Dim md As New SetTextDelegate(AddressOf SetTextBoxInfo)
Me.Invoke(md, txt)
Else
txtInfo.Text = txt
End If
End Sub
And this worked for me.