I have a project in VSTO/VB using a BackgroundWorker that works fine. It is a form that calls for a web page of information. The web page can take a while, so I have the form calling with the BackgroundWorker.
I then have an Excel Addin project that has added the BackgroundWorker project. When I call up the form from the Excel Addin project and use the BackgroundWorker to request the web page, it grabs the web page ok. But the work done upon completion, during the BackgroundWorker1_RunWorkerCompleted method, is resulting in an error message:
"Cross-thread operation not valid: Control 'TabPage2' accessed from a thread other than the thread it was created on."
Why does the BackgroundWorker project not work when called from the Excel Addin project?
I note that when I set the BackgroundWorker project as the "Startup Project" there is no error generated. Its something to do with calling this BackgroundWorker project from the Excel Addin project.
Edit: Could be that I am calling RunWorkerAsync() from a non UI thread?
I have an Excel Addin project with a Ribbon class. The Ribbon1.vb has a button click method that creates an instance of the second project, from which the backgroundworker will be called:
Private Sub Btn_Click(ByVal sender As System.Object, ByVal e As Microsoft.Office.Tools.Ribbon.RibbonControlEventArgs) Handles Btn.Click
Dim MySecondProject As SecondProject.Form1 = New SecondProject.Form1()
MySecondProject.Show()
End Sub
MySecondProject is then calling the BackgroundWorker from within its own button click method as:
BackgroundWorker1.RunWorkerAsync()
Then, after it completes, the backgroundworker is trying to update a label in MySecondProject:
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
SuccessLabel.Text = "Success!"
End Sub
When MySecondProject was the Startup Project, the backgroundworker kept track of the correct thread and updated the label successfully upon completion. With the Excel Addin as the Startup Project, and MySecondProject instantiated during runtime, the backgroundworker seems to lose track of the correct thread. Should I manually be inserting Invoke or BeginInvoke somewhere to help the backgroundworker keep track of the correct thread to update?
So, it turns out that a Backgroundworker launched from a Form launched from a Ribbon cannot update a control at the end of processing. I am not sure why it works from a Form launched as a Startup project while it does not work from a form launched from the Ribbon, but there it is -- turns out that you need to deal with the Backgroundworker losing track of the UI thread.
Using a MethodInvoker works, as in the following snippet:
Imports System.Windows.Forms
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
[do nothing]
End Sub
Private myString As String = "This is my string"
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
If Label1.InvokeRequired Then
Dim mi As MethodInvoker = AddressOf UpdateFormText
Label1.BeginInvoke(mi)
Else
Label1.Text = myString
End If
End Sub
Private Sub UpdateFormText()
Label1.Text = myString + " (BeginInvoked)"
End Sub
End Class
A better answer in VS2010 would use an inline MethodInvoker instead of the second function:
Me.Invoke(CType(Sub() Me.Label1.Text = "This is my string", MethodInvoker))
Related
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.
I am working in Visual Studio 2019 and writing in Visual Basic. I get this error: Cross-thread operation not valid: Control 'lblTesting' accessed from a thread other than the thread it was created on.' (lblTesting is just a label for testing purposes).
I call the FrmContacts like this:
Private Sub ContactsToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ContactsToolStripMenuItem.Click
Me.Enabled = False
FrmContacts.Show()
End Sub
and return from the form like this:
Private Sub FrmContacts_Closed(sender As Object, e As EventArgs) Handles Me.Closed
Me.Close()
FrmMain.Enabled = True
End Sub
The error pops up when I exit FrmContacts.
I tried deleting the form and recreating it but the error is there as soon as I add any control. I am dumbfounded. Searching has not revealed any hints.
I had to take out the Me.Close() because I was closing a closed form. The new code works fine. It is:
Private Sub FrmContacts_Closed(sender As Object, e As EventArgs) Handles Me.Closed
FrmMain.Enabled = True
End Sub
Also, if your intention is to make the "main" form inaccessible until the "child" form is closed, then simply use ShowDialog() instead:
You can use this method to display a modal dialog box in your
application. When this method is called, the code following it is not
executed until after the dialog box is closed.
Private Sub ContactsToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ContactsToolStripMenuItem.Click
FrmContacts.ShowDialog()
End Sub
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.
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.
I am trying to make a download manager for my program. But when I run this code it gives me the message box say "Download Started" but that is it. I do not get any file downloaded or progress bar change? Does anyone know why?
Public Class frmDownloader
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
WebClient1.DownloadFileAsync(New Uri("https://s3.amazonaws.com/MinecraftDownload/launcher/Minecraft_Server.exe"), "C:\hi.exe")
MsgBox("download started")
End Sub
Private Sub WebClient1_DownloadProgressChanged(ByVal sender As Object, ByVal e As System.Net.DownloadProgressChangedEventArgs) Handles WebClient1.DownloadProgressChanged
ProgressBar1.Value = e.ProgressPercentage
MsgBox("Download Progress Changed")
End Sub
End Class
The DownloadProgressChanged method is invoked on a different thread than the one that started the download. Inside this callback you seem to be manipulating some GUI element: ProgressBar1.Value. You should never manipulate GUI elements on different threads than the one on which they were created or you might get an exception. Depending on the type of application you are working on there are different ways to marshal calls on the GUI thread. For example in WinForms you should use the Control.BeginInvoke method. In WPF and Silverlight the equivalent is the Dispatcher.BeginInvoke.