I wanted to create an application with two forms, a main one called Source and a subsidiary called Viewer. Using buttons on each form, the user could switch to the other, even if that form had previously been closed. So I set my program to Close When Last Form Closes. Moreover, I added message boxes when each form closed, just to check that the user was sure.
But now my application won't exit after I close both forms! It shows up in Task Manager, and if I run it in MSVS, the debugger never stops! When each form is closed, My.Application.Forms returns an empty collection. And if I force quit it using End/Application.Exit, the program still quits — see the MWE below. What do I do?
MWE
Create a blank VB.NET Windows Forms project. In the "Application" tab of your project's settings, choose "Close When Last Form Closes." In the designer, create a Form Source with one button.
Public Class Source
Public Sub Switch(sender As Object, e As EventArgs) Handles Button1.Click
My.Forms.Viewer.Show()
End Sub
Public Sub Free(sender As Object, e As FormClosingEventArgs) Handles Me.Closing
e.Cancel=(MsgBoxResult.Cancel=MsgBox("Are you sure?"))
End Sub
'Public Sub Test(sender As Object, e As EventArgs) Handles Me.Closed
'Debug.Assert(Not My.Application.Forms.Count))
'If the next line is uncommented, the application will close, like we want
'If Not My.Forms.Viewer.Visible Then Application.Exit()
'End Sub
End Class
Then create an identical form called Viewer.
Public Class Viewer
Public Sub Switch(sender As Object, e As EventArgs) Handles Button1.Click
My.Forms.Source.Show()
End Sub
Public Sub Free(sender As Object, e As FormClosingEventArgs) Handles Me.Closing
e.Cancel=(MsgBoxResult.Cancel=MsgBox("Are you sure?"))
End Sub
'Public Sub Test(sender As Object, e As EventArgs) Handles Me.Closed
'Debug.Assert(Not My.Application.Forms.Count))
'If the next line is uncommented, the application will close, like we want
'If Not My.Forms.Source.Visible Then Application.Exit()
'End Sub
End Class
Try closing any form last. Neither will cause the app to quit.
Don't use Form.Closing and Form.Closed
In MSDN's page on Form.Closing, it remarks:
Caution
The Closing event is obsolete in the .NET Framework version 2.0 [and above]; use the FormClosing event instead.
The events were deprecated because
The Form.Closed and Form.Closing events are not raised when the Application.Exit method is called to exit your application. If you have validation code in either of these events that must be executed, you should call the Form.Close method for each open form individually before calling the Exit method.
The MSDN documentation nowhere states this, but the reverse is true: in order for your form to signal to the VB.NET runtime that the form closed, Form.Closing and Form.Closed cannot be subscribed to.
If you replace Me.Closing with Me.FormClosing and Me.Closed with Me.FormClosed in your MWE, it works.
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 dialog form which is a bar code scanner handler form that has events on the form it was called from, done like this:
Public Class FRMCheckout
Dim WithEvents Batch_Scanner_Dialog As New CheckoutBatchScanner
Private Sub Recieve_Scaned_Object(Scan_Object As tructures.ScanDetails) Handles Batch_Scanner_Dialog.Scanned_Item
'.....Do Stuff'
End Sub
Private Sub Button1_Click_1(sender As Object, e As EventArgs) Handles Button1.Click
Batch_Scanner_Dialog.Show()
End Sub
End Class
The Batch_Scanner_Dialog is closed with just the regular old Close.Me which opbviously disposes itself.
The problem being if you wish to open the dialog again, an accessing a disposed object exception is thrown.
Locally Declaring the Dialog will not work, because it has events, so how could I fix this issue? Calling a new instance of the dialog is fine, just the original Events should be on the calling form. (They vary depending on the form the dialog is called form)
(Note: I need to use .show not .showdialog to continue to run code on the original form.)
OK the problem was solved with:
Private Sub Button1_Click_1(sender As Object, e As EventArgs) Handles Button1.Click
If Batch_Scanner_Dialog.IsDisposed Then
Dim Batch_Scanner_Dialog As New CheckoutBatchScanner
AddHandler Batch_Scanner_Dialog.Scanned_Item, AddressOf Recieve_Scaned_Object
Batch_Scanner_Dialog.Show()
Else
Batch_Scanner_Dialog.Show()
End If
End Sub
I have created a form that allow users to close a form by clicking anywhere on the enlarged picture form (There are 3 objects to consider) and go back to the other form, which is called: "frmPhone". There's an actual picture on the form: "frmPhonePics" which is what I'm using to accomplish what I'm trying to do (was unable to insert an image on here. Sorry.) What I want to do is write a single click event to close the large picture form to allow the user to close it absolutely anywhere in the form, but I don't know how to do that. Here's the code I have so far:
Private Sub frmPhonePics_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Click
frmPhone.Show()
Me.Hide()
End Sub
It sounds as though you have a picture on your frmPhonePics form. If you double click that (from the VBA editor), you should be taken to the code - for example, you might see
Private Sub Image1_Click()
End Sub
Now all you have to do is add your code there:
Private Sub Image1_Click()
Me.Hide
frmPhone.Show()
End Sub
Note - the order matters, since frmPhone.Show() will "hijack" the code flow until it's dismissed, and in your code Me.Hide will not execute (so the form will not close) until frmPhone has been dismissed.
You can map the click handler for various object to one thing, if that is what you are asking:
Private Sub frmPhonePics_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Click, Handles picLarge.Click, Handles otherThing.Click
frmPhone.Show()
Me.Hide() ' should be Me.Close?
End Sub
Not sure why it is MyBase.Click in your code instead of Me.Click. Is this a subclassed form?
I'd strongly suggest using a DoubleClick instead of a single Click. The chances of an errant click doing the wrong thing is very great.
The easiest way is right from the designer. Write the sub routine, then for each control, in the properties window, click the events icon(thunderbolt) and assign the sub routine to the double-click event.
Alternatively, dispense with the Handles clause completely and use a series of Addhandler statements in the Load event handler. If you put a unique string in the names of the controls or if it's all the controls, you can iterate through the controls and use one addhandler statement for all of them
For Each c As Control In Me.Controls
AddHandler c.DoubleClick, AddressOf Ctrl_DoubleClick
Next
Private Sub Ctrl_DoubleClick(sender As Object, e As EventArgs)
'Do stuff
End Sub
Hans Passant gave me a great answer here, so I thought of asking for more details to try to understand the way Application.Run() works.
As far as I understand from the docs, it seems that Application.Run() starts a message loop on the current thread, which in turns enables it to process user input (Is that right?). The overloaded version Application.Run(Form) basically does the same, only it exists when the form closes, and it shows the form by default.
That raises a few questions:
How would one do to simply call from the Main() sub a function that can communicate with the user to (message boxes and so on) and wait for it to exit?
When the message loop is started without a form, how do you launch a new form from this loop, and wait for it to exit? ShowDialog could work, unless you don't want the form to display immediately when launched (eg. if you have a for that's launched minimized to the system tray)
Basically, the situation would be as follows: sub `Main` has a list of tasks to execute in 20mn, with a system tray icon telling the user that the program will operate in 20mn. A timer ticks after 20mns, and has to execute say approx. 15 tasks one by one, every time creating an instance of a progress dialog, initially hidden in the taskbar.
`ShowDialog` would display the form, which is not wanted; so the way I would do it would be to pass the progress dialog a callback to a function that starts the next task. But that wouldn't exit the first progress form before the second has exited, would it? Which means 15 forms would end up being opened...
So the solution may be to invoke (begininvoke?) the callback on the main application loop... Only, I don't know how to do this, because I don't have a form associated with the loop to invoke the callback on...
I hope my questions are clear (I might confuse many things, sorry),
Thanks,
CFP.
Drop a Timer, ProgressBar and a BackgroundWorker on the form. First thing you'll want to do is to prevent the form from getting visible when the program is started. Paste this code into the form class:
Protected Overrides Sub SetVisibleCore(ByVal value As Boolean)
If Not Me.IsHandleCreated Then
value = False
Me.CreateHandle
End If
MyBase.SetVisibleCore(value)
End Sub
Use the timer to get the job started. Set its Interval and Enabled properties, add the Tick event handler:
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Me.Show()
ProgressBar1.Visible = True
Me.Enabled = False
BackgroundWorker1.RunWorkerAsync()
End Sub
That makes the form visible when the job is started and starts the background worker. Set the BGW's WorkerReportsProgress property to True and add the 3 event handlers:
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'' Do stuff here, call BackgroundWorker1.ReportProgress to update the PB
End Sub
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
ProgressBar1.Value = e.ProgressPercentage
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
ProgressBar1.Visible = False
Me.Enabled = True
Me.Hide()
End Sub
It is up to you to fill in the code for the DoWork event handler. Have it do those 15 jobs, be sure to call BackgroundWorker1.ReportProgess so that the progress bar gets updated. Which is what the ProgressChanged event handler does. The RunWorkerCompleted event handler hides the form again.
You can call the Show() method in the context menu item event for the NotifyIcon so that the user can make your form visible again. Call Application.Exit() in the context menu item that allow the user to quit your app. Make sure you disable that when the BGW is running. Or implement a way to cleanly stop the job.