VB.Net code.
I have a program where I am running a process in a thread and in that thread I need to have a pop up message information box that is non-modal. The main process is in a thread because it has to run in parallel and the user can initiate this process many times at the same time.
I read that the modal message box needs to be a custom form that is also ran from a thread to not block the program from continuing on. such as .Show() stops the program and waits for the user input. And you have to use .ShowDialog() via a thread
My code:
Calling initial thread:
Public Event Report As EventHandler
'In a method
Task.Run(Function() BackgroundThread())
Private Function BackgroundThread() As Task()
RaiseEvent Report(Me, New System.EventArgs)
End Function
In the Report method I have a snippet of code that then calls the form window to pop up the modal window:
Private mDiaplayMessageBox As NonModalPopUp
Private Sub DisplayMessageBox()
mDiaplayMessageBox = New NonModalPopUp()
Task.Run(Sub() mDiaplayMessageBox.ShowDialog())
End Sub
The issue I am having is that when I am finished with the report method I want to close this popup message. But when there is more than one of these pop up windows open at a time, only the last window opened will close and the program loses the handle I think to the other pop up windows and they will not close.
To close the windows I have in the modal form this code
Public Sub CloseMe()
'This will grab the thread that this window is running on, solves Cross-Threading issue.
If Me.InvokeRequired Then
Me.Invoke(New MethodInvoker(AddressOf CloseMe))
Exit Sub
End If
Me.BackColor = Color.Red
Me.Close()
End Sub
This first time this code is called its will hit the Me.Invoke and then close the window. However, on any subsequent calls when it gets to Me.InvokeRequired this will then be set to false, not called the Me.Invoke and go to the Me.Close() but it will not close the window.
I tried to do something where I grab the Handle intptr value but when ever I vent just look at that value the program immediately throws a cross-threading exception.
All I want to do is close the other windows which does not seem like a hard task but I do not know what I am missing.
One of approaches you can follow to achieve your goal might be as code below shows:
You can create a custom event which you can use as a “call” to listen to for the closure of your form.
Public Class Form1
Dim frm2 As Form2
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
frm2 = New Form2
Task.Run(Sub()
AddHandler CloseFrm2, Sub()
Dim CloseMe As Action = Sub()
frm2.Close()
frm2.Dispose()
End Sub
If frm2.InvokeRequired Then
frm2.Invoke(Sub() CloseMe())
Else
CloseMe()
End If
End Sub
frm2.ShowDialog()
End Sub)
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
RaiseEventCloseFrm2()
End Sub
End Class
Module EventHelper
Public Event CloseFrm2()
Sub RaiseEventCloseFrm2()
RaiseEvent CloseFrm2()
End Sub
End Module
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'm new to Visual Basic and overall kind of new to coding in general.
Currently I work on a program which uses a filewatcher. But If I try this:
Public Class Form1
Private WithEvents fsw As IO.FileSystemWatcher
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
fsw = New IO.FileSystemWatcher("PATH")
fsw.EnableRaisingEvents = True
' fsw.Filter = "*.settings"
End Sub
Private Sub GetSettingsFromFile()
Some Code
More Code
CheckBox1.Checked = True
End Sub
Private Sub fsw_Changed(sender As Object, e As FileSystemEventArgs) Handles fsw.Changed
fsw.EnableRaisingEvents = False 'this is set because the file is changed many times in rapid succesion so I need to stop the Filewatcher from going of 200x (anyone has a better idea to do this?)
Threading.Thread.Sleep(100)
GetSettingsFromFile()
fsw.EnableRaisingEvents = True 'enabling it again
End Sub
End Class
But when I do this (trying to change anyhting in the form) I get this error:
System.InvalidOperationException (WinForms.IllegalCrossThreadCall)
It wont stop the program from working, but I want to understand what is wrong here and why the debugger is throwing this at me
regards
The event is being raised on a secondary thread. Any changes to the UI must be made on the UI thread. You need to marshal a method call to the UI thread and update the UI there. Lots of information around on how to do that. Here's an example:
Private Sub UpdateCheckBox1(checked As Boolean)
If CheckBox1.InvokeRequired Then
'We are on a secondary thread so marshal a method call to the UI thread.
CheckBox1.Invoke(New Action(Of Boolean)(AddressOf UpdateCheckBox1), checked)
Else
'We are on the UI thread so update the control.
CheckBox1.Checked = checked
End If
End Sub
Now you simply call that method wherever you are and whatever thread you're on. If you're already on the UI thread then the control will just be updated. If you're on a secondary thread then the method will invoke itself a second time, this time on the UI thread, and the control will be updated in that second invocation.
I'm currently working on a small auto-update project for my company. After some research on multi-threading, I manage to built up the code below :
Thread #01 :
Private Sub startUpdate()
If InvokeRequired Then
Invoke(New FTPDelegate(AddressOf startUpdate))
Else
'some code here
End If
End Sub
Thread #02 which is joined by thread #01 :
Private Sub startProcess()
myThread = New Thread(Sub() startUpdate())
myThread.Start()
myThread.Join()
'another code goes here
Me.close
End Sub
And thread #02 is accessed when the form loads :
Private Sub SUpdater_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
myThread1 = New Thread(Sub() startProcess())
myThread1.Start()
End Sub
There are 2 things which I'm stuck with :
I can't access Me.close from thread #01. It fires an error:
Control is in another thread
The main form froze even though I called another thread.
Please help me fix this error.
Thank you very much.
Invocation is required every time you are to access UI elements. Calling Me.Close() starts to dispose all the form's elements (components, buttons, labels, textboxes, etc.), causing interaction with both the form itself, but also everything in it.
The only things you are not required to invoke for are properties that you know doesn't modify anything on the UI when get or set, and also fields (aka variables).
This, for example, would not need to be invoked:
Dim x As Integer = 3
Private Sub Thread1()
x += 8
End Sub
To fix your problem you just need to invoke the closing of the form. This can be done simply using a delegate.
Delegate Sub CloseDelegate()
Private Sub Thread1()
If Me.InvokeRequired = True Then 'Always check this property, if invocation is not required there's no meaning doing so.
Me.Invoke(New CloseDelegate(AddressOf Me.Close))
Else
Me.Close() 'If invocation is not required.
End If
End Sub
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
I converted this app from VB6. I have 2 forms. Form1 instantiates Form2 via a Menu Item.
I am having trouble getting Form2 to end when clicking close (X). If Form2 is 'idle' it closes fine; but if I am in a loop processing anything all the events fire, but it continues processing in Form2. I've tried messing with Dispose, Close, Application.Exit, Application.ExitThread. My last attempt was creating my own event to fire back to Form1 and dispose Form2 -- and it hits it but Form2 is still running. What is the deal? BTW if I use just Show vs ShowDialog -- Form2 just blinks and disappears.
Form1 does this
Dim f2 as Import
:
Hide()
f2 = New Import
AddHandler f2.die, AddressOf killf2
f2.ShowDialog(Me)
Show()
Private Sub killf2()
f2.Dispose()
f2 = Nothing
End Sub
Form2
Public Event die()
Private Shadows Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
Dispose()
Close()
e.Cancel = False
RaiseEvent die()
End Sub
I think you've got your events crossed. You want form1, containing an instance of form2, to listen for form2's form_closing event. Then you can set f2 = nothing.
Form1 should fully enclose form2.
here's an example:
Public Class MDIMain
Private WithEvents _child As frmViewChild
Friend Sub viewChildShow()
_child = New frmViewChild
_child.MdiParent = Me
_child.WindowState = FormWindowState.Maximized
_child.Show()
End Sub
Private Sub _child_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles _child.FormClosing
_child = Nothing
End Sub
don't add anything to form2, try
Dim f2 as Import
Hide()
f2 = New Import
f2.ShowDialog(Me)
Show()
Private Sub f2_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles f2.FormClosing
set f2 = nothing
End Sub
re: your comment
it goes back to form2 and continues processing the next statement in the the click event handler
that's a feature and it will cause this behavior. you need to make sure me.close or close me is the last statement in form2, that there's nothing else to execute.
What is this loop you speak of? The user interface (windows) is separate from any code that is running. In your class derived form Form, Code is allowed to run, both before the Form is created, and after the Form is destroyed. If the code tries to access user-interface objects then an exception might occur, but otherwise there is nothing stopping your code from running when there is no user interface.
If you want your "for" loop to exit then you must send it a signal somehow, e.g. by creating a boolean "quit" member variable. Set "quit=True" when your form closes, then have your "for" loop check whether it is true.